Add bidirectional and twist optional args to point-and-click Extrude (#7496)
* WIP: Add bidirectional args to point-and-click Extrude Will eventually close #7495 * Wire up edit flow for symmetric * Show skip true args in header in review phase * Add bidirectionalLength * Make currentArg always part of header * WIP * Add twistAng * Proper optional args line in review * Labels in progress button and option arg section heading * Clean up extrude specific changes * Clean up to separate from #7506 * Clean up to separate from #7506 * More clean up across branches * More UI polish * Remove options bool icon * Fix labels for tests * Upgrade e2e tests to cmdBar fixtures with fixes * More fixes * Fixed up more tests related to sweep behavior change * Fix nodeToEdit not having hidden: true on Shell * Add typecheck * WIP: footer buttons * back to reg width * Clean up * Update snapshots * Update snapshots * Clean up * Fix tests and remove label * Refactor * Fix offset plane test * Add CommandBarDivider * Fix step back * Update snapshots * Add comment * Fix it, thanks bot * Clean up and inline optional heading * Little case tweak * Update src/components/CommandBar/CommandBarReview.tsx Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * Rename to CommandBarHeaderFooter * Rename to CommandBarHeaderFooter * Clean things up and fix edit * Add test * Revert something quick * Reorg args to match kcl order * Clean up edit arg retrieval error checks * Lint --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
@ -187,6 +187,13 @@ export class CmdBarFixture {
|
|||||||
return this.page.getByRole('option', options)
|
return this.page.getByRole('option', options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an optional argument from the command bar during review
|
||||||
|
*/
|
||||||
|
clickOptionalArgument = async (argName: string) => {
|
||||||
|
await this.page.getByTestId(`cmd-bar-add-optional-arg-${argName}`).click()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clicks the Create new variable button for kcl input
|
* Clicks the Create new variable button for kcl input
|
||||||
*/
|
*/
|
||||||
|
@ -4923,4 +4923,154 @@ extrude001 = extrude(profile001 length = 1)`
|
|||||||
await editor.expectEditor.toContain(badCode, { shouldNormalise: true })
|
await editor.expectEditor.toContain(badCode, { shouldNormalise: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Point-and-click extrude with optional args', async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const squareProfileCode = `length001 = 100
|
||||||
|
sketch001 = startSketchOn(XY)
|
||||||
|
profile001 = startProfile(sketch001, at = [0, 0])
|
||||||
|
|> yLine(length = length001)
|
||||||
|
|> xLine(length = length001)
|
||||||
|
|> yLine(length = -length001)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
`
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, squareProfileCode)
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await test.step('Select through code', async () => {
|
||||||
|
await editor.selectText('startProfile(sketch001, at = [0, 0])')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Go through command bar flow', async () => {
|
||||||
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'length',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '1 profile',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'length',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '1 profile',
|
||||||
|
Length: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.clickOptionalArgument('bidirectionalLength')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'bidirectionalLength',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '1 profile',
|
||||||
|
Length: '5',
|
||||||
|
BidirectionalLength: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'bidirectionalLength',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('10')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '1 profile',
|
||||||
|
Length: '5',
|
||||||
|
BidirectionalLength: '10',
|
||||||
|
},
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Check that the code has changed', async () => {
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`extrude001 = extrude(profile001, length = 5, bidirectionalLength = 10)`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Go through the edit flow via feature tree', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||||
|
await op.dblclick()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'length',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Length: '5',
|
||||||
|
BidirectionalLength: '10',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'length',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('10')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.getByRole('button', { name: 'BidirectionalLength' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'bidirectionalLength',
|
||||||
|
currentArgValue: '10',
|
||||||
|
headerArguments: {
|
||||||
|
Length: '10',
|
||||||
|
BidirectionalLength: '10',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'bidirectionalLength',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('20')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Length: '10',
|
||||||
|
BidirectionalLength: '20',
|
||||||
|
},
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Check that the code has changed again', async () => {
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`extrude001 = extrude(profile001, length = 10, bidirectionalLength = 20)`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
@ -102,7 +102,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
([argName, arg]) => {
|
([argName, arg]) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
data-testid="cmd-bar-add-optional-arg"
|
data-testid={`cmd-bar-add-optional-arg-${argName}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
|
@ -37,11 +37,17 @@ export function addExtrude({
|
|||||||
ast,
|
ast,
|
||||||
sketches,
|
sketches,
|
||||||
length,
|
length,
|
||||||
|
symmetric,
|
||||||
|
bidirectionalLength,
|
||||||
|
twistAngle,
|
||||||
nodeToEdit,
|
nodeToEdit,
|
||||||
}: {
|
}: {
|
||||||
ast: Node<Program>
|
ast: Node<Program>
|
||||||
sketches: Selections
|
sketches: Selections
|
||||||
length: KclCommandValue
|
length: KclCommandValue
|
||||||
|
symmetric?: boolean
|
||||||
|
bidirectionalLength?: KclCommandValue
|
||||||
|
twistAngle?: KclCommandValue
|
||||||
nodeToEdit?: PathToNode
|
nodeToEdit?: PathToNode
|
||||||
}):
|
}):
|
||||||
| {
|
| {
|
||||||
@ -63,15 +69,48 @@ export function addExtrude({
|
|||||||
return sketchesExprList
|
return sketchesExprList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra labeled args expressions
|
||||||
|
const symmetricExpr = symmetric
|
||||||
|
? [createLabeledArg('symmetric', createLiteral(symmetric))]
|
||||||
|
: []
|
||||||
|
const bidirectionalLengthExpr = bidirectionalLength
|
||||||
|
? [
|
||||||
|
createLabeledArg(
|
||||||
|
'bidirectionalLength',
|
||||||
|
valueOrVariable(bidirectionalLength)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
const twistAngleExpr = twistAngle
|
||||||
|
? [createLabeledArg('twistAngle', valueOrVariable(twistAngle))]
|
||||||
|
: []
|
||||||
|
|
||||||
const sketchesExpr = createSketchExpression(sketchesExprList)
|
const sketchesExpr = createSketchExpression(sketchesExprList)
|
||||||
const call = createCallExpressionStdLibKw('extrude', sketchesExpr, [
|
const call = createCallExpressionStdLibKw('extrude', sketchesExpr, [
|
||||||
createLabeledArg('length', valueOrVariable(length)),
|
createLabeledArg('length', valueOrVariable(length)),
|
||||||
|
...symmetricExpr,
|
||||||
|
...bidirectionalLengthExpr,
|
||||||
|
...twistAngleExpr,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Insert variables for labeled arguments if provided
|
// Insert variables for labeled arguments if provided
|
||||||
if ('variableName' in length && length.variableName) {
|
if ('variableName' in length && length.variableName) {
|
||||||
insertVariableAndOffsetPathToNode(length, modifiedAst, nodeToEdit)
|
insertVariableAndOffsetPathToNode(length, modifiedAst, nodeToEdit)
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
bidirectionalLength &&
|
||||||
|
'variableName' in bidirectionalLength &&
|
||||||
|
bidirectionalLength.variableName
|
||||||
|
) {
|
||||||
|
insertVariableAndOffsetPathToNode(
|
||||||
|
bidirectionalLength,
|
||||||
|
modifiedAst,
|
||||||
|
nodeToEdit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (twistAngle && 'variableName' in twistAngle && twistAngle.variableName) {
|
||||||
|
insertVariableAndOffsetPathToNode(twistAngle, modifiedAst, nodeToEdit)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. If edit, we assign the new function call declaration to the existing node,
|
// 3. If edit, we assign the new function call declaration to the existing node,
|
||||||
// otherwise just push to the end
|
// otherwise just push to the end
|
||||||
|
@ -75,6 +75,9 @@ export type ModelingCommandSchema = {
|
|||||||
// KCL stdlib arguments
|
// KCL stdlib arguments
|
||||||
sketches: Selections
|
sketches: Selections
|
||||||
length: KclCommandValue
|
length: KclCommandValue
|
||||||
|
symmetric?: boolean
|
||||||
|
bidirectionalLength?: KclCommandValue
|
||||||
|
twistAngle?: KclCommandValue
|
||||||
}
|
}
|
||||||
Sweep: {
|
Sweep: {
|
||||||
// Enables editing workflow
|
// Enables editing workflow
|
||||||
@ -408,6 +411,22 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
defaultValue: KCL_DEFAULT_LENGTH,
|
defaultValue: KCL_DEFAULT_LENGTH,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
symmetric: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ name: 'False', value: false },
|
||||||
|
{ name: 'True', value: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
bidirectionalLength: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
twistAngle: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Sweep: {
|
Sweep: {
|
||||||
|
@ -25,7 +25,7 @@ import type {
|
|||||||
HelixModes,
|
HelixModes,
|
||||||
ModelingCommandSchema,
|
ModelingCommandSchema,
|
||||||
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||||
import type { KclExpression } from '@src/lib/commandTypes'
|
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
stringToKclExpression,
|
stringToKclExpression,
|
||||||
retrieveArgFromPipedCallExpression,
|
retrieveArgFromPipedCallExpression,
|
||||||
@ -134,12 +134,63 @@ const prepareToEditExtrude: PrepareToEditCallback = async ({ operation }) => {
|
|||||||
return { reason: "Couldn't retrieve length argument" }
|
return { reason: "Couldn't retrieve length argument" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// symmetric argument from a string to boolean
|
||||||
|
let symmetric: boolean | undefined
|
||||||
|
if ('symmetric' in operation.labeledArgs && operation.labeledArgs.symmetric) {
|
||||||
|
symmetric =
|
||||||
|
codeManager.code.slice(
|
||||||
|
operation.labeledArgs.symmetric.sourceRange[0],
|
||||||
|
operation.labeledArgs.symmetric.sourceRange[1]
|
||||||
|
) === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// bidirectionalLength argument from a string to a KCL expression
|
||||||
|
let bidirectionalLength: KclCommandValue | undefined
|
||||||
|
if (
|
||||||
|
'bidirectionalLength' in operation.labeledArgs &&
|
||||||
|
operation.labeledArgs.bidirectionalLength
|
||||||
|
) {
|
||||||
|
const result = await stringToKclExpression(
|
||||||
|
codeManager.code.slice(
|
||||||
|
operation.labeledArgs.bidirectionalLength.sourceRange[0],
|
||||||
|
operation.labeledArgs.bidirectionalLength.sourceRange[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (err(result) || 'errors' in result) {
|
||||||
|
return { reason: "Couldn't retrieve bidirectionalLength argument" }
|
||||||
|
}
|
||||||
|
|
||||||
|
bidirectionalLength = result
|
||||||
|
}
|
||||||
|
|
||||||
|
// twistAngle argument from a string to a KCL expression
|
||||||
|
let twistAngle: KclCommandValue | undefined
|
||||||
|
if (
|
||||||
|
'twistAngle' in operation.labeledArgs &&
|
||||||
|
operation.labeledArgs.twistAngle
|
||||||
|
) {
|
||||||
|
const result = await stringToKclExpression(
|
||||||
|
codeManager.code.slice(
|
||||||
|
operation.labeledArgs.twistAngle.sourceRange[0],
|
||||||
|
operation.labeledArgs.twistAngle.sourceRange[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (err(result) || 'errors' in result) {
|
||||||
|
return { reason: "Couldn't retrieve twistAngle argument" }
|
||||||
|
}
|
||||||
|
|
||||||
|
twistAngle = result
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Assemble the default argument values for the command,
|
// 3. Assemble the default argument values for the command,
|
||||||
// with `nodeToEdit` set, which will let the actor know
|
// with `nodeToEdit` set, which will let the actor know
|
||||||
// to edit the node that corresponds to the StdLibCall.
|
// to edit the node that corresponds to the StdLibCall.
|
||||||
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
|
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
|
||||||
sketches,
|
sketches,
|
||||||
length,
|
length,
|
||||||
|
symmetric,
|
||||||
|
bidirectionalLength,
|
||||||
|
twistAngle,
|
||||||
nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath),
|
nodeToEdit: pathToNodeFromRustNodePath(operation.nodePath),
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -143,6 +143,7 @@ export const commandBarMachine = setup({
|
|||||||
: !a[1].hidden
|
: !a[1].hidden
|
||||||
)
|
)
|
||||||
let argIndex = 0
|
let argIndex = 0
|
||||||
|
let lastRequiredArg: CommandArgumentWithName<unknown> | undefined
|
||||||
|
|
||||||
while (argIndex < nonHiddenArgs.length) {
|
while (argIndex < nonHiddenArgs.length) {
|
||||||
const [argName, argConfig] = nonHiddenArgs[argIndex]
|
const [argName, argConfig] = nonHiddenArgs[argIndex]
|
||||||
@ -150,13 +151,14 @@ export const commandBarMachine = setup({
|
|||||||
typeof argConfig.required === 'function'
|
typeof argConfig.required === 'function'
|
||||||
? argConfig.required(context)
|
? argConfig.required(context)
|
||||||
: argConfig.required
|
: argConfig.required
|
||||||
/**
|
|
||||||
* TODO: we need to think harder about the relationship between
|
if (argIsRequired) {
|
||||||
* `required`, `skip`, and `hidden`.
|
lastRequiredArg = {
|
||||||
* This bit of logic essentially makes "skip false" arguments required.
|
...argConfig,
|
||||||
* We may need a bit of state to mark an argument as "visited" for "skip false" args
|
name: argName,
|
||||||
* to truly not require any value to continue.
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
const mustNotSkipArg =
|
const mustNotSkipArg =
|
||||||
(argIsRequired || argConfig.skip === false) &&
|
(argIsRequired || argConfig.skip === false) &&
|
||||||
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||||
@ -166,12 +168,21 @@ export const commandBarMachine = setup({
|
|||||||
'name' in rejectedArg &&
|
'name' in rejectedArg &&
|
||||||
rejectedArg.name === argName))
|
rejectedArg.name === argName))
|
||||||
|
|
||||||
if (
|
if (mustNotSkipArg) {
|
||||||
mustNotSkipArg === true ||
|
return {
|
||||||
|
...selectedCommand.args[argName],
|
||||||
|
name: argName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedEndOfArgs =
|
||||||
argIndex + 1 === Object.keys(nonHiddenArgs).length
|
argIndex + 1 === Object.keys(nonHiddenArgs).length
|
||||||
) {
|
if (reachedEndOfArgs) {
|
||||||
// If we have reached the end of the arguments and none are skippable,
|
if (lastRequiredArg) {
|
||||||
// return the last argument.
|
return lastRequiredArg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to the last argument that is not hidden
|
||||||
return {
|
return {
|
||||||
...selectedCommand.args[argName],
|
...selectedCommand.args[argName],
|
||||||
name: argName,
|
name: argName,
|
||||||
|
@ -2435,12 +2435,22 @@ export const modelingMachine = setup({
|
|||||||
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
return Promise.reject(new Error(NO_INPUT_PROVIDED_MESSAGE))
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodeToEdit, sketches, length } = input
|
const {
|
||||||
|
nodeToEdit,
|
||||||
|
sketches,
|
||||||
|
length,
|
||||||
|
symmetric,
|
||||||
|
bidirectionalLength,
|
||||||
|
twistAngle,
|
||||||
|
} = input
|
||||||
const { ast } = kclManager
|
const { ast } = kclManager
|
||||||
const astResult = addExtrude({
|
const astResult = addExtrude({
|
||||||
ast,
|
ast,
|
||||||
sketches,
|
sketches,
|
||||||
length,
|
length,
|
||||||
|
symmetric,
|
||||||
|
bidirectionalLength,
|
||||||
|
twistAngle,
|
||||||
nodeToEdit,
|
nodeToEdit,
|
||||||
})
|
})
|
||||||
if (err(astResult)) {
|
if (err(astResult)) {
|
||||||
|
Reference in New Issue
Block a user