Enable optional arguments in point-and-click Loft (#7587)

* Enable optional arguments in point-and-click Sweep
Fixes #7578

* Fix bug and add e2e test step

* Fix review not triggering bug and e2e test

* WIP: Enable optional arguments in point-and-click Loft

* Add edit flow for loft

* WIP: e2e test and fix

* Got it

* Got it v2 🤦
This commit is contained in:
Pierre Jacquier
2025-06-23 18:24:52 -04:00
committed by GitHub
parent bb3a74076f
commit dbc87292e4
5 changed files with 147 additions and 78 deletions

View File

@ -1610,6 +1610,8 @@ sketch002 = startSketchOn(plane001)
testPoint.y + 80 testPoint.y + 80
) )
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])' 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 test.step(`Look for the white of the sketch001 shape`, async () => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15) await scene.expectPixelColor([254, 254, 254], testPoint, 15)
@ -1681,6 +1683,39 @@ sketch002 = startSketchOn(plane001)
await scene.expectPixelColor([89, 89, 89], testPoint, 15) 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 test.step('Delete loft via feature tree selection', async () => {
await editor.closePane() await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0) 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 = [ const sweepCases = [
{ {
targetType: 'circle', targetType: 'circle',

View File

@ -237,9 +237,13 @@ export function addSweep({
export function addLoft({ export function addLoft({
ast, ast,
sketches, sketches,
vDegree,
nodeToEdit,
}: { }: {
ast: Node<Program> ast: Node<Program>
sketches: Selections sketches: Selections
vDegree?: KclCommandValue
nodeToEdit?: PathToNode
}): }):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -251,21 +255,52 @@ export function addLoft({
// 2. Prepare unlabeled and labeled arguments // 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument // 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)) { if (err(sketchesExprList)) {
return sketchesExprList return sketchesExprList
} }
const sketchesExpr = createSketchExpression(sketchesExprList) // Extra labeled args expressions
const call = createCallExpressionStdLibKw('loft', sketchesExpr, []) const vDegreeExpr = vDegree
? [createLabeledArg('vDegree', valueOrVariable(vDegree))]
: []
// 3. Just push the declaration to the end const sketchesExpr = createSketchExpression(sketchesExprList)
// Note that Loft doesn't support edit flows yet since it's selection only atm const call = createCallExpressionStdLibKw('loft', sketchesExpr, [
const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT) ...vDegreeExpr,
const declaration = createVariableDeclaration(name, call) ])
modifiedAst.body.push(declaration)
const toFirstKwarg = false // Insert variables for labeled arguments if provided
const pathToNode = createPathToNode(modifiedAst, toFirstKwarg) 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<CallExpressionKw>(
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 { return {
modifiedAst, modifiedAst,

View File

@ -89,7 +89,11 @@ export type ModelingCommandSchema = {
relativeTo?: string relativeTo?: string
} }
Loft: { Loft: {
// Enables editing workflow
nodeToEdit?: PathToNode
// KCL stdlib arguments
sketches: Selections sketches: Selections
vDegree?: KclCommandValue
} }
Revolve: { Revolve: {
// Enables editing workflow // Enables editing workflow
@ -477,12 +481,20 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
icon: 'loft', icon: 'loft',
needsReview: true, needsReview: true,
args: { args: {
nodeToEdit: {
...nodeToEditProps,
},
sketches: { sketches: {
inputType: 'selection', inputType: 'selection',
displayName: 'Profiles', displayName: 'Profiles',
selectionTypes: ['solid2d'], selectionTypes: ['solid2d'],
multiple: true, multiple: true,
required: true, required: true,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
vDegree: {
inputType: 'kcl',
required: false,
}, },
}, },
}, },

View File

@ -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 * Gather up the argument values for the Chamfer or Fillet command
* to be used in the command bar edit flow. * to be used in the command bar edit flow.
@ -1046,6 +1099,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
loft: { loft: {
label: 'Loft', label: 'Loft',
icon: 'loft', icon: 'loft',
prepareToEdit: prepareToEditLoft,
supportsAppearance: true, supportsAppearance: true,
supportsTransform: true, supportsTransform: true,
}, },

View File

@ -2516,9 +2516,8 @@ export const modelingMachine = setup({
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE)) return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
} }
const { sketches } = input
const { ast } = kclManager const { ast } = kclManager
const astResult = addLoft({ ast, sketches }) const astResult = addLoft({ ast, ...input })
if (err(astResult)) { if (err(astResult)) {
return Promise.reject(astResult) return Promise.reject(astResult)
} }