diff --git a/e2e/playwright/command-bar-tests.spec.ts b/e2e/playwright/command-bar-tests.spec.ts index 73937182f..4db44bdbe 100644 --- a/e2e/playwright/command-bar-tests.spec.ts +++ b/e2e/playwright/command-bar-tests.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from './zoo-test' import * as fsp from 'fs/promises' import { executorInputPath, getUtils } from './test-utils' import { KCL_DEFAULT_LENGTH } from 'lib/constants' -import path from 'path' +import path, { join } from 'path' test.describe('Command bar tests', { tag: ['@skipWin'] }, () => { test('Extrude from command bar selects extrude line after', async ({ @@ -487,4 +487,53 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => { await toolbar.expectFileTreeState(['main.kcl', 'test.kcl']) }) }) + + test(`Can add a named parameter or constant`, async ({ + page, + homePage, + context, + cmdBar, + scene, + editor, + }) => { + const projectName = 'test' + const beforeKclCode = `a = 5 +b = a * a +c = 3 + a` + await context.folderSetupFn(async (dir) => { + const testProject = join(dir, projectName) + await fsp.mkdir(testProject, { recursive: true }) + await fsp.writeFile(join(testProject, 'main.kcl'), beforeKclCode, 'utf-8') + }) + await homePage.openProject(projectName) + // TODO: you probably shouldn't need an engine connection to add a parameter, + // but you do because all modeling commands have that requirement + await scene.settled(cmdBar) + + await test.step(`Go through the command palette flow`, async () => { + await cmdBar.cmdBarOpenBtn.click() + await cmdBar.chooseCommand('create parameter') + await cmdBar.expectState({ + stage: 'arguments', + commandName: 'Create parameter', + currentArgKey: 'value', + currentArgValue: '5', + headerArguments: { + Value: '', + }, + highlightedHeaderArg: 'value', + }) + await cmdBar.argumentInput.locator('[contenteditable]').fill(`b - 5`) + // TODO: we have no loading indicator for the KCL argument input calculation + await page.waitForTimeout(100) + await cmdBar.progressCmdBar() + await cmdBar.expectState({ + stage: 'commandBarClosed', + }) + }) + + await editor.expectEditor.toContain( + `a = 5b = a * amyParameter001 = b - 5c = 3 + a` + ) + }) }) diff --git a/e2e/playwright/feature-tree-pane.spec.ts b/e2e/playwright/feature-tree-pane.spec.ts index 93afb63fd..c04269508 100644 --- a/e2e/playwright/feature-tree-pane.spec.ts +++ b/e2e/playwright/feature-tree-pane.spec.ts @@ -231,10 +231,10 @@ test.describe('Feature Tree pane', () => { |> circle(center = [0, 0], radius = 5) renamedExtrude = extrude(sketch001, length = ${initialInput})` const newConstantName = 'distance001' - const expectedCode = `sketch001 = startSketchOn('XZ') + const expectedCode = `${newConstantName} = 23 + sketch001 = startSketchOn('XZ') |> circle(center = [0, 0], radius = 5) - ${newConstantName} = 23 - renamedExtrude = extrude(sketch001, length = ${newConstantName})` + renamedExtrude = extrude(sketch001, length = ${newConstantName})` await context.folderSetupFn(async (dir) => { const testDir = join(dir, 'test-sample') diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index 037fc2df2..021c589bf 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index 9dc5837cf..80da594ad 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx index f1a4c9bc8..8788484f1 100644 --- a/src/components/CommandBar/CommandBar.tsx +++ b/src/components/CommandBar/CommandBar.tsx @@ -114,7 +114,7 @@ export const CommandBar = () => { leaveTo="opacity-0 scale-95" > diff --git a/src/components/CommandBar/CommandBarKclInput.tsx b/src/components/CommandBar/CommandBarKclInput.tsx index a4c76fb92..4de89a39a 100644 --- a/src/components/CommandBar/CommandBarKclInput.tsx +++ b/src/components/CommandBar/CommandBarKclInput.tsx @@ -81,7 +81,8 @@ function CommandBarKclInput({ const [value, setValue] = useState(initialValue) const [createNewVariable, setCreateNewVariable] = useState( (previouslySetValue && 'variableName' in previouslySetValue) || - arg.createVariableByDefault || + arg.createVariable === 'byDefault' || + arg.createVariable === 'force' || false ) const [canSubmit, setCanSubmit] = useState(true) @@ -248,7 +249,7 @@ function CommandBarKclInput({ {createNewVariable ? ( -
+
) : ( -
- -
+ arg.createVariable !== 'disallow' && ( +
+ +
+ ) )} ) diff --git a/src/components/ModelingSidebar/ModelingPanes/FeatureTreeMenu.tsx b/src/components/ModelingSidebar/ModelingPanes/FeatureTreeMenu.tsx new file mode 100644 index 000000000..d0cfb1f9e --- /dev/null +++ b/src/components/ModelingSidebar/ModelingPanes/FeatureTreeMenu.tsx @@ -0,0 +1,51 @@ +import { Menu } from '@headlessui/react' +import { PropsWithChildren } from 'react' +import { ActionIcon } from 'components/ActionIcon' +import styles from './KclEditorMenu.module.css' +import { commandBarActor } from 'machines/commandBarMachine' + +export const FeatureTreeMenu = ({ children }: PropsWithChildren) => { + return ( + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + const target = e.target as HTMLElement + if (e.eventPhase === 3 && target.closest('a') === null) { + e.stopPropagation() + e.preventDefault() + } + }} + > + + + + + + + + +
+
+ ) +} diff --git a/src/components/ModelingSidebar/ModelingPanes/index.tsx b/src/components/ModelingSidebar/ModelingPanes/index.tsx index 9ab2bd547..6ec850748 100644 --- a/src/components/ModelingSidebar/ModelingPanes/index.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/index.tsx @@ -19,6 +19,7 @@ import { ContextFrom } from 'xstate' import { settingsMachine } from 'machines/settingsMachine' import { FeatureTreePane } from './FeatureTreePane' import { kclErrorsByFilename } from 'lang/errors' +import { FeatureTreeMenu } from './FeatureTreeMenu' export type SidebarType = | 'code' @@ -85,6 +86,7 @@ export const sidebarPanes: SidebarPane[] = [ id={props.id} icon="model" title="Feature Tree" + Menu={FeatureTreeMenu} onClose={props.onClose} /> diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 0e419ccc2..53818526b 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -223,8 +223,8 @@ export function findUniqueName( if (!nameIsInString) return name // recursive case: name is not unique and ends in digits - const newPad = nameEndsInDigits[1].length - const newIndex = parseInt(nameEndsInDigits[1]) + 1 + const newPad = nameEndsInDigits[0].length + const newIndex = parseInt(nameEndsInDigits[0]) + 1 const nameWithoutDigits = name.replace(endingDigitsMatcher, '') return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex) diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 7567606c0..f5cf85011 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -201,6 +201,11 @@ export function traverse( ]) } else if (_node.type === 'VariableDeclarator') { _traverse(_node.init, [...pathToNode, ['init', '']]) + } else if (_node.type === 'ExpressionStatement') { + _traverse(_node.expression, [ + ...pathToNode, + ['expression', 'ExpressionStatement'], + ]) } else if (_node.type === 'PipeExpression') { _node.body.forEach((expression, index) => _traverse(expression, [ diff --git a/src/lang/queryAst/getIdentifiersInProgram.test.ts b/src/lang/queryAst/getIdentifiersInProgram.test.ts new file mode 100644 index 000000000..fef514d3e --- /dev/null +++ b/src/lang/queryAst/getIdentifiersInProgram.test.ts @@ -0,0 +1,44 @@ +import { assertParse, initPromise } from 'lang/wasm' +import { getIdentifiersInProgram } from './getIndentifiersInProgram' + +function identifier(name: string, start: number, end: number) { + return { + type: 'Identifier', + name, + start, + end, + } +} + +beforeAll(async () => { + await initPromise +}) + +describe(`getIdentifiersInProgram`, () => { + it(`finds no identifiers in an empty program`, () => { + const identifiers = getIdentifiersInProgram(assertParse('')) + expect(identifiers).toEqual([]) + }) + it(`finds a single identifier in an expression`, () => { + const identifiers = getIdentifiersInProgram(assertParse('55 + a')) + expect(identifiers).toEqual([identifier('a', 5, 6)]) + }) + it(`finds multiple identifiers in an expression`, () => { + const identifiers = getIdentifiersInProgram(assertParse('a + b + c')) + expect(identifiers).toEqual([ + identifier('a', 0, 1), + identifier('b', 4, 5), + identifier('c', 8, 9), + ]) + }) + it(`finds all the identifiers in a normal program`, () => { + const program = assertParse(`x = 5 + 2 +y = x * 2 +z = y + 1`) + const identifiers = getIdentifiersInProgram(program) + expect(identifiers).toEqual([ + identifier('x', 14, 15), + identifier('y', 24, 25), + ]) + }) +}) diff --git a/src/lang/queryAst/getIndentifiersInProgram.ts b/src/lang/queryAst/getIndentifiersInProgram.ts new file mode 100644 index 000000000..68f778a83 --- /dev/null +++ b/src/lang/queryAst/getIndentifiersInProgram.ts @@ -0,0 +1,21 @@ +import { traverse } from 'lang/queryAst' +import { Expr, Identifier, Program } from 'lang/wasm' +import { Node } from '@rust/kcl-lib/bindings/Node' + +/** + * Given an AST `Program`, return an array of + * all the `Identifier` nodes within. + */ +export function getIdentifiersInProgram( + program: Node +): Identifier[] { + const identifiers: Identifier[] = [] + traverse(program, { + enter(node) { + if (node.type === 'Identifier') { + identifiers.push(node) + } + }, + }) + return identifiers +} diff --git a/src/lang/queryAst/getSafeInsertIndex.test.ts b/src/lang/queryAst/getSafeInsertIndex.test.ts new file mode 100644 index 000000000..af62d6bd1 --- /dev/null +++ b/src/lang/queryAst/getSafeInsertIndex.test.ts @@ -0,0 +1,64 @@ +import { assertParse, initPromise } from 'lang/wasm' +import { getSafeInsertIndex } from './getSafeInsertIndex' + +beforeAll(async () => { + await initPromise +}) + +describe(`getSafeInsertIndex`, () => { + it(`expression with no identifiers`, () => { + const baseProgram = assertParse(`x = 5 + 2 +y = 2 +z = x + y`) + const targetExpr = assertParse(`5`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) + }) + it(`expression with no identifiers in longer program`, () => { + const baseProgram = assertParse(`x = 5 + 2 + profile001 = startProfileAt([0.07, 0], sketch001) + |> angledLine([0, x], %, $a) + |> angledLine([segAng(a) + 90, 5], %) + |> angledLine([segAng(a), -segLen(a)], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()`) + const targetExpr = assertParse(`5`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(0) + }) + it(`expression with an identifier in the middle`, () => { + const baseProgram = assertParse(`x = 5 + 2 +y = 2 +z = x + y`) + const targetExpr = assertParse(`5 + y`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) + it(`expression with an identifier at the end`, () => { + const baseProgram = assertParse(`x = 5 + 2 +y = 2 +z = x + y`) + const targetExpr = assertParse(`z * z`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(3) + }) + it(`expression with a tag declarator add to end`, () => { + const baseProgram = assertParse(`x = 5 + 2 + profile001 = startProfileAt([0.07, 0], sketch001) + |> angledLine([0, x], %, $a) + |> angledLine([segAng(a) + 90, 5], %) + |> angledLine([segAng(a), -segLen(a)], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()`) + const targetExpr = assertParse(`5 + segAng(a)`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) + it(`expression with a tag declarator and variable in the middle`, () => { + const baseProgram = assertParse(`x = 5 + 2 + profile001 = startProfileAt([0.07, 0], sketch001) + |> angledLine([0, x], %, $a) + |> angledLine([segAng(a) + 90, 5], %) + |> angledLine([segAng(a), -segLen(a)], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() + y = x + x`) + const targetExpr = assertParse(`x + segAng(a)`) + expect(getSafeInsertIndex(targetExpr, baseProgram)).toBe(2) + }) +}) diff --git a/src/lang/queryAst/getSafeInsertIndex.ts b/src/lang/queryAst/getSafeInsertIndex.ts new file mode 100644 index 000000000..6435f7597 --- /dev/null +++ b/src/lang/queryAst/getSafeInsertIndex.ts @@ -0,0 +1,36 @@ +import { getIdentifiersInProgram } from './getIndentifiersInProgram' +import { Program, Expr } from 'lang/wasm' +import { Node } from '@rust/kcl-lib/bindings/Node' +import { getTagDeclaratorsInProgram } from './getTagDeclaratorsInProgram' + +/** + * Given a target expression, return the body index of the last-used variable + * or tag declaration within the provided program. + */ +export function getSafeInsertIndex( + targetExpr: Node, + program: Node +) { + const identifiers = getIdentifiersInProgram(targetExpr) + const safeIdentifierIndex = identifiers.reduce((acc, curr) => { + const bodyIndex = program.body.findIndex( + (a) => + a.type === 'VariableDeclaration' && a.declaration.id?.name === curr.name + ) + return Math.max(acc, bodyIndex + 1) + }, 0) + + const tagDeclarators = getTagDeclaratorsInProgram(program) + console.log('FRANK tagDeclarators', { + identifiers, + tagDeclarators, + targetExpr, + }) + const safeTagIndex = tagDeclarators.reduce((acc, curr) => { + return identifiers.findIndex((a) => a.name === curr.tag.value) === -1 + ? acc + : Math.max(acc, curr.bodyIndex + 1) + }, 0) + + return Math.max(safeIdentifierIndex, safeTagIndex) +} diff --git a/src/lang/queryAst/getTagDeclaratorsInProgram.test.ts b/src/lang/queryAst/getTagDeclaratorsInProgram.test.ts new file mode 100644 index 000000000..8e93f178b --- /dev/null +++ b/src/lang/queryAst/getTagDeclaratorsInProgram.test.ts @@ -0,0 +1,68 @@ +import { assertParse, initPromise } from 'lang/wasm' +import { getTagDeclaratorsInProgram } from './getTagDeclaratorsInProgram' + +function tagDeclaratorWithIndex( + value: string, + start: number, + end: number, + bodyIndex: number +) { + return { + tag: { + type: 'TagDeclarator', + value, + start, + end, + }, + bodyIndex, + } +} + +beforeAll(async () => { + await initPromise +}) + +describe(`getTagDeclaratorsInProgram`, () => { + it(`finds no tag declarators in an empty program`, () => { + const tagDeclarators = getTagDeclaratorsInProgram(assertParse('')) + expect(tagDeclarators).toEqual([]) + }) + it(`finds a single tag declarators in a small program`, () => { + const tagDeclarators = getTagDeclaratorsInProgram( + assertParse(`sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([0, 0], sketch001) + |> angledLine([0, 11], %, $a)`) + ) + expect(tagDeclarators).toEqual([tagDeclaratorWithIndex('a', 107, 109, 1)]) + }) + it(`finds multiple tag declarators in a small program`, () => { + const program = `sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([0.07, 0], sketch001) + |> angledLine([0, 11], %, $a) + |> angledLine([segAng(a) + 90, 11.17], %, $b) + |> angledLine([segAng(a), -segLen(a)], %, $c) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()` + const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program)) + expect(tagDeclarators).toEqual([ + tagDeclaratorWithIndex('a', 110, 112, 1), + tagDeclaratorWithIndex('b', 158, 160, 1), + tagDeclaratorWithIndex('c', 206, 208, 1), + ]) + }) + it(`finds tag declarators at different indices`, () => { + const program = `sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([0.07, 0], sketch001) + |> angledLine([0, 11], %, $a) +profile002 = angledLine([segAng(a) + 90, 11.17], profile001, $b) + |> angledLine([segAng(a), -segLen(a)], %, $c) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close()` + const tagDeclarators = getTagDeclaratorsInProgram(assertParse(program)) + expect(tagDeclarators).toEqual([ + tagDeclaratorWithIndex('a', 110, 112, 1), + tagDeclaratorWithIndex('b', 175, 177, 2), + tagDeclaratorWithIndex('c', 223, 225, 2), + ]) + }) +}) diff --git a/src/lang/queryAst/getTagDeclaratorsInProgram.ts b/src/lang/queryAst/getTagDeclaratorsInProgram.ts new file mode 100644 index 000000000..31e96132e --- /dev/null +++ b/src/lang/queryAst/getTagDeclaratorsInProgram.ts @@ -0,0 +1,28 @@ +import { getBodyIndex, traverse } from 'lang/queryAst' +import { Expr, Program } from 'lang/wasm' +import { Node } from '@rust/kcl-lib/bindings/Node' +import { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator' +import { err } from 'lib/trap' + +type TagWithBodyIndex = { tag: TagDeclarator; bodyIndex: number } + +/** + * Given an AST `Program`, return an array of + * TagDeclarator nodes within, and the body index of their source expression. + */ +export function getTagDeclaratorsInProgram( + program: Node +): TagWithBodyIndex[] { + const tagLocations: TagWithBodyIndex[] = [] + traverse(program, { + enter(node, pathToNode) { + if (node.type === 'TagDeclarator') { + // Get the body index of the declarator's farthest ancestor + const bodyIndex = getBodyIndex(pathToNode) + if (err(bodyIndex)) return + tagLocations.push({ tag: node, bodyIndex }) + } + }, + }) + return tagLocations +} diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index a7e839d11..57833a419 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -92,6 +92,9 @@ export type ModelingCommandSchema = { axis: string length: KclCommandValue } + 'event.parameter.create': { + value: KclCommandValue + } 'change tool': { tool: SketchTool } @@ -582,6 +585,22 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< }, }, }, + 'event.parameter.create': { + displayName: 'Create parameter', + description: 'Add a named constant to use in geometry', + icon: 'make-variable', + status: 'development', + needsReview: false, + args: { + value: { + inputType: 'kcl', + required: true, + createVariable: 'force', + variableName: 'myParameter', + defaultValue: '5', + }, + }, + }, 'Constrain length': { description: 'Constrain the length of one or more segments.', icon: 'dimension', @@ -596,7 +615,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< length: { inputType: 'kcl', required: true, - createVariableByDefault: true, + createVariable: 'byDefault', defaultValue(_, machineContext) { const selectionRanges = machineContext?.selectionRanges if (!selectionRanges) return KCL_DEFAULT_LENGTH @@ -636,7 +655,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< namedValue: { inputType: 'kcl', required: true, - createVariableByDefault: true, + createVariable: 'byDefault', variableName(commandBarContext, machineContext) { const { currentValue } = commandBarContext.argumentsToSubmit if ( diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts index b77dc2a0d..9c42a22d4 100644 --- a/src/lib/commandTypes.ts +++ b/src/lib/commandTypes.ts @@ -186,7 +186,7 @@ export type CommandArgumentConfig< } | { inputType: 'kcl' - createVariableByDefault?: boolean + createVariable?: 'byDefault' | 'force' | 'disallow' variableName?: | string | (( @@ -306,7 +306,7 @@ export type CommandArgument< } | { inputType: 'kcl' - createVariableByDefault?: boolean + createVariable?: 'byDefault' | 'force' | 'disallow' variableName?: | string | (( diff --git a/src/lib/createMachineCommand.ts b/src/lib/createMachineCommand.ts index 2e3fdaa24..788ce0915 100644 --- a/src/lib/createMachineCommand.ts +++ b/src/lib/createMachineCommand.ts @@ -201,7 +201,7 @@ export function buildCommandArgument< } else if (arg.inputType === 'kcl') { return { inputType: arg.inputType, - createVariableByDefault: arg.createVariableByDefault, + createVariable: arg.createVariable, variableName: arg.variableName, defaultValue: arg.defaultValue, ...baseCommandArgument, diff --git a/src/lib/useCalculateKclExpression.ts b/src/lib/useCalculateKclExpression.ts index 826fb8be9..f020be7e1 100644 --- a/src/lib/useCalculateKclExpression.ts +++ b/src/lib/useCalculateKclExpression.ts @@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react' import { getCalculatedKclExpressionValue } from './kclHelpers' import { parse, resultIsOk } from 'lang/wasm' import { err } from 'lib/trap' +import { getSafeInsertIndex } from 'lang/queryAst/getSafeInsertIndex' const isValidVariableName = (name: string) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) @@ -48,6 +49,7 @@ export function useCalculateKclExpression({ insertIndex: 0, bodyPath: [], }) + const [insertIndex, setInsertIndex] = useState(0) const [valueNode, setValueNode] = useState(null) // Gotcha: If we do not attempt to parse numeric literals instantly it means that there is an async action to verify // the value is good. This means all E2E tests have a race condition on when they can hit "next" in the command bar. @@ -101,11 +103,13 @@ export function useCalculateKclExpression({ useEffect(() => { const execAstAndSetResult = async () => { const result = await getCalculatedKclExpressionValue(value) - if (result instanceof Error || 'errors' in result) { + if (result instanceof Error || 'errors' in result || !result.astNode) { setCalcResult('NAN') setValueNode(null) return } + const newInsertIndex = getSafeInsertIndex(result.astNode, kclManager.ast) + setInsertIndex(newInsertIndex) setCalcResult(result?.valueAsString || 'NAN') result?.astNode && setValueNode(result.astNode) } @@ -120,7 +124,7 @@ export function useCalculateKclExpression({ valueNode, calcResult, prevVariables: availableVarInfo.variables, - newVariableInsertIndex: availableVarInfo.insertIndex, + newVariableInsertIndex: insertIndex, newVariableName, isNewVariableNameUnique, setNewVariableName, diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 9c4e4775f..74eec2223 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -50,6 +50,7 @@ import { createIdentifier, createLiteral, extrudeSketch, + insertNamedConstant, loftSketches, } from 'lang/modifyAst' import { @@ -306,6 +307,10 @@ export type ModelingMachineEvent = | { type: 'Constrain parallel' } | { type: 'Constrain remove constraints'; data?: PathToNode } | { type: 'Re-execute' } + | { + type: 'event.parameter.create' + data: ModelingCommandSchema['event.parameter.create'] + } | { type: 'Export'; data: ModelingCommandSchema['Export'] } | { type: 'Make'; data: ModelingCommandSchema['Make'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } @@ -2291,6 +2296,32 @@ export const modelingMachine = setup({ if (err(filletResult)) return filletResult } ), + 'actor.parameter.create': fromPromise( + async ({ + input, + }: { + input: ModelingCommandSchema['event.parameter.create'] | undefined + }) => { + if (!input) return new Error('No input provided') + const { value } = input + if (!('variableName' in value)) { + return new Error('variable name is required') + } + const newAst = insertNamedConstant({ + node: kclManager.ast, + newExpression: value, + }) + const updateAstResult = await kclManager.updateAst(newAst, true) + + await codeManager.updateEditorWithAstAndWriteToFile( + updateAstResult.newAst + ) + + if (updateAstResult?.selections) { + editorManager.selectRange(updateAstResult?.selections) + } + } + ), 'set-up-draft-circle': fromPromise( async (_: { input: Pick & { @@ -2539,6 +2570,10 @@ export const modelingMachine = setup({ reenter: true, }, + 'event.parameter.create': { + target: '#Modeling.parameter.creating', + }, + Export: { target: 'Exporting', guard: 'Has exportable geometry', @@ -3839,6 +3874,24 @@ export const modelingMachine = setup({ }, }, + parameter: { + type: 'parallel', + states: { + creating: { + invoke: { + src: 'actor.parameter.create', + id: 'actor.parameter.create', + input: ({ event }) => { + if (event.type !== 'event.parameter.create') return undefined + return event.data + }, + onDone: ['#Modeling.idle'], + onError: ['#Modeling.idle'], + }, + }, + }, + }, + 'Applying Prompt-to-edit': { invoke: { src: 'submit-prompt-edit',