Skip to content

#7690 Plane selection improvements #7707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cb16b2d
selectOffsetSketchPlane now returns an Error potentially
andrewvarga Jul 7, 2025
c876d21
move findOperationArtifact to queryAst
andrewvarga Jul 7, 2025
374441e
add unit test for findOperationArtifact - not working yet
andrewvarga Jul 7, 2025
f434b95
Merge branch 'main' into andrewvarga/7690/plane-selection-improvements
andrewvarga Jul 7, 2025
57459fe
fix test
andrewvarga Jul 7, 2025
e84772e
Merge branch 'main' into andrewvarga/7690/plane-selection-improvements
andrewvarga Jul 8, 2025
ff6622e
lint
andrewvarga Jul 8, 2025
badc414
lint
andrewvarga Jul 8, 2025
b422669
move getCurrentPlaneId to queryAst and rename it to getSelectedPlaneId
andrewvarga Jul 8, 2025
d9ee587
move getSelectedPlane to queryAst
andrewvarga Jul 8, 2025
718d4a4
cleanups, renaming
andrewvarga Jul 8, 2025
e2845a5
Merge branch 'main' into andrewvarga/7690/plane-selection-improvements
andrewvarga Jul 8, 2025
10d276b
add tests for getSelectedPlaneId, getSelectedPlaneAsNode
andrewvarga Jul 8, 2025
16cbf72
Merge branch 'main' into andrewvarga/7690/plane-selection-improvements
andrewvarga Jul 8, 2025
10f07d8
getSelectedPlaneAsNode should take variables to avoid kclManager depe…
andrewvarga Jul 9, 2025
2c93c73
merge from main, conflicts in queryAst.test.ts
andrewvarga Jul 9, 2025
a1819d8
lint is now happy without this..
andrewvarga Jul 9, 2025
d7ef6e5
Merge branch 'main' into andrewvarga/7690/plane-selection-improvements
andrewvarga Jul 16, 2025
14b4bcb
rename findOperationArtifact -> findOperationPlaneArtifact
andrewvarga Jul 16, 2025
6a8130a
PR feedback
andrewvarga Jul 16, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import {
kclEditorActor,
selectionEventSelector,
} from '@src/machines/kclEditorMachine'
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
import {
selectDefaultSketchPlane,
selectOffsetSketchPlane,
} from '@src/lib/selections'
import type { DefaultPlaneStr } from '@src/lib/planes'
import { findOperationArtifact, isOffsetPlane } from '@src/lang/queryAst'

