Point-and-click Helix from cylinders (#5979)

This commit is contained in:
Pierre Jacquier
2025-03-26 17:57:30 -04:00
committed by GitHub
parent 4b2c745db5
commit 11160f0b40
11 changed files with 513 additions and 217 deletions

View File

@ -1082,8 +1082,8 @@ openSketch = startSketchOn(XY)
}) => { }) => {
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 } const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix( revolutions = 1, angleStart = 360, ccw = false, radius = 5, axis = 'X', length = 5,)` const expectedOutput = `helix001 = helix( axis = 'X', radius = 5, length = 5, revolutions = 1, angleStart = 360, ccw = false,)`
const expectedLine = `revolutions=1,` const expectedLine = `axis='X',`
await homePage.goToModelingScene() await homePage.goToModelingScene()
@ -1091,17 +1091,17 @@ openSketch = startSketchOn(XY)
await toolbar.helixButton.click() await toolbar.helixButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'arguments', stage: 'arguments',
currentArgKey: 'axisOrEdge', currentArgKey: 'mode',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Mode: '',
AngleStart: '', AngleStart: '',
AxisOrEdge: '', Revolutions: '',
CounterClockWise: '',
Length: '', Length: '',
Radius: '', Radius: '',
Revolutions: '', CounterClockWise: '',
}, },
highlightedHeaderArg: 'axisOrEdge', highlightedHeaderArg: 'mode',
commandName: 'Helix', commandName: 'Helix',
}) })
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
@ -1110,7 +1110,19 @@ openSketch = startSketchOn(XY)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.expectState({
stage: 'review',
headerArguments: {
Mode: 'Axis',
Axis: 'X',
AngleStart: '360',
Revolutions: '1',
Length: '5',
Radius: '5',
CounterClockWise: '',
},
commandName: 'Helix',
})
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
}) })
@ -1134,30 +1146,31 @@ openSketch = startSketchOn(XY)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Helix', commandName: 'Helix',
stage: 'arguments', stage: 'arguments',
currentArgKey: 'length', currentArgKey: 'CounterClockWise',
currentArgValue: initialInput, currentArgValue: '',
headerArguments: { headerArguments: {
AngleStart: '360',
Axis: 'X', Axis: 'X',
CounterClockWise: '', AngleStart: '360',
Length: initialInput,
Radius: '5',
Revolutions: '1', Revolutions: '1',
Radius: '5',
Length: initialInput,
CounterClockWise: '',
}, },
highlightedHeaderArg: 'length', highlightedHeaderArg: 'CounterClockWise',
}) })
await page.keyboard.press('Shift+Backspace')
await expect(cmdBar.currentArgumentInput).toBeVisible() await expect(cmdBar.currentArgumentInput).toBeVisible()
await cmdBar.currentArgumentInput.locator('.cm-content').fill(newInput) await cmdBar.currentArgumentInput.locator('.cm-content').fill(newInput)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'review', stage: 'review',
headerArguments: { headerArguments: {
AngleStart: '360',
Axis: 'X', Axis: 'X',
CounterClockWise: '', AngleStart: '360',
Length: newInput,
Radius: '5',
Revolutions: '1', Revolutions: '1',
Radius: '5',
Length: newInput,
CounterClockWise: '',
}, },
commandName: 'Helix', commandName: 'Helix',
}) })
@ -1181,14 +1194,14 @@ openSketch = startSketchOn(XY)
{ {
selectionType: 'segment', selectionType: 'segment',
testPoint: { x: 513, y: 221 }, testPoint: { x: 513, y: 221 },
expectedOutput: `helix001 = helix( revolutions = 20, angleStart = 0, ccw = true, radius = 1, axis = seg01, length = 100,)`, expectedOutput: `helix001 = helix( axis = seg01, radius = 1, length = 100, revolutions = 20, angleStart = 0, ccw = false,)`,
expectedEditedOutput: `helix001 = helix( revolutions = 20, angleStart = 0, ccw = true, radius = 1, axis = seg01, length = 50,)`, expectedEditedOutput: `helix001 = helix( axis = seg01, radius = 1, length = 50, revolutions = 20, angleStart = 0, ccw = false,)`,
}, },
{ {
selectionType: 'sweepEdge', selectionType: 'sweepEdge',
testPoint: { x: 564, y: 364 }, testPoint: { x: 564, y: 364 },
expectedOutput: `helix001 = helix( revolutions = 20, angleStart = 0, ccw = true, radius = 1, axis = getOppositeEdge(seg01), length = 100,)`, expectedOutput: `helix001 = helix( axis = getOppositeEdge(seg01), radius = 1, length = 100, revolutions = 20, angleStart = 0, ccw = false,)`,
expectedEditedOutput: `helix001 = helix( revolutions = 20, angleStart = 0, ccw = true, radius = 1, axis = getOppositeEdge(seg01), length = 50,)`, expectedEditedOutput: `helix001 = helix( axis = getOppositeEdge(seg01), radius = 1, length = 50, revolutions = 20, angleStart = 0, ccw = false,)`,
}, },
] ]
helixCases.map( helixCases.map(
@ -1225,17 +1238,17 @@ openSketch = startSketchOn(XY)
await toolbar.helixButton.click() await toolbar.helixButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'arguments', stage: 'arguments',
currentArgKey: 'axisOrEdge', currentArgKey: 'mode',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
AngleStart: '', AngleStart: '',
AxisOrEdge: '', Mode: '',
CounterClockWise: '', CounterClockWise: '',
Length: '', Length: '',
Radius: '', Radius: '',
Revolutions: '', Revolutions: '',
}, },
highlightedHeaderArg: 'axisOrEdge', highlightedHeaderArg: 'mode',
commandName: 'Helix', commandName: 'Helix',
}) })
await cmdBar.selectOption({ name: 'Edge' }).click() await cmdBar.selectOption({ name: 'Edge' }).click()
@ -1246,7 +1259,6 @@ openSketch = startSketchOn(XY)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await page.keyboard.insertText('0') await page.keyboard.insertText('0')
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.selectOption({ name: 'True' }).click()
await page.keyboard.insertText('1') await page.keyboard.insertText('1')
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await page.keyboard.insertText('100') await page.keyboard.insertText('100')
@ -1254,13 +1266,13 @@ openSketch = startSketchOn(XY)
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'review', stage: 'review',
headerArguments: { headerArguments: {
AngleStart: '0', Mode: 'Edge',
AxisOrEdge: 'Edge',
Edge: `1 ${selectionType}`, Edge: `1 ${selectionType}`,
CounterClockWise: '', AngleStart: '0',
Length: '100',
Radius: '1',
Revolutions: '20', Revolutions: '20',
Radius: '1',
Length: '100',
CounterClockWise: '',
}, },
commandName: 'Helix', commandName: 'Helix',
}) })
@ -1285,17 +1297,18 @@ openSketch = startSketchOn(XY)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Helix', commandName: 'Helix',
stage: 'arguments', stage: 'arguments',
currentArgKey: 'length', currentArgKey: 'CounterClockWise',
currentArgValue: initialInput, currentArgValue: '',
headerArguments: { headerArguments: {
AngleStart: '0', AngleStart: '0',
CounterClockWise: '',
Length: initialInput,
Radius: '1',
Revolutions: '20', Revolutions: '20',
Radius: '1',
Length: initialInput,
CounterClockWise: '',
}, },
highlightedHeaderArg: 'length', highlightedHeaderArg: 'CounterClockWise',
}) })
await page.keyboard.press('Shift+Backspace')
await expect(cmdBar.currentArgumentInput).toBeVisible() await expect(cmdBar.currentArgumentInput).toBeVisible()
await cmdBar.currentArgumentInput await cmdBar.currentArgumentInput
.locator('.cm-content') .locator('.cm-content')
@ -1305,10 +1318,10 @@ openSketch = startSketchOn(XY)
stage: 'review', stage: 'review',
headerArguments: { headerArguments: {
AngleStart: '0', AngleStart: '0',
CounterClockWise: '',
Length: newInput,
Radius: '1',
Revolutions: '20', Revolutions: '20',
Radius: '1',
Length: newInput,
CounterClockWise: '',
}, },
commandName: 'Helix', commandName: 'Helix',
}) })
@ -1336,6 +1349,141 @@ openSketch = startSketchOn(XY)
} }
) )
test('Helix point-and-click on cylinder', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XY)
profile001 = circle(
sketch001,
center = [0, 0],
radius = 100,
tag = $seg01,
)
extrude001 = extrude(profile001, length = 100)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 }
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `helix001 = helix( cylinder = extrude001, revolutions = 1, angleStart = 360, ccw = false,)`
const expectedLine = `cylinder = extrude001,`
const expectedEditedOutput = `helix001 = helix( cylinder = extrude001, revolutions = 1, angleStart = 360, ccw = true,)`
await test.step(`Go through the command bar flow`, async () => {
await toolbar.helixButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'mode',
currentArgValue: '',
headerArguments: {
Mode: '',
AngleStart: '',
Revolutions: '',
Length: '',
Radius: '',
CounterClockWise: '',
},
highlightedHeaderArg: 'mode',
commandName: 'Helix',
})
await cmdBar.selectOption({ name: 'Cylinder' }).click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'cylinder',
currentArgValue: '',
headerArguments: {
Mode: 'Cylinder',
Cylinder: '',
AngleStart: '',
Revolutions: '',
CounterClockWise: '',
},
highlightedHeaderArg: 'cylinder',
commandName: 'Helix',
})
await clickOnWall()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Mode: 'Cylinder',
Cylinder: '1 face',
AngleStart: '360',
Revolutions: '1',
CounterClockWise: '',
},
commandName: 'Helix',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedLine],
highlightedCode: '',
})
})
await test.step(`Edit helix through the feature tree`, async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
await operationButton.dblclick()
await cmdBar.expectState({
commandName: 'Helix',
stage: 'arguments',
currentArgKey: 'CounterClockWise',
currentArgValue: '',
headerArguments: {
AngleStart: '360',
Revolutions: '1',
CounterClockWise: '',
},
highlightedHeaderArg: 'CounterClockWise',
})
await cmdBar.selectOption({ name: 'True' }).click()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
AngleStart: '360',
Revolutions: '1',
CounterClockWise: 'true',
},
commandName: 'Helix',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(expectedEditedOutput)
await editor.closePane()
})
await test.step('Delete helix via feature tree selection', async () => {
await toolbar.openPane('feature-tree')
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.not.toContain(expectedEditedOutput)
})
})
const loftPointAndClickCases = [ const loftPointAndClickCases = [
{ shouldPreselect: true }, { shouldPreselect: true },
{ shouldPreselect: false }, { shouldPreselect: false },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -80,7 +80,8 @@ function CommandBarKclInput({
: arg.variableName : arg.variableName
} }
// or derive it from the previously set value or the argument name // or derive it from the previously set value or the argument name
return previouslySetValue && 'variableName' in previouslySetValue return typeof previouslySetValue === 'object' &&
'variableName' in previouslySetValue
? previouslySetValue.variableName ? previouslySetValue.variableName
: arg.name : arg.name
}, [ }, [
@ -96,7 +97,8 @@ function CommandBarKclInput({
) )
const [value, setValue] = useState(initialValue) const [value, setValue] = useState(initialValue)
const [createNewVariable, setCreateNewVariable] = useState( const [createNewVariable, setCreateNewVariable] = useState(
(previouslySetValue && 'variableName' in previouslySetValue) || (typeof previouslySetValue === 'object' &&
'variableName' in previouslySetValue) ||
arg.createVariable === 'byDefault' || arg.createVariable === 'byDefault' ||
arg.createVariable === 'force' || arg.createVariable === 'force' ||
false false
@ -132,7 +134,8 @@ function CommandBarKclInput({
selection: { selection: {
anchor: 0, anchor: 0,
head: head:
previouslySetValue && 'valueText' in previouslySetValue typeof previouslySetValue === 'object' &&
'valueText' in previouslySetValue
? previouslySetValue.valueText.length ? previouslySetValue.valueText.length
: defaultValue.length, : defaultValue.length,
}, },

View File

@ -811,40 +811,53 @@ export function addOffsetPlane({
*/ */
export function addHelix({ export function addHelix({
node, node,
axis,
cylinder,
revolutions, revolutions,
angleStart, angleStart,
ccw,
radius, radius,
axis,
length, length,
ccw,
insertIndex, insertIndex,
variableName, variableName,
}: { }: {
node: Node<Program> node: Node<Program>
axis?: Node<Literal> | Node<Name | CallExpression | CallExpressionKw>
cylinder?: VariableDeclarator
revolutions: Expr revolutions: Expr
angleStart: Expr angleStart: Expr
radius?: Expr
length?: Expr
ccw: boolean ccw: boolean
radius: Expr
axis: Node<Literal> | Node<Name | CallExpression | CallExpressionKw>
length: Expr
insertIndex?: number insertIndex?: number
variableName?: string variableName?: string
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } { }): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node) const modifiedAst = structuredClone(node)
const name = const name =
variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX) variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
const modeArgs: CallExpressionKw['arguments'] = []
if (axis && radius) {
modeArgs.push(createLabeledArg('axis', axis))
modeArgs.push(createLabeledArg('radius', radius))
if (length) {
modeArgs.push(createLabeledArg('length', length))
}
} else if (cylinder) {
modeArgs.push(
createLabeledArg('cylinder', createLocalName(cylinder.id.name))
)
}
const variable = createVariableDeclaration( const variable = createVariableDeclaration(
name, name,
createCallExpressionStdLibKw( createCallExpressionStdLibKw(
'helix', 'helix',
null, // Not in a pipeline null, // Not in a pipeline
[ [
...modeArgs,
createLabeledArg('revolutions', revolutions), createLabeledArg('revolutions', revolutions),
createLabeledArg('angleStart', angleStart), createLabeledArg('angleStart', angleStart),
createLabeledArg('ccw', createLiteral(ccw)), createLabeledArg('ccw', createLiteral(ccw)),
createLabeledArg('radius', radius),
createLabeledArg('axis', axis),
createLabeledArg('length', length),
] ]
) )
) )

View File

@ -38,6 +38,8 @@ export const EXTRUSION_RESULTS = [
export const COMMAND_APPEARANCE_COLOR_DEFAULT = 'default' export const COMMAND_APPEARANCE_COLOR_DEFAULT = 'default'
export type HelixModes = 'Axis' | 'Edge' | 'Cylinder'
export type ModelingCommandSchema = { export type ModelingCommandSchema = {
'Enter sketch': {} 'Enter sketch': {}
Export: { Export: {
@ -103,15 +105,17 @@ export type ModelingCommandSchema = {
// Enables editing workflow // Enables editing workflow
nodeToEdit?: PathToNode nodeToEdit?: PathToNode
// Flow arg // Flow arg
axisOrEdge: 'Axis' | 'Edge' mode: HelixModes
// Three different arguments depending on mode
axis?: string
edge?: Selections
cylinder?: Selections
// KCL stdlib arguments // KCL stdlib arguments
axis: string | undefined
edge: Selections | undefined
revolutions: KclCommandValue revolutions: KclCommandValue
angleStart: KclCommandValue angleStart: KclCommandValue
ccw: boolean radius?: KclCommandValue // axis or edge modes only
radius: KclCommandValue length?: KclCommandValue // axis or edge modes only
length: KclCommandValue ccw: boolean // optional boolean argument, default value to false
} }
'event.parameter.create': { 'event.parameter.create': {
value: KclCommandValue value: KclCommandValue
@ -530,7 +534,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
Helix: { Helix: {
description: 'Create a helix or spiral in 3D about an axis.', description: 'Create a helix or spiral in 3D about an axis.',
icon: 'helix', icon: 'helix',
status: 'development',
needsReview: true, needsReview: true,
args: { args: {
nodeToEdit: { nodeToEdit: {
@ -541,69 +544,91 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: false, required: false,
hidden: true, hidden: true,
}, },
axisOrEdge: { mode: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: 'Axis', defaultValue: 'Axis',
options: [ options: [
{ name: 'Axis', isCurrent: true, value: 'Axis' }, { name: 'Axis', isCurrent: true, value: 'Axis' },
{ name: 'Edge', isCurrent: false, value: 'Edge' }, { name: 'Edge', isCurrent: false, value: 'Edge' },
{ name: 'Cylinder', isCurrent: false, value: 'Cylinder' },
], ],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
axis: { axis: {
inputType: 'options', inputType: 'options',
required: (commandContext) => required: (commandContext) =>
['Axis'].includes( ['Axis'].includes(commandContext.argumentsToSubmit.mode as string),
commandContext.argumentsToSubmit.axisOrEdge as string
),
options: [ options: [
{ name: 'X Axis', value: 'X' }, { name: 'X Axis', value: 'X' },
{ name: 'Y Axis', value: 'Y' }, { name: 'Y Axis', value: 'Y' },
{ name: 'Z Axis', value: 'Z' }, { name: 'Z Axis', value: 'Z' },
], ],
hidden: false, // for consistency here, we can actually edit here since it's not a selection
}, },
edge: { edge: {
required: (commandContext) => required: (commandContext) =>
['Edge'].includes( ['Edge'].includes(commandContext.argumentsToSubmit.mode as string),
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'selection', inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge'], selectionTypes: ['segment', 'sweepEdge'],
multiple: false, multiple: false,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
cylinder: {
required: (commandContext) =>
['Cylinder'].includes(
commandContext.argumentsToSubmit.mode as string
),
inputType: 'selection',
selectionTypes: ['wall'],
multiple: false,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
revolutions: { revolutions: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: '1', defaultValue: '1',
required: true, required: true,
warningMessage:
'The helix workflow is new and under tested. Please break it and report issues.',
}, },
angleStart: { angleStart: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE, defaultValue: KCL_DEFAULT_DEGREE,
required: true, required: true,
}, },
ccw: {
inputType: 'options',
required: true,
displayName: 'CounterClockWise',
defaultValue: false,
options: [
{ name: 'False', value: false },
{ name: 'True', value: true },
],
},
radius: { radius: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH, defaultValue: KCL_DEFAULT_LENGTH,
required: true, required: (commandContext) =>
!['Cylinder'].includes(
commandContext.argumentsToSubmit.mode as string
),
}, },
length: { length: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH, defaultValue: KCL_DEFAULT_LENGTH,
required: (commandContext) =>
!['Cylinder'].includes(
commandContext.argumentsToSubmit.mode as string
),
},
ccw: {
inputType: 'options',
skip: true,
required: true, required: true,
defaultValue: false,
valueSummary: (value) => String(value),
displayName: 'CounterClockWise',
options: (commandContext) => [
{
name: 'False',
value: false,
isCurrent: !Boolean(commandContext.argumentsToSubmit.ccw),
},
{
name: 'True',
value: true,
isCurrent: Boolean(commandContext.argumentsToSubmit.ccw),
},
],
}, },
}, },
}, },

View File

@ -5,6 +5,7 @@ import {
getCapCodeRef, getCapCodeRef,
getEdgeCutConsumedCodeRef, getEdgeCutConsumedCodeRef,
getSweepEdgeCodeRef, getSweepEdgeCodeRef,
getWallCodeRef,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { Operation } from '@rust/kcl-lib/bindings/Operation' import { Operation } from '@rust/kcl-lib/bindings/Operation'
import { codeManager, engineCommandManager, kclManager } from './singletons' import { codeManager, engineCommandManager, kclManager } from './singletons'
@ -13,10 +14,14 @@ import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { sourceRangeFromRust } from 'lang/wasm' import { sourceRangeFromRust } from 'lang/wasm'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { stringToKclExpression } from './kclHelpers' import { stringToKclExpression } from './kclHelpers'
import { ModelingCommandSchema } from './commandBarConfigs/modelingCommandConfig' import {
HelixModes,
ModelingCommandSchema,
} from './commandBarConfigs/modelingCommandConfig'
import { isDefaultPlaneStr } from './planes' import { isDefaultPlaneStr } from './planes'
import { Selection, Selections } from './selections' import { Selection, Selections } from './selections'
import { rustContext } from './singletons' import { rustContext } from './singletons'
import { KclExpression } from './commandTypes'
type ExecuteCommandEvent = CommandBarMachineEvent & { type ExecuteCommandEvent = CommandBarMachineEvent & {
type: 'Find and select command' type: 'Find and select command'
@ -571,27 +576,32 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
groupId: 'modeling', groupId: 'modeling',
} }
if (operation.type !== 'StdLibCall' || !operation.labeledArgs) { if (operation.type !== 'StdLibCall' || !operation.labeledArgs) {
return baseCommand return { reason: 'Wrong operation type or arguments' }
} }
// TODO: find a way to loop over the arguments while keeping it safe // Flow arg
let mode: HelixModes | undefined
// axis options string arg // Three different arguments depending on mode
if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis) {
return baseCommand
}
const axisValue = operation.labeledArgs.axis.value
let axisOrEdge: 'Axis' | 'Edge' | undefined
let axis: string | undefined let axis: string | undefined
let edge: Selections | undefined let edge: Selections | undefined
let cylinder: Selections | undefined
// Rest of stdlib args
let revolutions: KclExpression | undefined // common to all modes, can't remain undefined
let angleStart: KclExpression | undefined // common to all modes, can't remain undefined
let length: KclExpression | undefined // axis or edge modes only
let radius: KclExpression | undefined // axis or edge modes only
let ccw = false // optional boolean argument, default value
if ('axis' in operation.labeledArgs && operation.labeledArgs.axis) {
// axis options string or selection arg
const axisValue = operation.labeledArgs.axis.value
if (axisValue.type === 'String') { if (axisValue.type === 'String') {
// default axis casee // default axis casee
axisOrEdge = 'Axis' mode = 'Axis'
axis = axisValue.value axis = axisValue.value
} else if (axisValue.type === 'TagIdentifier' && axisValue.artifact_id) { } else if (axisValue.type === 'TagIdentifier' && axisValue.artifact_id) {
// segment case // segment case
axisOrEdge = 'Edge' mode = 'Edge'
const artifact = getArtifactOfTypes( const artifact = getArtifactOfTypes(
{ {
key: axisValue.artifact_id, key: axisValue.artifact_id,
@ -600,7 +610,7 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(artifact)) { if (err(artifact)) {
return baseCommand return { reason: "Couldn't find related edge artifact" }
} }
edge = { edge = {
@ -614,7 +624,7 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
} }
} else if (axisValue.type === 'Uuid') { } else if (axisValue.type === 'Uuid') {
// sweepEdge case // sweepEdge case
axisOrEdge = 'Edge' mode = 'Edge'
const artifact = getArtifactOfTypes( const artifact = getArtifactOfTypes(
{ {
key: axisValue.value, key: axisValue.value,
@ -623,7 +633,7 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(artifact)) { if (err(artifact)) {
return baseCommand return { reason: "Couldn't find related edge artifact" }
} }
const codeRef = getSweepEdgeCodeRef( const codeRef = getSweepEdgeCodeRef(
@ -631,7 +641,7 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(codeRef)) { if (err(codeRef)) {
return baseCommand return { reason: "Couldn't find related edge code ref" }
} }
edge = { edge = {
@ -644,94 +654,148 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
otherSelections: [], otherSelections: [],
} }
} else { } else {
return baseCommand return { reason: 'The type of the axis argument is unsupported' }
}
} else if (
'cylinder' in operation.labeledArgs &&
operation.labeledArgs.cylinder
) {
mode = 'Cylinder'
// axis cylinder selection arg
if (operation.labeledArgs.cylinder.value.type !== 'Solid') {
return { reason: "Cylinder arg found isn't of type Solid" }
} }
// revolutions kcl arg const sweepId = operation.labeledArgs.cylinder.value.value.artifactId
if ( const wallArtifact = [...engineCommandManager.artifactGraph.values()].find(
!('revolutions' in operation.labeledArgs) || (p) => p.type === 'wall' && p.sweepId === sweepId
!operation.labeledArgs.revolutions )
) { if (!wallArtifact || wallArtifact.type !== 'wall') {
return baseCommand return {
reason: "Cylinder arg found doesn't point to a valid sweep wall",
} }
const revolutions = await stringToKclExpression( }
const wallCodeRef = getWallCodeRef(
wallArtifact,
engineCommandManager.artifactGraph
)
if (err(wallCodeRef)) {
return {
reason: "Cylinder arg found doesn't point to a valid sweep code ref",
}
}
cylinder = {
graphSelections: [
{
artifact: wallArtifact,
codeRef: wallCodeRef,
},
],
otherSelections: [],
}
} else {
return {
reason: "The axis or cylinder arguments couldn't be prepared for edit",
}
}
// revolutions kcl arg (common for all)
if (
'revolutions' in operation.labeledArgs &&
operation.labeledArgs.revolutions
) {
const r = await stringToKclExpression(
codeManager.code.slice( codeManager.code.slice(
operation.labeledArgs.revolutions.sourceRange[0], operation.labeledArgs.revolutions.sourceRange[0],
operation.labeledArgs.revolutions.sourceRange[1] operation.labeledArgs.revolutions.sourceRange[1]
) )
) )
if (err(revolutions) || 'errors' in revolutions) return baseCommand if (err(r) || 'errors' in r) {
return { reason: 'Errors found in revolutions argument' }
// angleStart kcl arg
if (
!('angleStart' in operation.labeledArgs) ||
!operation.labeledArgs.angleStart
) {
return baseCommand
} }
const angleStart = await stringToKclExpression(
revolutions = r
} else {
return { reason: "Couldn't find revolutions argument" }
}
// angleStart kcl arg (common for all)
if (
'angleStart' in operation.labeledArgs &&
operation.labeledArgs.angleStart
) {
const r = await stringToKclExpression(
codeManager.code.slice( codeManager.code.slice(
operation.labeledArgs.angleStart.sourceRange[0], operation.labeledArgs.angleStart.sourceRange[0],
operation.labeledArgs.angleStart.sourceRange[1] operation.labeledArgs.angleStart.sourceRange[1]
) )
) )
if (err(angleStart) || 'errors' in angleStart) { if (err(r) || 'errors' in r) {
return baseCommand return { reason: 'Errors found in angleStart argument' }
} }
// counterClockWise options boolean arg angleStart = r
if (!('ccw' in operation.labeledArgs) || !operation.labeledArgs.ccw) { } else {
return baseCommand return { reason: "Couldn't find angleStart argument" }
} }
const ccw =
codeManager.code.slice(
operation.labeledArgs.ccw.sourceRange[0],
operation.labeledArgs.ccw.sourceRange[1]
) === 'true'
// radius kcl arg // radius and cylinder and kcl arg (only for axis or edge)
if (!('radius' in operation.labeledArgs) || !operation.labeledArgs.radius) { if (mode !== 'Cylinder') {
console.log( if ('radius' in operation.labeledArgs && operation.labeledArgs.radius) {
"!('radius' in operation.labeledArgs) || !operation.labeledArgs.radius" const r = await stringToKclExpression(
)
return baseCommand
}
const radius = await stringToKclExpression(
codeManager.code.slice( codeManager.code.slice(
operation.labeledArgs.radius.sourceRange[0], operation.labeledArgs.radius.sourceRange[0],
operation.labeledArgs.radius.sourceRange[1] operation.labeledArgs.radius.sourceRange[1]
) )
) )
if (err(radius) || 'errors' in radius) { if (err(r) || 'errors' in r) {
return baseCommand return { reason: 'Error in radius argument retrieval' }
}
radius = r
} else {
return { reason: "Couldn't find radius argument" }
} }
// length kcl arg if ('length' in operation.labeledArgs && operation.labeledArgs.length) {
if (!('length' in operation.labeledArgs) || !operation.labeledArgs.length) { const r = await stringToKclExpression(
return baseCommand
}
const length = await stringToKclExpression(
codeManager.code.slice( codeManager.code.slice(
operation.labeledArgs.length.sourceRange[0], operation.labeledArgs.length.sourceRange[0],
operation.labeledArgs.length.sourceRange[1] operation.labeledArgs.length.sourceRange[1]
) )
) )
if (err(length) || 'errors' in length) { if (err(r) || 'errors' in r) {
return baseCommand return { reason: 'Error in length argument retrieval' }
}
length = r
} else {
return { reason: "Couldn't find length argument" }
}
}
// counterClockWise boolean arg (optional)
if ('ccw' in operation.labeledArgs && operation.labeledArgs.ccw) {
ccw =
codeManager.code.slice(
operation.labeledArgs.ccw.sourceRange[0],
operation.labeledArgs.ccw.sourceRange[1]
) === 'true'
} }
// Assemble the default argument values for the Offset Plane command, // Assemble the default argument values for the Offset Plane command,
// with `nodeToEdit` set, which will let the Offset Plane actor know // with `nodeToEdit` set, which will let the Offset Plane actor know
// to edit the node that corresponds to the StdLibCall. // to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Helix'] = { const argDefaultValues: ModelingCommandSchema['Helix'] = {
axisOrEdge, mode,
axis, axis,
edge, edge,
cylinder,
revolutions, revolutions,
angleStart, angleStart,
ccw,
radius, radius,
length, length,
ccw,
nodeToEdit: getNodePathFromSourceRange( nodeToEdit: getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
sourceRangeFromRust(operation.sourceRange) sourceRangeFromRust(operation.sourceRange)

View File

@ -366,6 +366,7 @@ export const commandBarMachine = setup({
!(argConfig.inputType === 'kcl' || argConfig.skip) !(argConfig.inputType === 'kcl' || argConfig.skip)
const hasInvalidKclValue = const hasInvalidKclValue =
argConfig.inputType === 'kcl' && argConfig.inputType === 'kcl' &&
isRequired &&
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst !(argValue as Partial<KclCommandValue> | undefined)?.valueAst
const hasInvalidOptionsValue = const hasInvalidOptionsValue =
isRequired && isRequired &&

View File

@ -1,5 +1,9 @@
import { import {
CallExpression,
CallExpressionKw,
Expr, Expr,
Literal,
Name,
PathToNode, PathToNode,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
@ -1910,11 +1914,13 @@ export const modelingMachine = setup({
}) => { }) => {
if (!input) return new Error('No input provided') if (!input) return new Error('No input provided')
// Extract inputs // Extract inputs
console.log('input', input)
const ast = kclManager.ast const ast = kclManager.ast
const { const {
axisOrEdge, mode,
axis, axis,
edge, edge,
cylinder,
revolutions, revolutions,
angleStart, angleStart,
ccw, ccw,
@ -1946,16 +1952,50 @@ export const modelingMachine = setup({
opInsertIndex = nodeToEdit[1][0] opInsertIndex = nodeToEdit[1][0]
} }
const getAxisResult = getAxisExpressionAndIndex( let cylinderDeclarator: VariableDeclarator | undefined
axisOrEdge, let axisExpression:
axis, | Node<CallExpression | CallExpressionKw | Name>
edge, | Node<Literal>
ast | undefined
if (mode === 'Cylinder') {
if (
!(
cylinder &&
cylinder.graphSelections[0] &&
cylinder.graphSelections[0].artifact?.type === 'wall'
)
) {
return new Error('Cylinder argument not valid')
}
const clonedAstForGetExtrude = structuredClone(ast)
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
cylinder.graphSelections[0],
engineCommandManager.artifactGraph
)
if (err(extrudeLookupResult)) {
return extrudeLookupResult
}
const extrudeNode = getNodeFromPath<VariableDeclaration>(
ast,
extrudeLookupResult.pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeNode)) {
return extrudeNode
}
cylinderDeclarator = extrudeNode.node.declaration
} else if (mode === 'Axis' || mode === 'Edge') {
const getAxisResult = getAxisExpressionAndIndex(mode, axis, edge, ast)
if (err(getAxisResult)) {
return getAxisResult
}
axisExpression = getAxisResult.generatedAxis
} else {
return new Error(
'Generated axis or cylinder declarator selection is missing.'
) )
if (err(getAxisResult)) return getAxisResult
const { generatedAxis } = getAxisResult
if (!generatedAxis) {
return new Error('Generated axis selection is missing.')
} }
// TODO: figure out if we want to smart insert after the sketch as below // TODO: figure out if we want to smart insert after the sketch as below
@ -1965,13 +2005,13 @@ export const modelingMachine = setup({
// opInsertIndex = axisIndexIfAxis + 1 // opInsertIndex = axisIndexIfAxis + 1
// } // }
for (const variable of [revolutions, angleStart, radius, length]) { for (const v of [revolutions, angleStart, radius, length]) {
if (v === undefined) {
continue
}
const variable = v as KclCommandValue
// Insert the variable if it exists // Insert the variable if it exists
if ( if ('variableName' in variable && variable.variableName) {
'variableName' in variable &&
variable.variableName &&
variable.insertIndex !== undefined
) {
const newBody = [...ast.body] const newBody = [...ast.body]
newBody.splice( newBody.splice(
variable.insertIndex, variable.insertIndex,
@ -1982,19 +2022,21 @@ export const modelingMachine = setup({
} }
} }
const valueOrVariable = (variable: KclCommandValue) => const valueOrVariable = (variable: KclCommandValue) => {
'variableName' in variable return 'variableName' in variable
? variable.variableIdentifierAst ? variable.variableIdentifierAst
: variable.valueAst : variable.valueAst
}
const { modifiedAst, pathToNode } = addHelix({ const { modifiedAst, pathToNode } = addHelix({
node: ast, node: ast,
revolutions: valueOrVariable(revolutions), revolutions: valueOrVariable(revolutions),
angleStart: valueOrVariable(angleStart), angleStart: valueOrVariable(angleStart),
ccw, ccw,
radius: valueOrVariable(radius), radius: radius ? valueOrVariable(radius) : undefined,
axis: generatedAxis, axis: axisExpression,
length: valueOrVariable(length), cylinder: cylinderDeclarator,
length: length ? valueOrVariable(length) : undefined,
insertIndex: opInsertIndex, insertIndex: opInsertIndex,
variableName: opVariableName, variableName: opVariableName,
}) })