Skip to content

Commit fa7633f

Browse files
authored
#7690 Plane selection improvements (#7707)
* selectOffsetSketchPlane now returns an Error potentially * move findOperationArtifact to queryAst * add unit test for findOperationArtifact - not working yet * fix test * lint * lint * move getCurrentPlaneId to queryAst and rename it to getSelectedPlaneId * move getSelectedPlane to queryAst * cleanups, renaming * add tests for getSelectedPlaneId, getSelectedPlaneAsNode * getSelectedPlaneAsNode should take variables to avoid kclManager dependency in queryAst * lint is now happy without this.. * rename findOperationArtifact -> findOperationPlaneArtifact * PR feedback
1 parent 2d9b292 commit fa7633f

File tree

7 files changed

+396
-144
lines changed

7 files changed

+396
-144
lines changed

src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ import {
4040
kclEditorActor,
4141
selectionEventSelector,
4242
} from '@src/machines/kclEditorMachine'
43-
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
4443
import {
4544
selectDefaultSketchPlane,
4645
selectOffsetSketchPlane,
4746
} from '@src/lib/selections'
4847
import type { DefaultPlaneStr } from '@src/lib/planes'
48+
import { findOperationPlaneArtifact, isOffsetPlane } from '@src/lang/queryAst'
49+
import { err } from '@src/lib/trap'
4950

5051
export const FeatureTreePane = () => {
5152
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
@@ -391,11 +392,17 @@ const OperationItem = (props: {
391392
)
392393
}, [kclContext.diagnostics.length])
393394