export const FeatureTreePane = () => {
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
Expand Down Expand Up @@ -363,7 +363,10 @@ const OperationItem = (props: {
function selectOperation() {
if (props.sketchNoFace) {
if (isOffsetPlane(props.item)) {
const artifact = findOperationArtifact(props.item)
const artifact = findOperationArtifact(
props.item,
kclManager.artifactGraph
)
void selectOffsetSketchPlane(artifact)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function selectOffsetSketchPlane now returns a Promise<Error | boolean> instead of a Promise<boolean>, but this call doesn't handle potential errors. Consider updating to:

const result = await selectOffsetSketchPlane(artifact);
if (err(result)) {
  console.error(result);
  return;
}

This ensures errors are properly logged and handled before proceeding.

Suggested change
void selectOffsetSketchPlane(artifact)
const result = await selectOffsetSketchPlane(artifact);
if (err(result)) {
console.error(result);
return;
}

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the bot is right there @andrewvarga ?

}
} else {
Expand Down Expand Up @@ -459,7 +462,10 @@ const OperationItem = (props: {

function startSketchOnOffsetPlane() {
if (isOffsetPlane(props.item)) {
const artifact = findOperationArtifact(props.item)
const artifact = findOperationArtifact(
props.item,
kclManager.artifactGraph
)
if (artifact?.id) {
sceneInfra.modelingSend({
type: 'Enter sketch',
Expand Down Expand Up @@ -715,17 +721,3 @@ const DefaultPlanes = () => {
</div>
)
}

type StdLibCallOp = Extract<Operation, { type: 'StdLibCall' }>

const isOffsetPlane = (item: Operation): item is StdLibCallOp => {
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
}

const findOperationArtifact = (item: StdLibCallOp) => {
const nodePath = JSON.stringify(item.nodePath)
const artifact = [...kclManager.artifactGraph.values()].find(
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
)
return artifact
}
4 changes: 3 additions & 1 deletion src/hooks/useEngineConnectionSubscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export function useEngineConnectionSubscriptions() {
}

const artifact = kclManager.artifactGraph.get(planeOrFaceId)
if (await selectOffsetSketchPlane(artifact)) {
const offsetPlaneSelected =
await selectOffsetSketchPlane(artifact)
if (!err(offsetPlaneSelected) && offsetPlaneSelected) {
return
}

Expand Down
56 changes: 56 additions & 0 deletions src/lang/queryAst.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch,
findAllPreviousVariables,
findOperationArtifact,
findUsesOfTagInPipe,
getNodeFromPath,
hasSketchPipeBeenExtruded,
isCursorInFunctionDefinition,
isNodeSafeToReplace,
isOffsetPlane,
traverse,
} from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
Expand All @@ -30,6 +32,7 @@
import { type Selection } from '@src/lib/selections'
import { enginelessExecutor } from '@src/lib/testHelpers'
import { err } from '@src/lib/trap'
import { kclManager } from '@src/lib/singletons'

beforeAll(async () => {
await initPromise
Expand Down Expand Up @@ -778,3 +781,56 @@
expect(result).toEqual(false)
})
})

describe('Testing findOperationArtifact', () => {
it('should find the correct artifact for a given operation', async () => {
const code = `sketch001 = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [10, 0])
|> line(end = [10, 10])
|> line(end = [0, 10])
|> close()

plane001 = offsetPlane(YZ, offset = 10)
part001 = startSketchOn(plane001)
|> startProfile(at = [0, 0])
|> line(end = [5, 0])
|> line(end = [5, 5])
|> line(end = [0, 5])
|> close()
`

// const ast = assertParse(code)
// await kclManager.executeAst({ ast })
// const operations = kclManager.lastSuccessfulOperations

const ast = assertParse(code)
const execState = await enginelessExecutor(ast, false)
const operations = execState.operations

//expect(kclManager.errors).toEqual([])

expect(operations).toBeTruthy()
expect(operations.length).toBeGreaterThan(0)

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

Check failure on line 814 in src/lang/queryAst.test.ts

View workflow job for this annotation

GitHub Actions / npm-test-unit

src/lang/queryAst.test.ts > Testing findOperationArtifact > should find the correct artifact for a given operation

AssertionError: expected 0 to be greater than 0 ❯ src/lang/queryAst.test.ts:814:31

// Find an offsetPlane operation
const offsetPlaneOp = operations.find(
(op) => op.type === 'StdLibCall' && op.name === 'offsetPlane'
)
expect(offsetPlaneOp).toBeTruthy()

if (offsetPlaneOp && isOffsetPlane(offsetPlaneOp)) {
const artifact = findOperationArtifact(
offsetPlaneOp,
kclManager.artifactGraph
)

expect(artifact).toBeTruthy()
expect(artifact?.type).toBe('plane')

// const artifactNodePath = JSON.stringify(artifact?.codeRef?.nodePath)
// const operationNodePath = JSON.stringify(offsetPlaneOp.nodePath)
// expect(artifactNodePath).toBe(operationNodePath)
}
})
})
22 changes: 21 additions & 1 deletion src/lang/queryAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,16 @@
import { err } from '@src/lib/trap'
import { getAngle, isArray } from '@src/lib/utils'

import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import type {
OpKclValue,
Operation,
StdLibCallOp,

Check failure on line 57 in src/lang/queryAst.ts

View workflow job for this annotation

GitHub Actions / npm-tsc

Module '"@rust/kcl-lib/bindings/Operation"' has no exported member 'StdLibCallOp'.
} from '@rust/kcl-lib/bindings/Operation'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes'
import type { UnaryExpression } from 'typescript'
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'

/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
Expand Down Expand Up @@ -1158,6 +1163,21 @@
}
}

export function findOperationArtifact(
item: StdLibCallOp,
artifactGraph: ArtifactGraph
) {
const nodePath = JSON.stringify(item.nodePath)
const artifact = [...artifactGraph.values()].find(
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
)
return artifact
}

export function isOffsetPlane(item: Operation): item is StdLibCallOp {
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
}

export function locateVariableWithCallOrPipe(
ast: Program,
pathToNode: PathToNode
Expand Down
143 changes: 71 additions & 72 deletions src/lib/selections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,77 +883,76 @@ export function selectDefaultSketchPlane(
return true
}

export async function selectOffsetSketchPlane(artifact: Artifact | undefined) {
return new Promise((resolve) => {
if (artifact?.type === 'plane') {
const planeId = artifact.id
void sceneEntitiesManager
.getFaceDetails(planeId)
.then((planeInfo) => {
// Apply camera-based orientation logic similar to default planes
let zAxis: [number, number, number] = [
planeInfo.z_axis.x,
planeInfo.z_axis.y,
planeInfo.z_axis.z,
]
let yAxis: [number, number, number] = [
planeInfo.y_axis.x,
planeInfo.y_axis.y,
planeInfo.y_axis.z,
]

// Get camera vector to determine which side of the plane we're viewing from
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)

// Determine the canonical (absolute) plane orientation
const absZAxis: [number, number, number] = [
Math.abs(zAxis[0]),
Math.abs(zAxis[1]),
Math.abs(zAxis[2]),
]

// Find the dominant axis (like default planes do)
const maxComponent = Math.max(...absZAxis)
const dominantAxisIndex = absZAxis.indexOf(maxComponent)

// Check camera position against canonical orientation (like default planes)
const cameraComponents = [camVector.x, camVector.y, camVector.z]
let negated = cameraComponents[dominantAxisIndex] < 0
if (dominantAxisIndex === 1) {
// offset of the XZ is being weird, not sure if this is a camera bug
negated = !negated
}
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'offsetPlane',
zAxis,
yAxis,
position: [
planeInfo.origin.x,
planeInfo.origin.y,
planeInfo.origin.z,
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
number,
number,
number,
],
planeId,
pathToNode: artifact.codeRef.pathToNode,
negated,
},
})
resolve(true)
})
.catch((error) => {
console.error('Error getting face details:', error)
resolve(false)
})
} else {
// selectOffsetSketchPlane called with an invalid artifact type',
resolve(false)
export async function selectOffsetSketchPlane(
artifact: Artifact | undefined
): Promise<Error | boolean> {
if (artifact?.type !== 'plane') {
return new Error(
`Invalid artifact type for offset sketch plane selection: ${artifact?.type}`
)
}
const planeId = artifact.id
try {
const planeInfo = await sceneEntitiesManager.getFaceDetails(planeId)

// Apply camera-based orientation logic similar to default planes
let zAxis: [number, number, number] = [
planeInfo.z_axis.x,
planeInfo.z_axis.y,
planeInfo.z_axis.z,
]
let yAxis: [number, number, number] = [
planeInfo.y_axis.x,
planeInfo.y_axis.y,
planeInfo.y_axis.z,
]

// Get camera vector to determine which side of the plane we're viewing from
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)

// Determine the canonical (absolute) plane orientation
const absZAxis: [number, number, number] = [
Math.abs(zAxis[0]),
Math.abs(zAxis[1]),
Math.abs(zAxis[2]),
]

// Find the dominant axis (like default planes do)
const maxComponent = Math.max(...absZAxis)
const dominantAxisIndex = absZAxis.indexOf(maxComponent)

// Check camera position against canonical orientation (like default planes)
const cameraComponents = [camVector.x, camVector.y, camVector.z]
let negated = cameraComponents[dominantAxisIndex] < 0
if (dominantAxisIndex === 1) {
// offset of the XZ is being weird, not sure if this is a camera bug
negated = !negated
}
})
sceneInfra.modelingSend({
type: 'Select sketch plane',
data: {
type: 'offsetPlane',
zAxis,
yAxis,
position: [
planeInfo.origin.x,
planeInfo.origin.y,
planeInfo.origin.z,
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
number,
number,
number,
],
planeId,
pathToNode: artifact.codeRef.pathToNode,
negated,
},
})
} catch (err) {
console.error(err)
return new Error('Error getting face details')
}
return true
}
Loading