Add edge and segment selection in point-and-click Helix flow
Fixes #5393
This commit is contained in:
2
Makefile
2
Makefile
@ -4,7 +4,7 @@ KCL_WASM_LIB_FILES := $(wildcard rust/**/*.rs)
|
|||||||
TS_SRC := $(wildcard src/**/*.tsx) $(wildcard src/**/*.ts)
|
TS_SRC := $(wildcard src/**/*.tsx) $(wildcard src/**/*.ts)
|
||||||
XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
|
XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
|
||||||
|
|
||||||
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
|
dev: node_modules public/kcl_wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
|
||||||
yarn start
|
yarn start
|
||||||
|
|
||||||
# I'm sorry this is so specific to my setup you may as well ignore this.
|
# I'm sorry this is so specific to my setup you may as well ignore this.
|
||||||
|
@ -2,7 +2,7 @@ import { test, expect } from './zoo-test'
|
|||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import { executorInputPath, getUtils } from './test-utils'
|
import { executorInputPath, getUtils } from './test-utils'
|
||||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
import path from 'path'
|
import path, { join } from 'path'
|
||||||
|
|
||||||
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
||||||
test('Extrude from command bar selects extrude line after', async ({
|
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'])
|
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,9 +231,9 @@ test.describe('Feature Tree pane', () => {
|
|||||||
|> circle(center = [0, 0], radius = 5)
|
|> circle(center = [0, 0], radius = 5)
|
||||||
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
||||||
const newConstantName = 'distance001'
|
const newConstantName = 'distance001'
|
||||||
const expectedCode = `sketch001 = startSketchOn('XZ')
|
const expectedCode = `${newConstantName} = 23
|
||||||
|
sketch001 = startSketchOn('XZ')
|
||||||
|> circle(center = [0, 0], radius = 5)
|
|> circle(center = [0, 0], radius = 5)
|
||||||
${newConstantName} = 23
|
|
||||||
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
|
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
|
||||||
|
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
@ -1879,6 +1879,119 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Fillet with large radius should update code even if engine fails`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
// Create a cube with small edges that will cause some fillets to fail
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
profile001 = startProfileAt([0, 0], sketch001)
|
||||||
|
|> yLine(length = -1)
|
||||||
|
|> xLine(length = -10)
|
||||||
|
|> yLine(length = 10)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 5)
|
||||||
|
`
|
||||||
|
const taggedSegment = `yLine(length = -1, tag = $seg01)`
|
||||||
|
const filletExpression = `fillet(radius = 1000, tags = [getNextAdjacentEdge(seg01)])`
|
||||||
|
|
||||||
|
// Locators
|
||||||
|
const edgeLocation = { x: 659, y: 313 }
|
||||||
|
const bodyLocation = { x: 594, y: 313 }
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||||
|
const edgeColorYellow: [number, number, number] = [251, 251, 120] // Mac:B=251,251,90 Ubuntu:240,241,180, Windows:240,241,180
|
||||||
|
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||||
|
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||||
|
const lowTolerance = 20
|
||||||
|
const highTolerance = 70
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
await test.step(`Initial test setup`, async () => {
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// verify modeling scene is loaded
|
||||||
|
await scene.expectPixelColor(backgroundColor, edgeLocation, lowTolerance)
|
||||||
|
|
||||||
|
// wait for stream to load
|
||||||
|
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test
|
||||||
|
await test.step('Select edges and apply oversized fillet', async () => {
|
||||||
|
await test.step(`Select the edge`, async () => {
|
||||||
|
await scene.expectPixelColor(edgeColorWhite, edgeLocation, lowTolerance)
|
||||||
|
const [clickOnTheEdge] = scene.makeMouseHelpers(
|
||||||
|
edgeLocation.x,
|
||||||
|
edgeLocation.y
|
||||||
|
)
|
||||||
|
await clickOnTheEdge()
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
edgeLocation,
|
||||||
|
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Apply fillet`, async () => {
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await toolbar.filletButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'radius',
|
||||||
|
currentArgKey: 'radius',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 sweepEdge',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
// Set a large radius (1000)
|
||||||
|
await cmdBar.currentArgumentInput.locator('.cm-content').fill('1000')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 sweepEdge',
|
||||||
|
Radius: '1000',
|
||||||
|
},
|
||||||
|
stage: 'review',
|
||||||
|
})
|
||||||
|
// Apply fillet with large radius
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Verify code is updated regardless of execution errors', async () => {
|
||||||
|
await editor.expectEditor.toContain(taggedSegment)
|
||||||
|
await editor.expectEditor.toContain(filletExpression)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test(`Chamfer point-and-click`, async ({
|
test(`Chamfer point-and-click`, async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
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: 61 KiB |
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
@ -114,7 +114,7 @@ export const CommandBar = () => {
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<WrapperComponent.Panel
|
<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"
|
as="div"
|
||||||
data-testid="command-bar"
|
data-testid="command-bar"
|
||||||
>
|
>
|
||||||
|
@ -81,7 +81,8 @@ function CommandBarKclInput({
|
|||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||||
arg.createVariableByDefault ||
|
arg.createVariable === 'byDefault' ||
|
||||||
|
arg.createVariable === 'force' ||
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
@ -248,7 +249,7 @@ function CommandBarKclInput({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{createNewVariable ? (
|
{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
|
<label
|
||||||
htmlFor="variable-name"
|
htmlFor="variable-name"
|
||||||
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
||||||
@ -269,7 +270,11 @@ function CommandBarKclInput({
|
|||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => setNewVariableName(e.target.value)}
|
onChange={(e) => setNewVariableName(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.currentTarget.value === '' && e.key === 'Backspace') {
|
if (
|
||||||
|
e.currentTarget.value === '' &&
|
||||||
|
e.key === 'Backspace' &&
|
||||||
|
arg.createVariable !== 'force'
|
||||||
|
) {
|
||||||
setCreateNewVariable(false)
|
setCreateNewVariable(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -290,6 +295,7 @@ function CommandBarKclInput({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
arg.createVariable !== 'disallow' && (
|
||||||
<div className="flex justify-between gap-2 px-4">
|
<div className="flex justify-between gap-2 px-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCreateNewVariable(true)}
|
onClick={() => setCreateNewVariable(true)}
|
||||||
@ -299,6 +305,7 @@ function CommandBarKclInput({
|
|||||||
Create new variable
|
Create new variable
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</form>
|
</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 { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { FeatureTreePane } from './FeatureTreePane'
|
import { FeatureTreePane } from './FeatureTreePane'
|
||||||
import { kclErrorsByFilename } from 'lang/errors'
|
import { kclErrorsByFilename } from 'lang/errors'
|
||||||
|
import { FeatureTreeMenu } from './FeatureTreeMenu'
|
||||||
|
|
||||||
export type SidebarType =
|
export type SidebarType =
|
||||||
| 'code'
|
| 'code'
|
||||||
@ -85,6 +86,7 @@ export const sidebarPanes: SidebarPane[] = [
|
|||||||
id={props.id}
|
id={props.id}
|
||||||
icon="model"
|
icon="model"
|
||||||
title="Feature Tree"
|
title="Feature Tree"
|
||||||
|
Menu={FeatureTreeMenu}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
<FeatureTreePane />
|
<FeatureTreePane />
|
||||||
|
83
src/lang/modelingWorkflows.ts
Normal file
83
src/lang/modelingWorkflows.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Modeling Workflows
|
||||||
|
*
|
||||||
|
* This module contains higher-level CAD operation workflows that
|
||||||
|
* coordinate between different subsystems in the modeling app:
|
||||||
|
* AST, code editor, file system and 3D engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
import { KclManager } from 'lang/KclSingleton'
|
||||||
|
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||||
|
import EditorManager from 'editor/manager'
|
||||||
|
import CodeManager from 'lang/codeManager'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the complete modeling state:
|
||||||
|
* AST, code editor, file, and 3D scene.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Updates the AST and internal state
|
||||||
|
* 2. Updates the code editor and writes to file
|
||||||
|
* 3. Sets focus in the editor if needed
|
||||||
|
* 4. Attempts to execute the model in the 3D engine
|
||||||
|
*
|
||||||
|
* This function follows common CAD application patterns where:
|
||||||
|
*
|
||||||
|
* - The feature tree reflects user's actions
|
||||||
|
* - The engine does its best to visualize what's possible
|
||||||
|
* - Invalid operations appear in feature tree but may not render fully
|
||||||
|
*
|
||||||
|
* This ensures the user can edit the feature tree,
|
||||||
|
* regardless of geometric validity issues.
|
||||||
|
*
|
||||||
|
* @param ast - AST to commit
|
||||||
|
* @param dependencies - Required system components
|
||||||
|
* @param options - Optional parameters for focus, zoom, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function updateModelingState(
|
||||||
|
ast: Node<Program>,
|
||||||
|
dependencies: {
|
||||||
|
kclManager: KclManager
|
||||||
|
editorManager: EditorManager
|
||||||
|
codeManager: CodeManager
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
|
focusPath?: Array<PathToNode>
|
||||||
|
zoomToFit?: boolean
|
||||||
|
zoomOnRangeAndType?: {
|
||||||
|
range: SourceRange
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
// Step 1: Update AST without executing (prepare selections)
|
||||||
|
const updatedAst = await dependencies.kclManager.updateAst(
|
||||||
|
ast,
|
||||||
|
false, // Execution handled separately for error resilience
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 2: Update the code editor and save file
|
||||||
|
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 3: Set focus on the newly added code if needed
|
||||||
|
if (updatedAst.selections) {
|
||||||
|
dependencies.editorManager.selectRange(updatedAst.selections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Try to execute the new code in the engine
|
||||||
|
// and continue regardless of errors
|
||||||
|
try {
|
||||||
|
await dependencies.kclManager.executeAst({
|
||||||
|
ast: updatedAst.newAst,
|
||||||
|
zoomToFit: options?.zoomToFit,
|
||||||
|
zoomOnRangeAndType: options?.zoomOnRangeAndType,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Engine execution error (UI is still updated):', e)
|
||||||
|
}
|
||||||
|
}
|
@ -223,8 +223,8 @@ export function findUniqueName(
|
|||||||
if (!nameIsInString) return name
|
if (!nameIsInString) return name
|
||||||
|
|
||||||
// recursive case: name is not unique and ends in digits
|
// recursive case: name is not unique and ends in digits
|
||||||
const newPad = nameEndsInDigits[1].length
|
const newPad = nameEndsInDigits[0].length
|
||||||
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
const newIndex = parseInt(nameEndsInDigits[0]) + 1
|
||||||
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
||||||
|
|
||||||
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
||||||
|
@ -43,6 +43,7 @@ import { KclManager } from 'lang/KclSingleton'
|
|||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
import EditorManager from 'editor/manager'
|
import EditorManager from 'editor/manager'
|
||||||
import CodeManager from 'lang/codeManager'
|
import CodeManager from 'lang/codeManager'
|
||||||
|
import { updateModelingState } from 'lang/modelingWorkflows'
|
||||||
|
|
||||||
// Edge Treatment Types
|
// Edge Treatment Types
|
||||||
export enum EdgeTreatmentType {
|
export enum EdgeTreatmentType {
|
||||||
@ -83,7 +84,17 @@ export async function applyEdgeTreatmentToSelection(
|
|||||||
const { modifiedAst, pathToEdgeTreatmentNode } = result
|
const { modifiedAst, pathToEdgeTreatmentNode } = result
|
||||||
|
|
||||||
// 2. update ast
|
// 2. update ast
|
||||||
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies)
|
await updateModelingState(
|
||||||
|
modifiedAst,
|
||||||
|
{
|
||||||
|
kclManager: dependencies.kclManager,
|
||||||
|
editorManager: dependencies.editorManager,
|
||||||
|
codeManager: dependencies.codeManager,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
focusPath: pathToEdgeTreatmentNode,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modifyAstWithEdgeTreatmentAndTag(
|
export function modifyAstWithEdgeTreatmentAndTag(
|
||||||
@ -294,33 +305,6 @@ export function getPathToExtrudeForSegmentSelection(
|
|||||||
return { pathToSegmentNode, pathToExtrudeNode }
|
return { pathToSegmentNode, pathToExtrudeNode }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateAstAndFocus(
|
|
||||||
modifiedAst: Node<Program>,
|
|
||||||
pathToEdgeTreatmentNode: Array<PathToNode>,
|
|
||||||
dependencies: {
|
|
||||||
kclManager: KclManager
|
|
||||||
engineCommandManager: EngineCommandManager
|
|
||||||
editorManager: EditorManager
|
|
||||||
codeManager: CodeManager
|
|
||||||
}
|
|
||||||
): Promise<void> {
|
|
||||||
const updatedAst = await dependencies.kclManager.updateAst(
|
|
||||||
modifiedAst,
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
focusPath: pathToEdgeTreatmentNode,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
|
||||||
updatedAst.newAst
|
|
||||||
)
|
|
||||||
|
|
||||||
if (updatedAst?.selections) {
|
|
||||||
dependencies.editorManager.selectRange(updatedAst?.selections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mutateAstWithTagForSketchSegment(
|
export function mutateAstWithTagForSketchSegment(
|
||||||
astClone: Node<Program>,
|
astClone: Node<Program>,
|
||||||
pathToSegmentNode: PathToNode
|
pathToSegmentNode: PathToNode
|
||||||
|
@ -201,6 +201,11 @@ export function traverse(
|
|||||||
])
|
])
|
||||||
} else if (_node.type === 'VariableDeclarator') {
|
} else if (_node.type === 'VariableDeclarator') {
|
||||||
_traverse(_node.init, [...pathToNode, ['init', '']])
|
_traverse(_node.init, [...pathToNode, ['init', '']])
|
||||||
|
} else if (_node.type === 'ExpressionStatement') {
|
||||||
|
_traverse(_node.expression, [
|
||||||
|
...pathToNode,
|
||||||
|
['expression', 'ExpressionStatement'],
|
||||||
|
])
|
||||||
} else if (_node.type === 'PipeExpression') {
|
} else if (_node.type === 'PipeExpression') {
|
||||||
_node.body.forEach((expression, index) =>
|
_node.body.forEach((expression, index) =>
|
||||||
_traverse(expression, [
|
_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
|
||||||
|
}
|
@ -95,6 +95,9 @@ export type ModelingCommandSchema = {
|
|||||||
radius: KclCommandValue
|
radius: KclCommandValue
|
||||||
length: KclCommandValue
|
length: KclCommandValue
|
||||||
}
|
}
|
||||||
|
'event.parameter.create': {
|
||||||
|
value: KclCommandValue
|
||||||
|
}
|
||||||
'change tool': {
|
'change tool': {
|
||||||
tool: SketchTool
|
tool: SketchTool
|
||||||
}
|
}
|
||||||
@ -607,6 +610,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': {
|
'Constrain length': {
|
||||||
description: 'Constrain the length of one or more segments.',
|
description: 'Constrain the length of one or more segments.',
|
||||||
icon: 'dimension',
|
icon: 'dimension',
|
||||||
@ -621,7 +640,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
length: {
|
length: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
required: true,
|
required: true,
|
||||||
createVariableByDefault: true,
|
createVariable: 'byDefault',
|
||||||
defaultValue(_, machineContext) {
|
defaultValue(_, machineContext) {
|
||||||
const selectionRanges = machineContext?.selectionRanges
|
const selectionRanges = machineContext?.selectionRanges
|
||||||
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
||||||
@ -661,7 +680,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
namedValue: {
|
namedValue: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
required: true,
|
required: true,
|
||||||
createVariableByDefault: true,
|
createVariable: 'byDefault',
|
||||||
variableName(commandBarContext, machineContext) {
|
variableName(commandBarContext, machineContext) {
|
||||||
const { currentValue } = commandBarContext.argumentsToSubmit
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
if (
|
if (
|
||||||
|
@ -186,7 +186,7 @@ export type CommandArgumentConfig<
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
createVariableByDefault?: boolean
|
createVariable?: 'byDefault' | 'force' | 'disallow'
|
||||||
variableName?:
|
variableName?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
@ -306,7 +306,7 @@ export type CommandArgument<
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
createVariableByDefault?: boolean
|
createVariable?: 'byDefault' | 'force' | 'disallow'
|
||||||
variableName?:
|
variableName?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
|
@ -201,7 +201,7 @@ export function buildCommandArgument<
|
|||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
createVariableByDefault: arg.createVariableByDefault,
|
createVariable: arg.createVariable,
|
||||||
variableName: arg.variableName,
|
variableName: arg.variableName,
|
||||||
defaultValue: arg.defaultValue,
|
defaultValue: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
|
@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
||||||
import { parse, resultIsOk } from 'lang/wasm'
|
import { parse, resultIsOk } from 'lang/wasm'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { getSafeInsertIndex } from 'lang/queryAst/getSafeInsertIndex'
|
||||||
|
|
||||||
const isValidVariableName = (name: string) =>
|
const isValidVariableName = (name: string) =>
|
||||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||||
@ -48,6 +49,7 @@ export function useCalculateKclExpression({
|
|||||||
insertIndex: 0,
|
insertIndex: 0,
|
||||||
bodyPath: [],
|
bodyPath: [],
|
||||||
})
|
})
|
||||||
|
const [insertIndex, setInsertIndex] = useState(0)
|
||||||
const [valueNode, setValueNode] = useState<Expr | null>(null)
|
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
|
// 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.
|
// 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(() => {
|
useEffect(() => {
|
||||||
const execAstAndSetResult = async () => {
|
const execAstAndSetResult = async () => {
|
||||||
const result = await getCalculatedKclExpressionValue(value)
|
const result = await getCalculatedKclExpressionValue(value)
|
||||||
if (result instanceof Error || 'errors' in result) {
|
if (result instanceof Error || 'errors' in result || !result.astNode) {
|
||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const newInsertIndex = getSafeInsertIndex(result.astNode, kclManager.ast)
|
||||||
|
setInsertIndex(newInsertIndex)
|
||||||
setCalcResult(result?.valueAsString || 'NAN')
|
setCalcResult(result?.valueAsString || 'NAN')
|
||||||
result?.astNode && setValueNode(result.astNode)
|
result?.astNode && setValueNode(result.astNode)
|
||||||
}
|
}
|
||||||
@ -120,7 +124,7 @@ export function useCalculateKclExpression({
|
|||||||
valueNode,
|
valueNode,
|
||||||
calcResult,
|
calcResult,
|
||||||
prevVariables: availableVarInfo.variables,
|
prevVariables: availableVarInfo.variables,
|
||||||
newVariableInsertIndex: availableVarInfo.insertIndex,
|
newVariableInsertIndex: insertIndex,
|
||||||
newVariableName,
|
newVariableName,
|
||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
|
@ -53,6 +53,7 @@ import {
|
|||||||
createIdentifier,
|
createIdentifier,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
|
insertNamedConstant,
|
||||||
loftSketches,
|
loftSketches,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
@ -309,6 +310,10 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Constrain parallel' }
|
| { type: 'Constrain parallel' }
|
||||||
| { type: 'Constrain remove constraints'; data?: PathToNode }
|
| { type: 'Constrain remove constraints'; data?: PathToNode }
|
||||||
| { type: 'Re-execute' }
|
| { type: 'Re-execute' }
|
||||||
|
| {
|
||||||
|
type: 'event.parameter.create'
|
||||||
|
data: ModelingCommandSchema['event.parameter.create']
|
||||||
|
}
|
||||||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||||
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
@ -2313,6 +2318,32 @@ export const modelingMachine = setup({
|
|||||||
if (err(filletResult)) return filletResult
|
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(
|
'set-up-draft-circle': fromPromise(
|
||||||
async (_: {
|
async (_: {
|
||||||
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
input: Pick<ModelingMachineContext, 'sketchDetails'> & {
|
||||||
@ -2561,6 +2592,10 @@ export const modelingMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'event.parameter.create': {
|
||||||
|
target: '#Modeling.parameter.creating',
|
||||||
|
},
|
||||||
|
|
||||||
Export: {
|
Export: {
|
||||||
target: 'Exporting',
|
target: 'Exporting',
|
||||||
guard: 'Has exportable geometry',
|
guard: 'Has exportable geometry',
|
||||||
@ -3861,6 +3896,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': {
|
'Applying Prompt-to-edit': {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'submit-prompt-edit',
|
src: 'submit-prompt-edit',
|
||||||
|
Reference in New Issue
Block a user