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:
@ -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`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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 |
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 />
|
||||
|
@ -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)
|
||||
|
@ -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, [
|
||||
|
44
src/lang/queryAst/getIdentifiersInProgram.test.ts
Normal file
44
src/lang/queryAst/getIdentifiersInProgram.test.ts
Normal 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),
|
||||
])
|
||||
})
|
||||
})
|
21
src/lang/queryAst/getIndentifiersInProgram.ts
Normal file
21
src/lang/queryAst/getIndentifiersInProgram.ts
Normal 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
|
||||
}
|
64
src/lang/queryAst/getSafeInsertIndex.test.ts
Normal file
64
src/lang/queryAst/getSafeInsertIndex.test.ts
Normal 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)
|
||||
})
|
||||
})
|
36
src/lang/queryAst/getSafeInsertIndex.ts
Normal file
36
src/lang/queryAst/getSafeInsertIndex.ts
Normal 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)
|
||||
}
|
68
src/lang/queryAst/getTagDeclaratorsInProgram.test.ts
Normal file
68
src/lang/queryAst/getTagDeclaratorsInProgram.test.ts
Normal 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),
|
||||
])
|
||||
})
|
||||
})
|
28
src/lang/queryAst/getTagDeclaratorsInProgram.ts
Normal file
28
src/lang/queryAst/getTagDeclaratorsInProgram.ts
Normal 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
|
||||
}
|
@ -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 (
|
||||
|
@ -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
|
||||
| ((
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
Reference in New Issue
Block a user