Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions src/components/CommandBar/CommandBarSelectionMixedInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from '@src/lib/selections'
import { kclManager, engineCommandManager } from '@src/lib/singletons'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
import { coerceSelectionsToBody } from '@src/lang/std/artifactGraph'
import { err } from '@src/lib/trap'
import type { Selections } from '@src/machines/modelingSharedTypes'

const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
Expand All @@ -26,12 +28,53 @@ export default function CommandBarSelectionMixedInput({
const commandBarState = useCommandBarState()
const [hasSubmitted, setHasSubmitted] = useState(false)
const [hasAutoSkipped, setHasAutoSkipped] = useState(false)
const [hasCoercedSelections, setHasCoercedSelections] = useState(false)
const [hasClearedSelection, setHasClearedSelection] = useState(false)
const selection: Selections = useSelector(arg.machineActor, selectionSelector)

const selectionsByType = useMemo(() => {
return getSelectionCountByType(selection)
}, [selection])

// Coerce selections to bodies if this argument requires bodies
useEffect(() => {
// Only run once per component mount
if (hasCoercedSelections) return

// Mark as attempted to prevent infinite loops
setHasCoercedSelections(true)

if (!selection || selection.graphSelections.length === 0) return

// Check if this argument only accepts body types (path, sweep, compositeSolid)
// These are the artifact types that represent 3D bodies/objects
const onlyAcceptsBodies = arg.selectionTypes?.every(
(type) => type === 'sweep' || type === 'compositeSolid' || type === 'path'
)

if (onlyAcceptsBodies && arg.machineActor) {
const coercedSelections = coerceSelectionsToBody(
selection,
kclManager.artifactGraph
)

if (!err(coercedSelections)) {
// Immediately update the modeling machine state with coerced selection
// This needs to happen BEFORE the selection filter is applied
if (arg.machineActor) {
arg.machineActor.send({
type: 'Set selection',
data: {
selectionType: 'completeSelection',
selection: coercedSelections,
},
})
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only run on mount
}, [])

const isArgRequired =
arg.required instanceof Function
? arg.required(commandBarState.context)
Expand Down Expand Up @@ -81,11 +124,20 @@ export default function CommandBarSelectionMixedInput({
}, [arg.name])

// Set selection filter if needed, and reset it when the component unmounts
// This runs after coercion completes and updates the selection
useEffect(() => {
arg.selectionFilter && kclManager.setSelectionFilter(arg.selectionFilter)
return () => kclManager.defaultSelectionFilter(selection)
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: blanket-ignored fix me!
}, [arg.selectionFilter])
if (arg.selectionFilter && hasCoercedSelections) {
// Pass the current selection to restore it after applying the filter
// This is critical for body-only commands where we've coerced face/edge selections to bodies
kclManager.setSelectionFilter(arg.selectionFilter, selection)
}
return () => {
if (arg.selectionFilter && hasCoercedSelections) {
kclManager.defaultSelectionFilter(selection)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Need to react to selection changes after coercion
}, [arg.selectionFilter, selection, hasCoercedSelections])

// Watch for outside teardowns of this component
// (such as clicking another argument in the command palette header)
Expand Down
11 changes: 8 additions & 3 deletions src/lang/KclSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import type {
PlaneVisibilityMap,
Selections,
} from '@src/machines/modelingSharedTypes'
import { type handleSelectionBatch as handleSelectionBatchFn } from '@src/lib/selections'
import { handleSelectionBatch as handleSelectionBatchFn } from '@src/lib/selections'

interface ExecuteArgs {
ast?: Node<Program>
Expand Down Expand Up @@ -792,8 +792,13 @@ export class KclManager extends EventTarget {
setSelectionFilterToDefault(this.engineCommandManager, selectionsToRestore)
}
/** TODO: this function is hiding unawaited asynchronous work */
setSelectionFilter(filter: EntityType[]) {
setSelectionFilter(filter, this.engineCommandManager)
setSelectionFilter(filter: EntityType[], selectionsToRestore?: Selections) {
setSelectionFilter(
filter,
this.engineCommandManager,
selectionsToRestore,
handleSelectionBatchFn
)
}

// Determines if there is no KCL code which means it is executing a blank KCL file
Expand Down
185 changes: 185 additions & 0 deletions src/lang/std/artifactGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { describe, it, expect } from 'vitest'
import {

Check failure on line 2 in src/lang/std/artifactGraph.test.ts

View workflow job for this annotation

GitHub Actions / npm-lint

'./artifactGraph' import is restricted from being used by a pattern. Use absolute imports instead
coerceSelectionsToBody,
getSweepArtifactFromSelection,
type Artifact,
} from './artifactGraph'
import type { ArtifactGraph } from '@src/lang/wasm'
import type { Selections, Selection } from '@src/machines/modelingSharedTypes'

describe('getSweepArtifactFromSelection', () => {
it('should return sweep from edgeCut selection', () => {
const artifactGraph: ArtifactGraph = new Map()

// Create path -> sweep -> segment -> edgeCut chain
const path: Artifact = {
type: 'path',
id: 'path-1',
codeRef: { range: [0, 0, 0], pathToNode: [], nodePath: { steps: [] } },
planeId: 'plane-1',
segIds: ['segment-1'],
sweepId: 'sweep-1',
}

const sweep: Artifact = {
type: 'sweep',
id: 'sweep-1',
codeRef: {
range: [0, 0, 0],
pathToNode: [],
nodePath: { steps: [] },
},
pathId: 'path-1',
subType: 'extrusion',
surfaceIds: [],
edgeIds: [],
}

const segment: Artifact = {
type: 'segment',
id: 'segment-1',
pathId: 'path-1',
edgeIds: [],
commonSurfaceIds: [],
codeRef: {
range: [0, 0, 0],
pathToNode: [],
nodePath: { steps: [] },
},
}

const edgeCut: Artifact = {
type: 'edgeCut',
id: 'edge-cut-1',
consumedEdgeId: 'segment-1',
subType: 'chamfer',
edgeIds: [],
codeRef: {
range: [0, 0, 0],
pathToNode: [],
nodePath: { steps: [] },
},
}

artifactGraph.set('path-1', path)
artifactGraph.set('sweep-1', sweep)
artifactGraph.set('segment-1', segment)
artifactGraph.set('edge-cut-1', edgeCut)

const selection: Selection = {
artifact: edgeCut,
codeRef: { range: [0, 0, 0], pathToNode: [] },
}

const result = getSweepArtifactFromSelection(selection, artifactGraph)

expect(result).not.toBeInstanceOf(Error)
if (!(result instanceof Error)) {
expect('type' in result ? result.type : undefined).toBe('sweep')
expect(result.id).toBe('sweep-1')
}
})
})

describe('coerceSelectionsToBody', () => {
it('should pass through path artifact unchanged', () => {
const artifactGraph: ArtifactGraph = new Map()

const path: Artifact = {
type: 'path',
id: 'path-1',
codeRef: { range: [0, 100, 0], pathToNode: [], nodePath: { steps: [] } },
planeId: 'plane-1',
segIds: [],
}
artifactGraph.set('path-1', path)

const selections: Selections = {
graphSelections: [
{
artifact: path,
codeRef: { range: [0, 100, 0], pathToNode: [] },
},
],
otherSelections: [],
}

const result = coerceSelectionsToBody(selections, artifactGraph)

expect(result).not.toBeInstanceOf(Error)
if (!(result instanceof Error)) {
expect(result.graphSelections).toHaveLength(1)
expect(result.graphSelections[0].artifact?.type).toBe('path')
expect(result.graphSelections[0].artifact?.id).toBe('path-1')
}
})

it('should coerce edgeCut selection to parent path', () => {
const artifactGraph: ArtifactGraph = new Map()

const path: Artifact = {
type: 'path',
id: 'path-1',
codeRef: { range: [0, 100, 0], pathToNode: [], nodePath: { steps: [] } },
planeId: 'plane-1',
segIds: ['segment-1'],
sweepId: 'sweep-1',
}

const sweep: Artifact = {
type: 'sweep',
id: 'sweep-1',
codeRef: {
range: [100, 200, 0],
pathToNode: [],
nodePath: { steps: [] },
},
pathId: 'path-1',
subType: 'extrusion',
surfaceIds: [],
edgeIds: [],
}

const segment: Artifact = {
type: 'segment',
id: 'segment-1',
pathId: 'path-1',
edgeIds: [],
commonSurfaceIds: [],
codeRef: { range: [10, 20, 0], pathToNode: [], nodePath: { steps: [] } },
}

const edgeCut: Artifact = {
type: 'edgeCut',
id: 'edge-cut-1',
consumedEdgeId: 'segment-1',
subType: 'chamfer',
edgeIds: [],
codeRef: { range: [90, 95, 0], pathToNode: [], nodePath: { steps: [] } },
}

artifactGraph.set('path-1', path)
artifactGraph.set('sweep-1', sweep)
artifactGraph.set('segment-1', segment)
artifactGraph.set('edge-cut-1', edgeCut)

const selections: Selections = {
graphSelections: [
{
artifact: edgeCut,
codeRef: { range: [90, 95, 0], pathToNode: [] },
},
],
otherSelections: [],
}

const result = coerceSelectionsToBody(selections, artifactGraph)

expect(result).not.toBeInstanceOf(Error)
if (!(result instanceof Error)) {
expect(result.graphSelections).toHaveLength(1)
expect(result.graphSelections[0].artifact?.type).toBe('path')
expect(result.graphSelections[0].artifact?.id).toBe('path-1')
}
})
})
Loading
Loading