diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 5178c7099..5822fc8bd 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -1610,6 +1610,8 @@ sketch002 = startSketchOn(plane001) testPoint.y + 80 ) const loftDeclaration = 'loft001 = loft([sketch001, sketch002])' + const editedLoftDeclaration = + 'loft001 = loft([sketch001, sketch002], vDegree = 3)' await test.step(`Look for the white of the sketch001 shape`, async () => { await scene.expectPixelColor([254, 254, 254], testPoint, 15) @@ -1681,6 +1683,39 @@ sketch002 = startSketchOn(plane001) await scene.expectPixelColor([89, 89, 89], testPoint, 15) }) + await test.step('Go through the edit flow via feature tree', async () => { + await toolbar.openPane('feature-tree') + const op = await toolbar.getFeatureTreeOperation('Loft', 0) + await op.dblclick() + await cmdBar.expectState({ + stage: 'review', + headerArguments: {}, + commandName: 'Loft', + }) + await cmdBar.clickOptionalArgument('vDegree') + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'vDegree', + currentArgValue: '', + headerArguments: { + VDegree: '', + }, + highlightedHeaderArg: 'vDegree', + commandName: 'Loft', + }) + await page.keyboard.insertText('3') + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + VDegree: '3', + }, + commandName: 'Loft', + }) + await cmdBar.submit() + await editor.expectEditor.toContain(editedLoftDeclaration) + }) + await test.step('Delete loft via feature tree selection', async () => { await editor.closePane() const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0) @@ -1691,72 +1726,6 @@ sketch002 = startSketchOn(plane001) }) }) - // TODO: merge with above test. Right now we're not able to delete a loft - // right after creation via selection for some reason, so we go with a new instance - test('Loft and offset plane deletion via selection', async ({ - context, - page, - homePage, - scene, - cmdBar, - }) => { - const initialCode = `sketch001 = startSketchOn(XZ) - |> circle(center = [0, 0], radius = 30) - plane001 = offsetPlane(XZ, offset = 50) - sketch002 = startSketchOn(plane001) - |> circle(center = [0, 0], radius = 20) -loft001 = loft([sketch001, sketch002]) -` - await context.addInitScript((initialCode) => { - localStorage.setItem('persistCode', initialCode) - }, initialCode) - await page.setBodyDimensions({ width: 1000, height: 500 }) - await homePage.goToModelingScene() - await scene.settled(cmdBar) - - // One dumb hardcoded screen pixel value - const testPoint = { x: 575, y: 200 } - const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) - const [clickOnSketch2] = scene.makeMouseHelpers( - testPoint.x, - testPoint.y + 80 - ) - - await test.step(`Delete loft`, async () => { - // Check for loft - await scene.expectPixelColor([89, 89, 89], testPoint, 15) - await clickOnSketch1() - await expect(page.locator('.cm-activeLine')).toHaveText(` - |> circle(center = [0, 0], radius = 30) - `) - await page.keyboard.press('Delete') - // Check for sketch 1 - await scene.expectPixelColor([254, 254, 254], testPoint, 15) - }) - - await test.step('Delete sketch002', async () => { - await page.waitForTimeout(1000) - await clickOnSketch2() - await expect(page.locator('.cm-activeLine')).toHaveText(` - |> circle(center = [0, 0], radius = 20) - `) - await page.keyboard.press('Delete') - // Check for plane001 - await scene.expectPixelColor([228, 228, 228], testPoint, 15) - }) - - await test.step('Delete plane001', async () => { - await page.waitForTimeout(1000) - await clickOnSketch2() - await expect(page.locator('.cm-activeLine')).toHaveText(` - plane001 = offsetPlane(XZ, offset = 50) - `) - await page.keyboard.press('Delete') - // Check for sketch 1 - await scene.expectPixelColor([254, 254, 254], testPoint, 15) - }) - }) - const sweepCases = [ { targetType: 'circle', diff --git a/src/lang/modifyAst/addSweep.ts b/src/lang/modifyAst/addSweep.ts index df7101861..d42e77003 100644 --- a/src/lang/modifyAst/addSweep.ts +++ b/src/lang/modifyAst/addSweep.ts @@ -237,9 +237,13 @@ export function addSweep({ export function addLoft({ ast, sketches, + vDegree, + nodeToEdit, }: { ast: Node sketches: Selections + vDegree?: KclCommandValue + nodeToEdit?: PathToNode }): | { modifiedAst: Node @@ -251,21 +255,52 @@ export function addLoft({ // 2. Prepare unlabeled and labeled arguments // Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument - const sketchesExprList = getSketchExprsFromSelection(sketches, modifiedAst) + const sketchesExprList = getSketchExprsFromSelection( + sketches, + modifiedAst, + nodeToEdit + ) if (err(sketchesExprList)) { return sketchesExprList } - const sketchesExpr = createSketchExpression(sketchesExprList) - const call = createCallExpressionStdLibKw('loft', sketchesExpr, []) + // Extra labeled args expressions + const vDegreeExpr = vDegree + ? [createLabeledArg('vDegree', valueOrVariable(vDegree))] + : [] - // 3. Just push the declaration to the end - // Note that Loft doesn't support edit flows yet since it's selection only atm - const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT) - const declaration = createVariableDeclaration(name, call) - modifiedAst.body.push(declaration) - const toFirstKwarg = false - const pathToNode = createPathToNode(modifiedAst, toFirstKwarg) + const sketchesExpr = createSketchExpression(sketchesExprList) + const call = createCallExpressionStdLibKw('loft', sketchesExpr, [ + ...vDegreeExpr, + ]) + + // Insert variables for labeled arguments if provided + if (vDegree && 'variableName' in vDegree && vDegree.variableName) { + insertVariableAndOffsetPathToNode(vDegree, modifiedAst, nodeToEdit) + } + + // 3. If edit, we assign the new function call declaration to the existing node, + // otherwise just push to the end + let pathToNode: PathToNode | undefined + if (nodeToEdit) { + const result = getNodeFromPath( + modifiedAst, + nodeToEdit, + 'CallExpressionKw' + ) + if (err(result)) { + return result + } + + Object.assign(result.node, call) + pathToNode = nodeToEdit + } else { + const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT) + const declaration = createVariableDeclaration(name, call) + modifiedAst.body.push(declaration) + const toFirstKwarg = !!vDegree + pathToNode = createPathToNode(modifiedAst, toFirstKwarg) + } return { modifiedAst, diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 10a2a911b..830bf8164 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -89,7 +89,11 @@ export type ModelingCommandSchema = { relativeTo?: string } Loft: { + // Enables editing workflow + nodeToEdit?: PathToNode + // KCL stdlib arguments sketches: Selections + vDegree?: KclCommandValue } Revolve: { // Enables editing workflow @@ -477,12 +481,20 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< icon: 'loft', needsReview: true, args: { + nodeToEdit: { + ...nodeToEditProps, + }, sketches: { inputType: 'selection', displayName: 'Profiles', selectionTypes: ['solid2d'], multiple: true, required: true, + hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), + }, + vDegree: { + inputType: 'kcl', + required: false, }, }, }, diff --git a/src/lib/operations.ts b/src/lib/operations.ts index 7fd0e8edc..933d9910d 100644 --- a/src/lib/operations.ts +++ b/src/lib/operations.ts @@ -199,6 +199,59 @@ const prepareToEditExtrude: PrepareToEditCallback = async ({ operation }) => { } } +/** + * Gather up the argument values for the Loft command + * to be used in the command bar edit flow. + */ +const prepareToEditLoft: PrepareToEditCallback = async ({ operation }) => { + const baseCommand = { + name: 'Loft', + groupId: 'modeling', + } + if (operation.type !== 'StdLibCall') { + return { reason: 'Wrong operation type' } + } + + // 1. Map the unlabeled arguments to solid2d selections + const sketches = getSketchSelectionsFromOperation( + operation, + kclManager.artifactGraph + ) + if (err(sketches)) { + return { reason: "Couldn't retrieve sketches" } + } + + // 2. + // vDegree argument from a string to a KCL expression + let vDegree: KclCommandValue | undefined + if ('vDegree' in operation.labeledArgs && operation.labeledArgs.vDegree) { + const result = await stringToKclExpression( + codeManager.code.slice( + operation.labeledArgs.vDegree.sourceRange[0], + operation.labeledArgs.vDegree.sourceRange[1] + ) + ) + if (err(result) || 'errors' in result) { + return { reason: "Couldn't retrieve vDegree argument" } + } + + vDegree = result + } + + // 3. Assemble the default argument values for the command, + // with `nodeToEdit` set, which will let the actor know + // to edit the node that corresponds to the StdLibCall. + const argDefaultValues: ModelingCommandSchema['Loft'] = { + sketches, + vDegree, + nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath), + } + return { + ...baseCommand, + argDefaultValues, + } +} + /** * Gather up the argument values for the Chamfer or Fillet command * to be used in the command bar edit flow. @@ -1046,6 +1099,7 @@ export const stdLibMap: Record = { loft: { label: 'Loft', icon: 'loft', + prepareToEdit: prepareToEditLoft, supportsAppearance: true, supportsTransform: true, }, diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 630e372e5..543974717 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -2516,9 +2516,8 @@ export const modelingMachine = setup({ return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE)) } - const { sketches } = input const { ast } = kclManager - const astResult = addLoft({ ast, sketches }) + const astResult = addLoft({ ast, ...input }) if (err(astResult)) { return Promise.reject(astResult) }