Add offset plane point-and-click user flow (#4552)
* Add a code mod for offset plane * Add support for default plane selections to our `otherSelections` object * Make availableVars work without a selection range (because default planes don't have one) * Make default planes selectable in cmdbar even if AST is empty * Add offset plane command and activate in toolbar * Avoid unnecessary error when sketching on offset plane by returning early * Add supporting test features for offset plane E2E test * Add WIP E2E test for offset plane Struggling to get local electron test suite running properly * Typos * Lints * Fix test by making it a web-based one: I couldn't use the cmdBar fixture with an electron test for some reason. * Update src/lib/commandBarConfigs/modelingCommandConfig.ts * Update src/machines/modelingMachine.ts * Revert changes to `homePageFixture`, as they were unused * @Irev-Dev feedback: convert action to actor, fix machine layout * Update plane icon to be not dashed, follow conventions closer
This commit is contained in:
@ -35,7 +35,7 @@ export class CmdBarFixture {
|
||||
}
|
||||
|
||||
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
|
||||
const reviewForm = await this.page.locator('#review-form')
|
||||
const reviewForm = this.page.locator('#review-form')
|
||||
const getHeaderArgs = async () => {
|
||||
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
|
||||
const entries = await Promise.all(
|
||||
|
@ -6,6 +6,7 @@ export class ToolbarFixture {
|
||||
public page: Page
|
||||
|
||||
extrudeButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
lineBtn!: Locator
|
||||
rectangleBtn!: Locator
|
||||
@ -25,6 +26,7 @@ export class ToolbarFixture {
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||
|
@ -551,3 +551,53 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Offset plane point-and-click`, async ({
|
||||
app,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await app.initialise()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 150 }
|
||||
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
|
||||
|
||||
await test.step(`Look for the blue of the XZ plane`, async () => {
|
||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||
})
|
||||
await test.step(`Go through the command bar flow`, async () => {
|
||||
await toolbar.offsetPlaneButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'plane',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Plane: '', Distance: '' },
|
||||
highlightedHeaderArg: 'plane',
|
||||
commandName: 'Offset plane',
|
||||
})
|
||||
await clickOnXzPlane()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'distance',
|
||||
currentArgValue: '5',
|
||||
headerArguments: { Plane: '1 plane', Distance: '' },
|
||||
highlightedHeaderArg: 'distance',
|
||||
commandName: 'Offset plane',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await editor.expectEditor.toContain(expectedOutput)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [expectedOutput],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { Axis, NonCodeSelection } from 'lib/selections'
|
||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { CameraControls } from './CameraControls'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
@ -654,7 +654,7 @@ export class SceneInfra {
|
||||
await this.onClickCallback({ mouseEvent, intersects })
|
||||
}
|
||||
}
|
||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||
updateOtherSelectionColors = (otherSelections: NonCodeSelection[]) => {
|
||||
const axisGroup = this.scene.children.find(
|
||||
({ userData }) => userData?.type === AXIS_GROUP
|
||||
)
|
||||
|
@ -1,21 +1,26 @@
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { Artifact } from 'lang/std/artifactGraph'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
canSubmitSelectionArg,
|
||||
getSelectionType,
|
||||
getSelectionCountByType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const semanticEntityNames: { [key: string]: Array<Artifact['type']> } = {
|
||||
const semanticEntityNames: {
|
||||
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
|
||||
} = {
|
||||
face: ['wall', 'cap', 'solid2D'],
|
||||
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||
point: [],
|
||||
plane: ['defaultPlane'],
|
||||
}
|
||||
|
||||
function getSemanticSelectionType(selectionType: Array<Artifact['type']>) {
|
||||
@ -43,21 +48,13 @@ function CommandBarSelectionInput({
|
||||
stepBack: () => void
|
||||
onSubmit: (data: unknown) => void
|
||||
}) {
|
||||
const { code } = useKclContext()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const selectionsByType = useMemo(() => {
|
||||
const selectionRangeEnd = !selection
|
||||
? null
|
||||
: selection?.graphSelections[0]?.codeRef?.range[1]
|
||||
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
|
||||
? 'none'
|
||||
: !selection
|
||||
? 'none'
|
||||
: getSelectionType(selection)
|
||||
}, [selection, code])
|
||||
return getSelectionCountByType(selection)
|
||||
}, [selection])
|
||||
const canSubmitSelection = useMemo<boolean>(
|
||||
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||
[selectionsByType]
|
||||
@ -67,6 +64,30 @@ function CommandBarSelectionInput({
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
||||
// Show the default planes if the selection type is 'plane'
|
||||
useEffect(() => {
|
||||
if (arg.selectionTypes.includes('plane') && !canSubmitSelection) {
|
||||
toSync(() => {
|
||||
return Promise.all([
|
||||
kclManager.showPlanes(),
|
||||
kclManager.setSelectionFilter(['plane', 'object']),
|
||||
])
|
||||
}, reportRejection)()
|
||||
}
|
||||
|
||||
return () => {
|
||||
toSync(() => {
|
||||
const promises = [
|
||||
new Promise(() => kclManager.defaultSelectionFilter()),
|
||||
]
|
||||
if (!kclManager._isAstEmpty(kclManager.ast)) {
|
||||
promises.push(kclManager.hidePlanes())
|
||||
}
|
||||
return Promise.all(promises)
|
||||
}, reportRejection)()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
@ -109,11 +130,15 @@ function CommandBarSelectionInput({
|
||||
{arg.warningMessage}
|
||||
</p>
|
||||
)}
|
||||
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
||||
{arg.name}
|
||||
</span>
|
||||
<input
|
||||
id="selection"
|
||||
name="selection"
|
||||
ref={inputRef}
|
||||
required
|
||||
data-testid="cmd-bar-arg-value"
|
||||
placeholder="Select an entity with your mouse"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
|
||||
onKeyDown={(event) => {
|
||||
|
@ -818,15 +818,16 @@ const CustomIconMap = {
|
||||
),
|
||||
plane: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="plane"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.92871 5.11391L4.43964 5.00995V4.10898V3.60898V3.10898L4.92871 3.21293L5.41778 3.31689L6.29907 3.50421V4.00421V4.50421L5.41778 4.31689V5.21786L4.92871 5.11391ZM11.8774 4.68991L8.1585 3.89945V4.39945V4.89945L11.8774 5.68991V5.18991V4.68991ZM13.7368 5.08515V5.58515V6.08515L14.6181 6.27247V7.17344L15.1071 7.2774L15.5962 7.38135V6.48038V5.98038V5.48038L15.1071 5.37643L14.6181 5.27247L13.7368 5.08515ZM15.5962 9.28233L15.1071 9.17837L14.6181 9.07441V12.8764L15.1071 12.9803L15.5962 13.0843V9.28233ZM15.5962 14.9852L15.1071 14.8813L14.6181 14.7773V15.6783L13.7368 15.491V15.991V16.491L14.6181 16.6783L15.1071 16.7823L15.5962 16.8862V16.3862V15.8862V14.9852ZM11.8774 16.0957V15.5957V15.0957L8.1585 14.3053V14.8053V15.3053L11.8774 16.0957ZM6.29907 14.91V14.41V13.91L5.41778 13.7227V12.8217L4.92871 12.7178L4.43964 12.6138V13.5148V14.0148V14.5148L4.92871 14.6188L5.41778 14.7227L6.29907 14.91ZM4.43964 10.7129L4.92871 10.8168L5.41778 10.9208V7.11883L4.92871 7.01488L4.43964 6.91092V10.7129Z"
|
||||
d="M10.9781 5.49876L14.6181 6.27247V9.99381L10.9781 9.22011V5.49876ZM10 4.29085L10.9781 4.49876L14.6181 5.27247L14.6182 5.27247L15.5963 5.48038H15.5963V6.48038V10.2017V11.2017L15.5963 11.2017V15.8862V16.8862L14.6181 16.6783L5.41784 14.7227L4.4397 14.5148V13.5148V4.10898V3.10898L5.41784 3.31689L10 4.29085ZM14.6181 10.9938V15.6783L5.41784 13.7227V4.31689L10 5.29085V9.0122V10.0122L10.9781 10.2201L14.6181 10.9938Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -317,6 +317,7 @@ export const ModelingMachineProvider = ({
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let selections: Selections = {
|
||||
graphSelections: [],
|
||||
otherSelections: [],
|
||||
@ -375,7 +376,10 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (setSelections.selectionType === 'otherSelection') {
|
||||
if (
|
||||
setSelections.selectionType === 'axisSelection' ||
|
||||
setSelections.selectionType === 'defaultPlaneSelection'
|
||||
) {
|
||||
if (editorManager.isShiftDown) {
|
||||
selections = {
|
||||
graphSelections: selectionRanges.graphSelections,
|
||||
@ -387,20 +391,11 @@ export const ModelingMachineProvider = ({
|
||||
otherSelections: [setSelections.selection],
|
||||
}
|
||||
}
|
||||
const { engineEvents, updateSceneObjectColors } =
|
||||
handleSelectionBatch({
|
||||
selections: selections,
|
||||
})
|
||||
engineEvents &&
|
||||
engineEvents.forEach((event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand(event)
|
||||
})
|
||||
updateSceneObjectColors()
|
||||
return {
|
||||
selectionRanges: selections,
|
||||
}
|
||||
}
|
||||
|
||||
if (setSelections.selectionType === 'completeSelection') {
|
||||
editorManager.selectRange(setSelections.selection)
|
||||
if (!sketchDetails)
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
import { useRouteLoaderData } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -30,6 +31,7 @@ export const Stream = () => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state, send } = useModelingContext()
|
||||
const { commandBarState } = useCommandsContext()
|
||||
const { mediaStream } = useAppStream()
|
||||
const { overallState, immediateState } = useNetworkContext()
|
||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||
@ -260,7 +262,15 @@ export const Stream = () => {
|
||||
if (!videoRef.current) return
|
||||
// If we're in sketch mode, don't send a engine-side select event
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches({ idle: 'showPlanes' })) return
|
||||
// Only respect default plane selection if we're on a selection command argument
|
||||
if (
|
||||
state.matches({ idle: 'showPlanes' }) &&
|
||||
!(
|
||||
commandBarState.matches('Gathering arguments') &&
|
||||
commandBarState.context.currentArgument?.inputType === 'selection'
|
||||
)
|
||||
)
|
||||
return
|
||||
// If we're mousing up from a camera drag, don't send a select event
|
||||
if (sceneInfra.camControls.wasDragging === true) return
|
||||
|
||||
|
@ -169,6 +169,7 @@ export function useEngineConnectionSubscriptions() {
|
||||
pathToNode: artifact.codeRef.pathToNode,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Artifact is likely an extrusion face
|
||||
|
@ -23,6 +23,7 @@ import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
|
||||
interface ExecuteArgs {
|
||||
ast?: Node<Program>
|
||||
@ -281,7 +282,7 @@ export class KclManager {
|
||||
this.lints = await lintAst({ ast: ast })
|
||||
|
||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||
defaultSelectionFilter(execState.memory, this.engineCommandManager)
|
||||
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
|
||||
|
||||
if (args.zoomToFit) {
|
||||
let zoomObjectId: string | undefined = ''
|
||||
@ -568,8 +569,13 @@ export class KclManager {
|
||||
}
|
||||
return Promise.all(thePromises)
|
||||
}
|
||||
/** TODO: this function is hiding unawaited asynchronous work */
|
||||
defaultSelectionFilter() {
|
||||
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
|
||||
setSelectionFilterToDefault(this.programMemory, this.engineCommandManager)
|
||||
}
|
||||
/** TODO: this function is hiding unawaited asynchronous work */
|
||||
setSelectionFilter(filter: EntityType_type[]) {
|
||||
setSelectionFilter(filter, this.engineCommandManager)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -591,18 +597,35 @@ export class KclManager {
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSelectionFilter(
|
||||
const defaultSelectionFilter: EntityType_type[] = [
|
||||
'face',
|
||||
'edge',
|
||||
'solid2d',
|
||||
'curve',
|
||||
'object',
|
||||
]
|
||||
|
||||
/** TODO: This function is not synchronous but is currently treated as such */
|
||||
function setSelectionFilterToDefault(
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
programMemory.hasSketchOrSolid() &&
|
||||
setSelectionFilter(defaultSelectionFilter, engineCommandManager)
|
||||
}
|
||||
|
||||
/** TODO: This function is not synchronous but is currently treated as such */
|
||||
function setSelectionFilter(
|
||||
filter: EntityType_type[],
|
||||
engineCommandManager: EngineCommandManager
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter: ['face', 'edge', 'solid2d', 'curve'],
|
||||
filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -527,6 +527,45 @@ export function sketchOnExtrudedFace(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an offset plane to the AST
|
||||
*/
|
||||
export function addOffsetPlane({
|
||||
node,
|
||||
defaultPlane,
|
||||
offset,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
defaultPlane: DefaultPlaneStr
|
||||
offset: Expr
|
||||
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
const newPlaneName = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
|
||||
|
||||
const newPlane = createVariableDeclaration(
|
||||
newPlaneName,
|
||||
createCallExpressionStdLib('offsetPlane', [
|
||||
createLiteral(defaultPlane.toUpperCase()),
|
||||
offset,
|
||||
])
|
||||
)
|
||||
|
||||
modifiedAst.body.push(newPlane)
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the AST to create a new sketch using the variable declaration
|
||||
* of an offset plane. The new sketch just has to come after the offset
|
||||
|
@ -40,6 +40,10 @@ export type ModelingCommandSchema = {
|
||||
selection: Selections
|
||||
radius: KclCommandValue
|
||||
}
|
||||
'Offset plane': {
|
||||
plane: Selections
|
||||
distance: KclCommandValue
|
||||
}
|
||||
'change tool': {
|
||||
tool: SketchTool
|
||||
}
|
||||
@ -276,6 +280,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
'Offset plane': {
|
||||
description: 'Offset a plane.',
|
||||
icon: 'plane',
|
||||
args: {
|
||||
plane: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['plane'],
|
||||
multiple: false,
|
||||
required: true,
|
||||
skip: true,
|
||||
},
|
||||
distance: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_LENGTH,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Fillet: {
|
||||
description: 'Fillet edge',
|
||||
icon: 'fillet',
|
||||
|
@ -54,6 +54,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||
EXTRUDE: 'extrude',
|
||||
SEGMENT: 'seg',
|
||||
REVOLVE: 'revolve',
|
||||
PLANE: 'plane',
|
||||
} as const
|
||||
/** The default KCL length expression */
|
||||
export const KCL_DEFAULT_LENGTH = `5`
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from 'lang/queryAst'
|
||||
import { CommandArgument } from './commandTypes'
|
||||
import {
|
||||
DefaultPlaneStr,
|
||||
getParentGroup,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
@ -46,6 +47,10 @@ export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
|
||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
||||
export type DefaultPlaneSelection = {
|
||||
name: DefaultPlaneStr
|
||||
id: string
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link Artifact} instead. */
|
||||
type Selection__old =
|
||||
@ -72,9 +77,11 @@ type Selection__old =
|
||||
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
|
||||
secondaryRange: SourceRange
|
||||
}
|
||||
export type NonCodeSelection = Axis | DefaultPlaneSelection
|
||||
|
||||
/** @deprecated Use {@link Selection} instead. */
|
||||
export type Selections__old = {
|
||||
otherSelections: Axis[]
|
||||
otherSelections: NonCodeSelection[]
|
||||
codeBasedSelections: Selection__old[]
|
||||
}
|
||||
export interface Selection {
|
||||
@ -82,7 +89,7 @@ export interface Selection {
|
||||
codeRef: CodeRef
|
||||
}
|
||||
export type Selections = {
|
||||
otherSelections: Array<Axis>
|
||||
otherSelections: Array<NonCodeSelection>
|
||||
graphSelections: Array<Selection>
|
||||
}
|
||||
|
||||
@ -172,11 +179,31 @@ export async function getEventForSelectWithPoint({
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'otherSelection',
|
||||
selectionType: 'axisSelection',
|
||||
selection: X_AXIS_UUID === data.entity_id ? 'x-axis' : 'y-axis',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check for default plane selection
|
||||
const foundDefaultPlane =
|
||||
engineCommandManager.defaultPlanes !== null &&
|
||||
Object.entries(engineCommandManager.defaultPlanes).find(
|
||||
([, plane]) => plane === data.entity_id
|
||||
)
|
||||
if (foundDefaultPlane) {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'defaultPlaneSelection',
|
||||
selection: {
|
||||
name: foundDefaultPlane[0] as DefaultPlaneStr,
|
||||
id: data.entity_id,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
|
||||
const codeRefs = getCodeRefsByArtifactId(
|
||||
data.entity_id,
|
||||
@ -207,7 +234,7 @@ export function getEventForSegmentSelection(
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'otherSelection',
|
||||
selectionType: 'axisSelection',
|
||||
selection: obj?.userData?.type === X_AXIS ? 'x-axis' : 'y-axis',
|
||||
},
|
||||
}
|
||||
@ -272,7 +299,6 @@ export function handleSelectionBatch({
|
||||
}): {
|
||||
engineEvents: Models['WebSocketRequest_type'][]
|
||||
codeMirrorSelection: EditorSelection
|
||||
otherSelections: Axis[]
|
||||
updateSceneObjectColors: () => void
|
||||
} {
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
@ -303,7 +329,6 @@ export function handleSelectionBatch({
|
||||
ranges,
|
||||
selections.graphSelections.length - 1
|
||||
),
|
||||
otherSelections: selections.otherSelections,
|
||||
updateSceneObjectColors: () =>
|
||||
updateSceneObjectColors(selections.graphSelections),
|
||||
}
|
||||
@ -314,7 +339,6 @@ export function handleSelectionBatch({
|
||||
0
|
||||
),
|
||||
engineEvents,
|
||||
otherSelections: selections.otherSelections,
|
||||
updateSceneObjectColors: () =>
|
||||
updateSceneObjectColors(selections.graphSelections),
|
||||
}
|
||||
@ -536,7 +560,8 @@ export function canSweepSelection(selection: Selections) {
|
||||
}
|
||||
|
||||
// This accounts for non-geometry selections under "other"
|
||||
export type ResolvedSelectionType = [Artifact['type'] | 'other', number]
|
||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||
|
||||
/**
|
||||
* In the future, I'd like this function to properly return the type of each selected entity based on
|
||||
@ -545,28 +570,48 @@ export type ResolvedSelectionType = [Artifact['type'] | 'other', number]
|
||||
* @param selection
|
||||
* @returns
|
||||
*/
|
||||
export function getSelectionType(
|
||||
export function getSelectionCountByType(
|
||||
selection?: Selections
|
||||
): ResolvedSelectionType[] {
|
||||
if (!selection) return []
|
||||
const selectionsWithArtifacts = selection.graphSelections.filter(
|
||||
(s) => !!s.artifact
|
||||
): SelectionCountsByType | 'none' {
|
||||
const selectionsByType: SelectionCountsByType = new Map()
|
||||
if (
|
||||
!selection ||
|
||||
(!selection.graphSelections.length && !selection.otherSelections.length)
|
||||
)
|
||||
const firstSelection = selectionsWithArtifacts[0]
|
||||
const firstSelectionType = firstSelection?.artifact?.type
|
||||
if (!firstSelectionType) return []
|
||||
const selectionsWithSameType = selectionsWithArtifacts.filter(
|
||||
(s) => s.artifact?.type === firstSelection.artifact?.type
|
||||
)
|
||||
return [[firstSelectionType, selectionsWithSameType.length]]
|
||||
return 'none'
|
||||
|
||||
function incrementOrInitializeSelectionType(type: ResolvedSelectionType) {
|
||||
const count = selectionsByType.get(type) || 0
|
||||
selectionsByType.set(type, count + 1)
|
||||
}
|
||||
|
||||
selection.otherSelections.forEach((selection) => {
|
||||
if (typeof selection === 'string') {
|
||||
incrementOrInitializeSelectionType('other')
|
||||
} else if ('name' in selection) {
|
||||
incrementOrInitializeSelectionType('plane')
|
||||
}
|
||||
})
|
||||
|
||||
selection.graphSelections.forEach((selection) => {
|
||||
if (!selection.artifact) {
|
||||
incrementOrInitializeSelectionType('other')
|
||||
return
|
||||
}
|
||||
incrementOrInitializeSelectionType(selection.artifact.type)
|
||||
})
|
||||
|
||||
return selectionsByType
|
||||
}
|
||||
|
||||
export function getSelectionTypeDisplayText(
|
||||
selection?: Selections
|
||||
): string | null {
|
||||
const selectionsByType = getSelectionType(selection)
|
||||
const selectionsByType = getSelectionCountByType(selection)
|
||||
if (selectionsByType === 'none') return null
|
||||
|
||||
return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
|
||||
return selectionsByType
|
||||
.entries()
|
||||
.map(
|
||||
// Hack for showing "face" instead of "extrude-wall" in command bar text
|
||||
([type, count]) =>
|
||||
@ -575,16 +620,17 @@ export function getSelectionTypeDisplayText(
|
||||
.replace('solid2D', 'face')
|
||||
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
|
||||
)
|
||||
.toArray()
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export function canSubmitSelectionArg(
|
||||
selectionsByType: 'none' | ResolvedSelectionType[],
|
||||
selectionsByType: 'none' | Map<ResolvedSelectionType, number>,
|
||||
argument: CommandArgument<unknown> & { inputType: 'selection' }
|
||||
) {
|
||||
return (
|
||||
selectionsByType !== 'none' &&
|
||||
selectionsByType.every(([type, count]) => {
|
||||
selectionsByType.entries().every(([type, count]) => {
|
||||
const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
|
||||
return (
|
||||
foundIndex !== -1 &&
|
||||
|
@ -252,10 +252,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
[
|
||||
{
|
||||
id: 'plane-offset',
|
||||
onClick: () =>
|
||||
console.error('Plane through normal not yet implemented'),
|
||||
onClick: ({ commandBarSend }) => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Offset plane', groupId: 'modeling' },
|
||||
})
|
||||
},
|
||||
hotkey: 'O',
|
||||
icon: 'plane',
|
||||
status: 'unavailable',
|
||||
status: 'available',
|
||||
title: 'Offset plane',
|
||||
description: 'Create a plane parallel to an existing plane.',
|
||||
links: [],
|
||||
|
@ -34,6 +34,8 @@ export function useCalculateKclExpression({
|
||||
} {
|
||||
const { programMemory, code } = useKclContext()
|
||||
const { context } = useModelingContext()
|
||||
// If there is no selection, use the end of the code
|
||||
// so all variables are available
|
||||
const selectionRange:
|
||||
| (typeof context)['selectionRanges']['graphSelections'][number]['codeRef']['range']
|
||||
| undefined = context.selectionRanges.graphSelections[0]?.codeRef?.range
|
||||
@ -72,11 +74,12 @@ export function useCalculateKclExpression({
|
||||
}, [programMemory, newVariableName])
|
||||
|
||||
useEffect(() => {
|
||||
if (!programMemory || !selectionRange) return
|
||||
if (!programMemory) return
|
||||
const varInfo = findAllPreviousVariables(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
selectionRange
|
||||
// If there is no selection, use the end of the code
|
||||
selectionRange || [code.length, code.length]
|
||||
)
|
||||
setAvailableVarInfo(varInfo)
|
||||
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user