394-
function selectOperation() {
395+
async function selectOperation() {
395396
if (props.sketchNoFace) {
396397
if (isOffsetPlane(props.item)) {
397-
const artifact = findOperationArtifact(props.item)
398-
void selectOffsetSketchPlane(artifact)
398+
const artifact = findOperationPlaneArtifact(
399+
props.item,
400+
kclManager.artifactGraph
401+
)
402+
const result = await selectOffsetSketchPlane(artifact)
403+
if (err(result)) {
404+
console.error(result)
405+
}
399406
}
400407
} else {
401408
if (props.item.type === 'GroupEnd') {
@@ -502,7 +509,10 @@ const OperationItem = (props: {
502509

503510
function startSketchOnOffsetPlane() {
504511
if (isOffsetPlane(props.item)) {
505-
const artifact = findOperationArtifact(props.item)
512+
const artifact = findOperationPlaneArtifact(
513+
props.item,
514+
kclManager.artifactGraph
515+
)
506516
if (artifact?.id) {
507517
sceneInfra.modelingSend({
508518
type: 'Enter sketch',
@@ -665,7 +675,9 @@ const OperationItem = (props: {
665675
variableName={variableName}
666676
valueDetail={valueDetail}
667677
menuItems={menuItems}
668-
onClick={selectOperation}
678+
onClick={() => {
679+
void selectOperation()
680+
}}
669681
onDoubleClick={props.sketchNoFace ? undefined : enterEditFlow} // no double click in "Sketch no face" mode
670682
errors={errors}
671683
greyedOut={!enabled}
@@ -774,17 +786,3 @@ const DefaultPlanes = () => {
774786
</div>
775787
)
776788
}
777-
778-
type StdLibCallOp = Extract<Operation, { type: 'StdLibCall' }>
779-
780-
const isOffsetPlane = (item: Operation): item is StdLibCallOp => {
781-
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
782-
}
783-
784-
const findOperationArtifact = (item: StdLibCallOp) => {
785-
const nodePath = JSON.stringify(item.nodePath)
786-
const artifact = [...kclManager.artifactGraph.values()].find(
787-
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
788-
)
789-
return artifact
790-
}

src/components/ViewControlMenu.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import { kclManager, sceneInfra } from '@src/lib/singletons'
1414
import { err, reportRejection } from '@src/lib/trap'
1515
import { useSettings } from '@src/lib/singletons'
1616
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
17-
import type { Selections } from '@src/lib/selections'
1817
import {
1918
selectDefaultSketchPlane,
2019
selectOffsetSketchPlane,
2120
} from '@src/lib/selections'
21+
import { getSelectedPlaneId } from '@src/lang/queryAst'
2222

2323
export function useViewControlMenuItems() {
2424
const { state: modelingState, send: modelingSend } = useModelingContext()
25-
const selectedPlaneId = getCurrentPlaneId(
25+
const selectedPlaneId = getSelectedPlaneId(
2626
modelingState.context.selectionRanges
2727
)
2828

@@ -112,21 +112,3 @@ export function ViewControlContextMenu({
112112
/>
113113
)
114114
}
115-
116-
function getCurrentPlaneId(selectionRanges: Selections): string | null {
117-
const defaultPlane = selectionRanges.otherSelections.find(
118-
(selection) => typeof selection === 'object' && 'name' in selection
119-
)
120-
if (defaultPlane) {
121-
return defaultPlane.id
122-
}
123-
124-
const planeSelection = selectionRanges.graphSelections.find(
125-
(selection) => selection.artifact?.type === 'plane'
126-
)
127-
if (planeSelection) {
128-
return planeSelection.artifact?.id || null
129-
}
130-
131-
return null
132-
}

src/hooks/useEngineConnectionSubscriptions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export function useEngineConnectionSubscriptions() {
109109
}
110110

111111
const artifact = kclManager.artifactGraph.get(planeOrFaceId)
112-
if (await selectOffsetSketchPlane(artifact)) {
112+
const offsetPlaneSelected =
113+
await selectOffsetSketchPlane(artifact)
114+
if (!err(offsetPlaneSelected) && offsetPlaneSelected) {
113115
return
114116
}
115117

src/lang/queryAst.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import {
1313
doesSceneHaveExtrudedSketch,
1414
doesSceneHaveSweepableSketch,
1515
findAllPreviousVariables,
16+
findOperationPlaneArtifact,
1617
findUsesOfTagInPipe,
1718
getNodeFromPath,
19+
getSelectedPlaneId,
20+
getSelectedPlaneAsNode,
1821
getVariableExprsFromSelection,
1922
hasSketchPipeBeenExtruded,
2023
isCursorInFunctionDefinition,
2124
isNodeSafeToReplace,
25+
isOffsetPlane,
2226
retrieveSelectionsFromOpArg,
2327
traverse,
2428
} from '@src/lang/queryAst'
@@ -32,6 +36,7 @@ import { initPromise } from '@src/lang/wasmUtils'
3236
import type { Selections, Selection } from '@src/lib/selections'
3337
import { enginelessExecutor } from '@src/lib/testHelpers'
3438
import { err } from '@src/lib/trap'
39+
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
3540

3641
beforeAll(async () => {
3742
await initPromise
@@ -781,6 +786,223 @@ describe('Testing specific sketch getNodeFromPath workflow', () => {
781786
})
782787
})
783788

789+
describe('Testing findOperationArtifact', () => {
790+
it('should find the correct artifact for a given operation', async () => {
791+
const code = `sketch001 = startSketchOn(XY)
792+
|> startProfile(at = [0, 0])
793+
|> line(end = [10, 0])
794+
|> line(end = [10, 10])
795+
|> line(end = [0, 10])
796+
|> close()
797+
plane001 = offsetPlane(YZ, offset = 10)
798+
part001 = startSketchOn(plane001)
799+
|> startProfile(at = [0, 0])
800+
|> line(end = [5, 0])
801+
|> line(end = [5, 5])
802+
|> line(end = [0, 5])
803+
|> close()
804+
`
805+
806+
const ast = assertParse(code)
807+
const execState = await enginelessExecutor(ast, false)
808+
const { operations, artifactGraph } = execState
809+
810+
expect(operations).toBeTruthy()
811+
expect(operations.length).toBeGreaterThan(0)
812+
813+
// Find an offsetPlane operation
814+
const offsetPlaneOp = operations.find(
815+
(op) => op.type === 'StdLibCall' && op.name === 'offsetPlane'
816+
)
817+
expect(offsetPlaneOp).toBeTruthy()
818+
819+
if (offsetPlaneOp && isOffsetPlane(offsetPlaneOp)) {
820+
const artifact = findOperationPlaneArtifact(offsetPlaneOp, artifactGraph)
821+
822+
expect(artifact).toBeTruthy()
823+
expect(artifact?.type).toBe('plane')
824+
825+
const artifactNodePath = JSON.stringify(
826+
(artifact as Plane)?.codeRef?.nodePath
827+
)
828+
const operationNodePath = JSON.stringify(offsetPlaneOp.nodePath)
829+
expect(artifactNodePath).toBe(operationNodePath)
830+
}
831+
})
832+
})
833+
834+
describe('Testing getSelectedPlaneId', () => {
835+
it('should return the id of the selected default plane', () => {
836+
const selections: Selections = {
837+
otherSelections: [
838+
{
839+
name: 'XY', // actually, this is lowercase during runtime ("xy")!
840+
id: 'default-plane-xy-id',
841+
},
842+
],
843+
graphSelections: [],
844+
}
845+
846+
const result = getSelectedPlaneId(selections)
847+
expect(result).toBe('default-plane-xy-id')
848+
})
849+
850+
it('should return the id of the selected offset plane', () => {
851+
const selections: Selections = {
852+
otherSelections: [],
853+
graphSelections: [
854+
{
855+
artifact: {
856+
type: 'plane' as const,
857+
id: 'offset-plane-id',
858+
pathIds: [],
859+
codeRef: {
860+
nodePath: {
861+
steps: [],
862+
},
863+
range: [0, 10, 0] as [number, number, number],
864+
pathToNode: [
865+
['body', ''],
866+
[0, 'index'],
867+
] as PathToNode,
868+
},
869+
},
870+
codeRef: {
871+
range: [0, 10, 0] as [number, number, number],
872+
pathToNode: [
873+
['body', ''],
874+
[0, 'index'],
875+
] as PathToNode,
876+
},
877+
},
878+
],
879+
}
880+
881+
const result = getSelectedPlaneId(selections)
882+
expect(result).toBe('offset-plane-id')
883+
})
884+
885+
it('should prioritize default plane over offset plane when both are selected', () => {
886+
const mockPlaneArtifact = {
887+
type: 'plane' as const,
888+
id: 'offset-plane-id',
889+
pathIds: [],
890+
codeRef: {
891+
range: [0, 10, 0] as [number, number, number],
892+
nodePath: { steps: [] },
893+
pathToNode: [
894+
['body', ''],
895+
[0, 'index'],
896+
] as PathToNode,
897+
},
898+
}
899+
900+
const selections: Selections = {
901+
otherSelections: [
902+
{
903+
name: 'XY',
904+
id: 'default-plane-xy-id',
905+
},
906+
],
907+
graphSelections: [
908+
{
909+
artifact: mockPlaneArtifact,
910+
codeRef: {
911+
range: [0, 10, 0] as [number, number, number],
912+
pathToNode: [
913+
['body', ''],
914+
[0, 'index'],
915+
] as PathToNode,
916+
},
917+
},
918+
],
919+
}
920+
921+
const result = getSelectedPlaneId(selections)
922+
expect(result).toBe('default-plane-xy-id')
923+
})
924+
925+
it('should return null when no plane is selected', () => {
926+
const selections: Selections = {
927+
otherSelections: ['x-axis'],
928+
graphSelections: [
929+
{
930+
artifact: {
931+
type: 'startSketchOnFace' as const,
932+
id: 'segment-id',
933+
faceId: 'face-id',
934+
codeRef: {
935+
range: [0, 10, 0] as [number, number, number],
936+
nodePath: { steps: [] },
937+
pathToNode: [
938+
['body', ''],
939+
[0, 'index'],
940+
] as PathToNode,
941+
},
942+
},
943+
codeRef: {
944+
range: [0, 10, 0] as [number, number, number],
945+
pathToNode: [
946+
['body', ''],
947+
[0, 'index'],
948+
] as PathToNode,
949+
},
950+
},
951+
],
952+
}
953+
954+
const result = getSelectedPlaneId(selections)
955+
expect(result).toBeNull()
956+
})
957+
958+
it('should return null when selection is empty', () => {
959+
const selections: Selections = {
960+
otherSelections: [],
961+
graphSelections: [],
962+
}
963+
964+
const result = getSelectedPlaneId(selections)
965+
expect(result).toBeNull()
966+
})
967+
})
968+
969+
describe('Testing getSelectedPlaneAsNode', () => {
970+
it('should return a Node<Literal> for default plane selection', async () => {
971+
const code = `sketch001 = startSketchOn(XY)
972+
|> startProfile(at = [0, 0])
973+
|> line(end = [10, 0])
974+
|> line(end = [10, 10])
975+
|> line(end = [0, 10])
976+
|> close()
977+
plane001 = offsetPlane(YZ, offset = 10)
978+
`
979+
980+
const ast = assertParse(code)
981+
const execState = await enginelessExecutor(ast, false)
982+
const { variables } = execState
983+
984+
const selections: Selections = {
985+
otherSelections: [
986+
{
987+
name: 'XY',
988+
id: 'default-plane-xy-id',
989+
},
990+
],
991+
graphSelections: [],
992+
}
993+
994+
const result = getSelectedPlaneAsNode(selections, variables)
995+
expect(result).toBeTruthy()
996+
expect(result?.type).toBe('Literal')
997+
if (result?.type !== 'Literal') {
998+
// To make TypeScript happy
999+
throw new Error('Expected result to be a Literal node')
1000+
}
1001+
1002+
expect(result?.value).toBe('XY')
1003+
})
1004+
})
1005+
7841006
describe('Testing getVariableExprsFromSelection', () => {
7851007
it('should find the variable expr in a simple profile selection', async () => {
7861008
const circleProfileInVar = `sketch001 = startSketchOn(XY)

0 commit comments

Comments
 (0)