Add edit flow for Helix (#5394)
* WIP: Add edit flow for Helix Fixes #5392 * Clean upp * Add e2e test step
This commit is contained in:
		@ -1125,7 +1125,49 @@ openSketch = startSketchOn('XY')
 | 
			
		||||
      await scene.expectPixelColor([250, 250, 250], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await test.step('Delete offset plane via feature tree selection', async () => {
 | 
			
		||||
    await test.step(`Edit helix through the feature tree`, async () => {
 | 
			
		||||
      await editor.closePane()
 | 
			
		||||
      const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
 | 
			
		||||
      await operationButton.dblclick()
 | 
			
		||||
      const initialInput = '5'
 | 
			
		||||
      const newInput = '50'
 | 
			
		||||
      await cmdBar.expectState({
 | 
			
		||||
        commandName: 'Helix',
 | 
			
		||||
        stage: 'arguments',
 | 
			
		||||
        currentArgKey: 'length',
 | 
			
		||||
        currentArgValue: initialInput,
 | 
			
		||||
        headerArguments: {
 | 
			
		||||
          AngleStart: '360',
 | 
			
		||||
          Axis: 'X',
 | 
			
		||||
          CounterClockWise: '',
 | 
			
		||||
          Length: initialInput,
 | 
			
		||||
          Radius: '5',
 | 
			
		||||
          Revolutions: '1',
 | 
			
		||||
        },
 | 
			
		||||
        highlightedHeaderArg: 'length',
 | 
			
		||||
      })
 | 
			
		||||
      await expect(cmdBar.currentArgumentInput).toBeVisible()
 | 
			
		||||
      await cmdBar.currentArgumentInput.locator('.cm-content').fill(newInput)
 | 
			
		||||
      await cmdBar.progressCmdBar()
 | 
			
		||||
      await cmdBar.expectState({
 | 
			
		||||
        stage: 'review',
 | 
			
		||||
        headerArguments: {
 | 
			
		||||
          AngleStart: '360',
 | 
			
		||||
          Axis: 'X',
 | 
			
		||||
          CounterClockWise: '',
 | 
			
		||||
          Length: newInput,
 | 
			
		||||
          Radius: '5',
 | 
			
		||||
          Revolutions: '1',
 | 
			
		||||
        },
 | 
			
		||||
        commandName: 'Helix',
 | 
			
		||||
      })
 | 
			
		||||
      await cmdBar.progressCmdBar()
 | 
			
		||||
      await toolbar.closeFeatureTreePane()
 | 
			
		||||
      await editor.openPane()
 | 
			
		||||
      await editor.expectEditor.toContain('length = ' + newInput)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await test.step('Delete helix via feature tree selection', async () => {
 | 
			
		||||
      await editor.closePane()
 | 
			
		||||
      const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
 | 
			
		||||
      await operationButton.click({ button: 'left' })
 | 
			
		||||
 | 
			
		||||
@ -717,6 +717,8 @@ export function addHelix({
 | 
			
		||||
  radius,
 | 
			
		||||
  axis,
 | 
			
		||||
  length,
 | 
			
		||||
  insertIndex,
 | 
			
		||||
  variableName,
 | 
			
		||||
}: {
 | 
			
		||||
  node: Node<Program>
 | 
			
		||||
  revolutions: Expr
 | 
			
		||||
@ -725,9 +727,12 @@ export function addHelix({
 | 
			
		||||
  radius: Expr
 | 
			
		||||
  axis: string
 | 
			
		||||
  length: Expr
 | 
			
		||||
  insertIndex?: number
 | 
			
		||||
  variableName?: string
 | 
			
		||||
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
 | 
			
		||||
  const modifiedAst = structuredClone(node)
 | 
			
		||||
  const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
 | 
			
		||||
  const name =
 | 
			
		||||
    variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
 | 
			
		||||
  const variable = createVariableDeclaration(
 | 
			
		||||
    name,
 | 
			
		||||
    createCallExpressionStdLibKw(
 | 
			
		||||
@ -744,12 +749,20 @@ export function addHelix({
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // TODO: figure out smart insertion than just appending at the end
 | 
			
		||||
  const insertAt =
 | 
			
		||||
    insertIndex !== undefined
 | 
			
		||||
      ? insertIndex
 | 
			
		||||
      : modifiedAst.body.length
 | 
			
		||||
      ? modifiedAst.body.length
 | 
			
		||||
      : 0
 | 
			
		||||
 | 
			
		||||
  modifiedAst.body.length
 | 
			
		||||
    ? modifiedAst.body.splice(insertAt, 0, variable)
 | 
			
		||||
    : modifiedAst.body.push(variable)
 | 
			
		||||
  const argIndex = 0
 | 
			
		||||
  modifiedAst.body.push(variable)
 | 
			
		||||
  const pathToNode: PathToNode = [
 | 
			
		||||
    ['body', ''],
 | 
			
		||||
    [modifiedAst.body.length - 1, 'index'],
 | 
			
		||||
    [insertAt, 'index'],
 | 
			
		||||
    ['declaration', 'VariableDeclaration'],
 | 
			
		||||
    ['init', 'VariableDeclarator'],
 | 
			
		||||
    ['arguments', 'CallExpressionKw'],
 | 
			
		||||
 | 
			
		||||
@ -83,6 +83,9 @@ export type ModelingCommandSchema = {
 | 
			
		||||
    distance: KclCommandValue
 | 
			
		||||
  }
 | 
			
		||||
  Helix: {
 | 
			
		||||
    // Enables editing workflow
 | 
			
		||||
    nodeToEdit?: PathToNode
 | 
			
		||||
    // KCL stdlib arguments
 | 
			
		||||
    revolutions: KclCommandValue
 | 
			
		||||
    angleStart: KclCommandValue
 | 
			
		||||
    counterClockWise: boolean
 | 
			
		||||
@ -472,6 +475,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
    status: 'development',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      nodeToEdit: {
 | 
			
		||||
        description:
 | 
			
		||||
          'Path to the node in the AST to edit. Never shown to the user.',
 | 
			
		||||
        skip: true,
 | 
			
		||||
        inputType: 'text',
 | 
			
		||||
        required: false,
 | 
			
		||||
      },
 | 
			
		||||
      revolutions: {
 | 
			
		||||
        inputType: 'kcl',
 | 
			
		||||
        defaultValue: '1',
 | 
			
		||||
@ -487,9 +497,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      counterClockWise: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        options: [
 | 
			
		||||
          { name: 'True', isCurrent: false, value: true },
 | 
			
		||||
          { name: 'False', isCurrent: true, value: false },
 | 
			
		||||
          { name: 'False', value: false },
 | 
			
		||||
          { name: 'True', value: true },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      radius: {
 | 
			
		||||
@ -500,10 +511,11 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      axis: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: 'X',
 | 
			
		||||
        options: [
 | 
			
		||||
          { name: 'X Axis', isCurrent: true, value: 'X' },
 | 
			
		||||
          { name: 'Y Axis', isCurrent: false, value: 'Y' },
 | 
			
		||||
          { name: 'Z Axis', isCurrent: false, value: 'Z' },
 | 
			
		||||
          { name: 'X Axis', value: 'X' },
 | 
			
		||||
          { name: 'Y Axis', value: 'Y' },
 | 
			
		||||
          { name: 'Z Axis', value: 'Z' },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      length: {
 | 
			
		||||
 | 
			
		||||
@ -191,6 +191,114 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
 | 
			
		||||
  const baseCommand = {
 | 
			
		||||
    name: 'Helix',
 | 
			
		||||
    groupId: 'modeling',
 | 
			
		||||
  }
 | 
			
		||||
  if (operation.type !== 'StdLibCall' || !operation.labeledArgs) {
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: find a way to loop over the arguments while keeping it safe
 | 
			
		||||
  // revolutions kcl arg
 | 
			
		||||
  if (
 | 
			
		||||
    !('revolutions' in operation.labeledArgs) ||
 | 
			
		||||
    !operation.labeledArgs.revolutions
 | 
			
		||||
  )
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const revolutions = await stringToKclExpression(
 | 
			
		||||
    codeManager.code.slice(
 | 
			
		||||
      operation.labeledArgs.revolutions.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.revolutions.sourceRange[1]
 | 
			
		||||
    ),
 | 
			
		||||
    {}
 | 
			
		||||
  )
 | 
			
		||||
  if (err(revolutions) || 'errors' in revolutions) return baseCommand
 | 
			
		||||
 | 
			
		||||
  // angleStart kcl arg
 | 
			
		||||
  if (
 | 
			
		||||
    !('angleStart' in operation.labeledArgs) ||
 | 
			
		||||
    !operation.labeledArgs.angleStart
 | 
			
		||||
  )
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const angleStart = await stringToKclExpression(
 | 
			
		||||
    codeManager.code.slice(
 | 
			
		||||
      operation.labeledArgs.angleStart.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.angleStart.sourceRange[1]
 | 
			
		||||
    ),
 | 
			
		||||
    {}
 | 
			
		||||
  )
 | 
			
		||||
  if (err(angleStart) || 'errors' in angleStart) return baseCommand
 | 
			
		||||
 | 
			
		||||
  // counterClockWise options boolean arg
 | 
			
		||||
  if (
 | 
			
		||||
    !('counterClockWise' in operation.labeledArgs) ||
 | 
			
		||||
    !operation.labeledArgs.counterClockWise
 | 
			
		||||
  )
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const counterClockWise =
 | 
			
		||||
    codeManager.code.slice(
 | 
			
		||||
      operation.labeledArgs.counterClockWise.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.counterClockWise.sourceRange[1]
 | 
			
		||||
    ) === 'true'
 | 
			
		||||
 | 
			
		||||
  // radius kcl arg
 | 
			
		||||
  if (!('radius' in operation.labeledArgs) || !operation.labeledArgs.radius)
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const radius = await stringToKclExpression(
 | 
			
		||||
    codeManager.code.slice(
 | 
			
		||||
      operation.labeledArgs.radius.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.radius.sourceRange[1]
 | 
			
		||||
    ),
 | 
			
		||||
    {}
 | 
			
		||||
  )
 | 
			
		||||
  if (err(radius) || 'errors' in radius) return baseCommand
 | 
			
		||||
 | 
			
		||||
  // axis options string arg
 | 
			
		||||
  if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis)
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const axis = codeManager.code
 | 
			
		||||
    .slice(
 | 
			
		||||
      operation.labeledArgs.axis.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.axis.sourceRange[1]
 | 
			
		||||
    )
 | 
			
		||||
    .replaceAll("'", '') // TODO: fix this crap
 | 
			
		||||
 | 
			
		||||
  // length kcl arg
 | 
			
		||||
  if (!('length' in operation.labeledArgs) || !operation.labeledArgs.length)
 | 
			
		||||
    return baseCommand
 | 
			
		||||
  const length = await stringToKclExpression(
 | 
			
		||||
    codeManager.code.slice(
 | 
			
		||||
      operation.labeledArgs.length.sourceRange[0],
 | 
			
		||||
      operation.labeledArgs.length.sourceRange[1]
 | 
			
		||||
    ),
 | 
			
		||||
    {}
 | 
			
		||||
  )
 | 
			
		||||
  if (err(length) || 'errors' in length) return baseCommand
 | 
			
		||||
 | 
			
		||||
  // Assemble the default argument values for the Offset Plane command,
 | 
			
		||||
  // with `nodeToEdit` set, which will let the Offset Plane actor know
 | 
			
		||||
  // to edit the node that corresponds to the StdLibCall.
 | 
			
		||||
  const argDefaultValues: ModelingCommandSchema['Helix'] = {
 | 
			
		||||
    revolutions,
 | 
			
		||||
    angleStart,
 | 
			
		||||
    counterClockWise,
 | 
			
		||||
    radius,
 | 
			
		||||
    axis,
 | 
			
		||||
    length,
 | 
			
		||||
    nodeToEdit: getNodePathFromSourceRange(
 | 
			
		||||
      kclManager.ast,
 | 
			
		||||
      sourceRangeFromRust(operation.sourceRange)
 | 
			
		||||
    ),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...baseCommand,
 | 
			
		||||
    argDefaultValues,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A map of standard library calls to their corresponding information
 | 
			
		||||
 * for use in the feature tree UI.
 | 
			
		||||
@ -214,6 +322,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
 | 
			
		||||
  helix: {
 | 
			
		||||
    label: 'Helix',
 | 
			
		||||
    icon: 'helix',
 | 
			
		||||
    prepareToEdit: prepareToEditHelix,
 | 
			
		||||
  },
 | 
			
		||||
  hole: {
 | 
			
		||||
    label: 'Hole',
 | 
			
		||||
 | 
			
		||||
@ -1801,8 +1801,32 @@ export const modelingMachine = setup({
 | 
			
		||||
          radius,
 | 
			
		||||
          axis,
 | 
			
		||||
          length,
 | 
			
		||||
          nodeToEdit,
 | 
			
		||||
        } = input
 | 
			
		||||
 | 
			
		||||
        let opInsertIndex: number | undefined = undefined
 | 
			
		||||
        let opVariableName: string | undefined = undefined
 | 
			
		||||
 | 
			
		||||
        // If this is an edit flow, first we're going to remove the old one
 | 
			
		||||
        if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
 | 
			
		||||
          // Extract the old name from the node to edit
 | 
			
		||||
          const oldNode = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
            ast,
 | 
			
		||||
            nodeToEdit,
 | 
			
		||||
            'VariableDeclaration'
 | 
			
		||||
          )
 | 
			
		||||
          if (err(oldNode)) {
 | 
			
		||||
            console.error('Error extracting plane name')
 | 
			
		||||
          } else {
 | 
			
		||||
            opVariableName = oldNode.node.declaration.id.name
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const newBody = [...ast.body]
 | 
			
		||||
          newBody.splice(nodeToEdit[1][0], 1)
 | 
			
		||||
          ast.body = newBody
 | 
			
		||||
          opInsertIndex = nodeToEdit[1][0]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const variable of [revolutions, angleStart, radius, length]) {
 | 
			
		||||
          // Insert the variable if it exists
 | 
			
		||||
          if (
 | 
			
		||||
@ -1833,6 +1857,8 @@ export const modelingMachine = setup({
 | 
			
		||||
          radius: valueOrVariable(radius),
 | 
			
		||||
          axis,
 | 
			
		||||
          length: valueOrVariable(length),
 | 
			
		||||
          insertIndex: opInsertIndex,
 | 
			
		||||
          variableName: opVariableName,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const updateAstResult = await kclManager.updateAst(
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user