Add ability to create named constant without code (#5840)

* Add support for forcing kcl input create variable

* Command palette padding tweak

* Make traverse function work for ExpressionStatements

* Add utilities for getting earliest safe index in AST

* Fix the insertIndex logic to not be based on the selection anymore

* Add workflow to create a named constant

* Fix bug with nameEndInDigits matcher

* Tweak command config

* Add a three-dot menu to feature tree pane to create parameters

* Add E2E test for create parameter flow

* Remove edit flow oops

* Fix tsc error

* Fix E2E test

* Update named constant position in edit flow test

* Add tags into consideration for safe insert index

Per @Irev-dev's helpful feedback, with unit tests!
This commit is contained in:
Frank Noirot
2025-03-19 11:58:53 -04:00
committed by GitHub
parent af492d2cb6
commit 533fa749b2
21 changed files with 477 additions and 26 deletions

View File

@ -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`
)
})
})

View File

@ -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')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -114,7 +114,7 @@ export const CommandBar = () => {
leaveTo="opacity-0 scale-95"
>
<WrapperComponent.Panel
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
className="relative z-50 pointer-events-auto w-full max-w-xl pt-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
data-testid="command-bar"
>

View File

@ -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({
</span>
</label>
{createNewVariable ? (
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
<div className="flex mb-2 items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
<label
htmlFor="variable-name"
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
@ -269,7 +270,11 @@ function CommandBarKclInput({
autoFocus
onChange={(e) => setNewVariableName(e.target.value)}
onKeyDown={(e) => {
if (e.currentTarget.value === '' && e.key === 'Backspace') {
if (
e.currentTarget.value === '' &&
e.key === 'Backspace' &&
arg.createVariable !== 'force'
) {
setCreateNewVariable(false)
}
}}
@ -290,15 +295,17 @@ function CommandBarKclInput({
</span>
</div>
) : (
<div className="flex justify-between gap-2 px-4">
<button
onClick={() => setCreateNewVariable(true)}
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
<CustomIcon name="plus" className="w-5 h-5" />
Create new variable
</button>
</div>
arg.createVariable !== 'disallow' && (
<div className="flex justify-between gap-2 px-4">
<button
onClick={() => setCreateNewVariable(true)}
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
<CustomIcon name="plus" className="w-5 h-5" />
Create new variable
</button>
</div>
)
)}
</form>
)

View File

@ -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 (
<Menu>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="relative"
onClick={(e) => {
const target = e.target as HTMLElement
if (e.eventPhase === 3 && target.closest('a') === null) {
e.stopPropagation()
e.preventDefault()
}
}}
>
<Menu.Button className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 ui-open:!border-primary dark:ui-open:!border-chalkboard-70 !outline-none">
<ActionIcon
icon="three-dots"
className="p-1"
size="sm"
bgClassName="bg-transparent dark:bg-transparent"
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
/>
</Menu.Button>
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item>
<button
onClick={() =>
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'modeling',
name: 'event.parameter.create',
},
})
}
className={styles.button}
>
<span>Create parameter</span>
</button>
</Menu.Item>
</Menu.Items>
</div>
</Menu>
)
}

View File

@ -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}
/>
<FeatureTreePane />

View File

@ -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)

View File

@ -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, [

View File

@ -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),
])
})
})

View File

@ -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<Program | Expr>
): Identifier[] {
const identifiers: Identifier[] = []
traverse(program, {
enter(node) {
if (node.type === 'Identifier') {
identifiers.push(node)
}
},
})
return identifiers
}

View File

@ -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)
})
})

View File

@ -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 | Expr>,
program: Node<Program>
) {
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)
}

View File

@ -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),
])
})
})

View File

@ -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<Program | Expr>
): 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
}

View File

@ -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 (

View File

@ -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
| ((

View File

@ -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,

View File

@ -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<Expr | null>(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,

View File

@ -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<ModelingMachineContext, 'sketchDetails'> & {
@ -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',