fix no profile errors (#5877)

* fix no profile errors

* add test and tweak a couple things

* quick fix

* fix animation

* add another test

* Use actor.getSnapshot in the debug function

So we don't have to rebuild that listener every time that the state
changes.

* try fix tests

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
This commit is contained in:
Kurt Hutten
2025-03-20 08:30:11 +11:00
committed by GitHub
parent e7d00f148b
commit 4b6166dc4f
9 changed files with 195 additions and 31 deletions

View File

@ -152,9 +152,15 @@ export class EditorFixture {
} }
replaceCode = async (findCode: string, replaceCode: string) => { replaceCode = async (findCode: string, replaceCode: string) => {
const lines = await this.page.locator('.cm-line').all() const lines = await this.page.locator('.cm-line').all()
let code = (await Promise.all(lines.map((c) => c.textContent()))).join('\n') let code = (await Promise.all(lines.map((c) => c.textContent()))).join('\n')
if (!lines) return if (!findCode) {
code = code.replace(findCode, replaceCode) // nuke everything
code = replaceCode
} else {
if (!lines) return
code = code.replace(findCode, replaceCode)
}
await this.codeContent.fill(code) await this.codeContent.fill(code)
} }
checkIfPaneIsOpen() { checkIfPaneIsOpen() {

View File

@ -1662,6 +1662,96 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
}) })
} }
) )
test('can enter sketch mode for sketch with no profiles', async ({
scene,
toolbar,
editor,
cmdBar,
page,
homePage,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// open feature tree and double click the first sketch
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
await page.waitForTimeout(600)
// click in the scene twice to add a segment
const [startProfile1] = scene.makeMouseHelpers(658, 140)
const [segment1Clk] = scene.makeMouseHelpers(701, 200)
// wait for line to be aria pressed
await expect
.poll(async () => toolbar.lineBtn.getAttribute('aria-pressed'))
.toBe('true')
await startProfile1()
await editor.expectEditor.toContain(`profile001 = startProfileAt`)
await segment1Clk()
await editor.expectEditor.toContain(`|> line(end`)
})
test('can delete all profiles in sketch mode and user can still equip a tool and draw something', async ({
scene,
toolbar,
editor,
page,
homePage,
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
const [startProfile1] = scene.makeMouseHelpers(568, 70)
const [segment1Clk] = scene.makeMouseHelpers(701, 78)
const [segment2Clk] = scene.makeMouseHelpers(745, 189)
await test.step('add two segments', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
)
await segment1Clk()
await editor.expectEditor.toContain(`|> line(end`)
await segment2Clk()
await editor.expectEditor.toContain(`|> line(end = [2.98, -7.52])`)
})
await test.step('delete all profiles', async () => {
await editor.replaceCode('', "sketch001 = startSketchOn('XZ')\n")
await page.waitForTimeout(600) // wait for deferred execution
})
await test.step('equip circle and draw it', async () => {
await toolbar.circleBtn.click()
await page.mouse.click(700, 200)
await page.mouse.click(750, 200)
await editor.expectEditor.toContain('circle(sketch001, center = [')
})
})
test('Can add multiple profiles to a sketch (all tool types)', async ({ test('Can add multiple profiles to a sketch (all tool types)', async ({
scene, scene,
toolbar, toolbar,

View File

@ -353,6 +353,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
const camPosition1 = async () => { const camPosition1 = async () => {

View File

@ -580,8 +580,7 @@ export class SceneEntities {
if (interaction !== 'none') return if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchEntryNodePath) if (!intersectionPoint?.twoD) return
return
const parent = getParentGroup( const parent = getParentGroup(
args?.intersects?.[0]?.object, args?.intersects?.[0]?.object,
@ -616,7 +615,7 @@ export class SceneEntities {
const inserted = insertNewStartProfileAt( const inserted = insertNewStartProfileAt(
kclManager.ast, kclManager.ast,
sketchDetails.sketchEntryNodePath, sketchDetails.sketchEntryNodePath || [],
sketchDetails.sketchNodePaths, sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath, sketchDetails.planeNodePath,
[snappedClickPoint.x, snappedClickPoint.y], [snappedClickPoint.x, snappedClickPoint.y],

View File

@ -114,10 +114,11 @@ import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useSettings } from 'machines/appMachine' import { useSettings } from 'machines/appMachine'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { OutputFormat3d } from '@rust/kcl-lib/bindings/ModelingCmd' import { OutputFormat3d, Point3d } from '@rust/kcl-lib/bindings/ModelingCmd'
import { EXPORT_TOAST_MESSAGES, MAKE_TOAST_MESSAGES } from 'lib/constants' import { EXPORT_TOAST_MESSAGES, MAKE_TOAST_MESSAGES } from 'lib/constants'
import { exportMake } from 'lib/exportMake' import { exportMake } from 'lib/exportMake'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { Plane } from '@rust/kcl-lib/bindings/Plane'
export const ModelingMachineContext = createContext( export const ModelingMachineContext = createContext(
{} as { {} as {
@ -573,8 +574,9 @@ export const ModelingMachineProvider = ({
kclManager.ast, kclManager.ast,
selectionRanges.graphSelections[0] selectionRanges.graphSelections[0]
) )
) ) {
return false return false
}
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
selectionRanges selectionRanges
@ -602,7 +604,6 @@ export const ModelingMachineProvider = ({
} }
let fileName = file?.name?.replace('.kcl', `.${input.type}`) || '' let fileName = file?.name?.replace('.kcl', `.${input.type}`) || ''
console.log('fileName', fileName)
// Ensure the file has an extension. // Ensure the file has an extension.
if (!fileName.includes('.')) { if (!fileName.includes('.')) {
fileName += `.${input.type}` fileName += `.${input.type}`
@ -852,6 +853,7 @@ export const ModelingMachineProvider = ({
? artifact?.pathId ? artifact?.pathId
: plane?.pathIds[0] : plane?.pathIds[0]
let sketch: KclValue | null = null let sketch: KclValue | null = null
let planeVar: Plane | null = null
for (const variable of Object.values( for (const variable of Object.values(
kclManager.execState.variables kclManager.execState.variables
)) { )) {
@ -875,13 +877,43 @@ export const ModelingMachineProvider = ({
} }
break break
} }
if (
variable?.type === 'Plane' &&
plane.id === variable.value.id
) {
planeVar = variable.value
}
} }
if (!sketch || sketch.type !== 'Sketch') if (!sketch || sketch.type !== 'Sketch') {
return Promise.reject(new Error('No sketch')) if (artifact?.type !== 'plane')
if (!sketch || sketch.type !== 'Sketch') return Promise.reject(new Error('No sketch'))
const planeCodeRef = getFaceCodeRef(artifact)
if (planeVar && planeCodeRef) {
const toTuple = (point: Point3d): [number, number, number] => [
point.x,
point.y,
point.z,
]
const planPath = getNodePathFromSourceRange(
kclManager.ast,
planeCodeRef.range
)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
artifact.id
)
return {
sketchEntryNodePath: [],
planeNodePath: planPath,
sketchNodePaths: [],
zAxis: toTuple(planeVar.zAxis),
yAxis: toTuple(planeVar.yAxis),
origin: toTuple(planeVar.origin),
}
}
return Promise.reject(new Error('No sketch')) return Promise.reject(new Error('No sketch'))
}
const info = await getSketchOrientationDetails(sketch.value) const info = await getSketchOrientationDetails(sketch.value)
await letEngineAnimateAndSyncCamAfter( await letEngineAnimateAndSyncCamAfter(
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
@ -1576,7 +1608,7 @@ export const ModelingMachineProvider = ({
'setup-client-side-sketch-segments': fromPromise( 'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => { async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return if (!sketchDetails.sketchEntryNodePath?.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) { if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false }) sceneEntitiesManager.tearDownSketch({ removeAxis: false })
} }
@ -1617,6 +1649,9 @@ export const ModelingMachineProvider = ({
updatedPlaneNodePath: sketchDetails.planeNodePath, updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1, expressionIndexToDelete: -1,
} as const } as const
if (!sketchDetails?.sketchEntryNodePath?.length) {
return existingSketchInfoNoOp
}
if ( if (
!sketchDetails.sketchNodePaths.length && !sketchDetails.sketchNodePaths.length &&
sketchDetails.planeNodePath.length sketchDetails.planeNodePath.length
@ -1727,6 +1762,18 @@ export const ModelingMachineProvider = ({
} }
) )
// Add debug function to window object
useEffect(() => {
// @ts-ignore - we're intentionally adding this to window
window.getModelingState = () => {
const modelingState = modelingActor.getSnapshot()
return {
modelingState,
id: modelingState._nodes[modelingState._nodes.length - 1].id,
}
}
}, [modelingActor])
useSetupEngineManager( useSetupEngineManager(
streamRef, streamRef,
modelingSend, modelingSend,

View File

@ -61,6 +61,7 @@ export function getNodeFromPath<T>(
path: PathToNode, path: PathToNode,
stopAt?: SyntaxType | SyntaxType[], stopAt?: SyntaxType | SyntaxType[],
returnEarly = false, returnEarly = false,
suppressNoise = false,
replacement?: any replacement?: any
): ):
| { | {
@ -105,9 +106,11 @@ export function getNodeFromPath<T>(
.filter((a) => a) .filter((a) => a)
.join(' > ')}` .join(' > ')}`
) )
console.error(tree) if (!suppressNoise) {
console.error(sourceCode) console.error(tree)
console.error(error.stack) console.error(sourceCode)
console.error(error.stack)
}
return error return error
} }
parent = currentNode parent = currentNode
@ -967,3 +970,11 @@ export function getSettingsAnnotation(
return settings return settings
} }
function pathToNodeKeys(pathToNode: PathToNode): (string | number)[] {
return pathToNode.map(([key]) => key)
}
export function stringifyPathToNode(pathToNode: PathToNode): string {
return JSON.stringify(pathToNodeKeys(pathToNode))
}

View File

@ -748,6 +748,9 @@ export function getPathsFromPlaneArtifact(
) )
} }
} }
if (nodePaths.length === 0) {
return []
}
return onlyConsecutivePaths(nodePaths, nodePaths[0], ast) return onlyConsecutivePaths(nodePaths, nodePaths[0], ast)
} }

View File

@ -3541,7 +3541,7 @@ function addTagKw(): addTagFn {
// If we changed the node, we must replace the old node with the new node in the AST. // If we changed the node, we must replace the old node with the new node in the AST.
const mustReplaceNode = primaryCallExp.type !== callExpr.node.type const mustReplaceNode = primaryCallExp.type !== callExpr.node.type
if (mustReplaceNode) { if (mustReplaceNode) {
getNodeFromPath(_node, pathToNode, ['CallExpression'], false, { getNodeFromPath(_node, pathToNode, ['CallExpression'], false, false, {
...primaryCallExp, ...primaryCallExp,
start: callExpr.node.start, start: callExpr.node.start,
end: callExpr.node.end, end: callExpr.node.end,

File diff suppressed because one or more lines are too long