Add edit flows for extrude and offset plane operations (#5045)
* Start implementing a "prepareToEdit" callback for extrude
* Start of generic edit flow for operations
* Actually invoking command bar send generically on double-click
* Refactor: break out non-React hook helper to calculate Kcl expression value
* Add unit tests, fmt
* Integrate helper to get calculated KclExpression
* Clean up unused imports, simplify use of `programMemoryFromVariables`
* Implement basic extrude editing
* Refactor: move DefaultPlanesStr to its own lib file
* Add support for editing offset planes
* Add Edit right-click menu option
* Turn off edit flow for sketch for now
* Add e2e tests for sketch and offset plane editing, fix bug found with offset plane editing
* Add failing e2e extrude edit test
* Remove action version of extrude AST mod
* Fix behavior when adding a constant while editing operation, fixing e2e test
* Patch in changes from 61b02b5703
* Remove shell's prepareToEdit
* Add other Surface types to `artifactIsPlaneWithPaths`
* refactor: rename `item` to `operation`
* Allow `prepareToEdit` to fail with a toast, signal sketch-on-offset is unimplemented
* Rework sketch e2e test to test several working and failing cases
* Fix tsc errors related to making `codeRef` optional
* Make basic error messages more friendly
* fmt
* Reset modifyAst.ts to main
* Fix broken artifactGraph unit test
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Remove unused import
* Look at this (photo)Graph *in the voice of Nickelback*
* Make the offset plane insert at the end, not one before
* Fix bug caught by e2e test failure with "Command needs review" logic
* Update src/machines/modelingMachine.ts
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
* Remove console logs per @pierremtb
* Update src/components/CommandBar/CommandBarHeader.tsx
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
* Use better programMemory init thanks @jtran
* Fix tsc post merge of #5068
* Fix logic for `artifactIsPlaneWithPaths` post-merge
* Need to disable the sketch-on-face case now that artifactGraph is in Rust. Will active in a future PR (cc @jtran)
* Re-run CI after snapshots
* Update FeatureTreePane to not use `useCommandsContext`, missed during merge
* Fix merge issue, import location change on edited file
* fix click test step, which I believe is waiting for context scripts to load
* Convert toolbarFixture.exeIndicator to getter
We need to convert all these selectors on fixtures to getters, because
they can go stale if called on the fixture constructor.
* Missed a dumb little thing in toolbarFixture.ts
* Fix goof with merge
* fmt
* Another dumb missed thing during merge
I gotta get used to the LazyGit merge tool I'm not good at it yet
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Conver sceneFixture's exeIndicator to a getter
Locators on fixtures will be frozen from the time of the fixture's
initialization, I'm increasingly convinced
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Post-kwargs E2E test cleanup
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
@ -35,6 +35,30 @@ sketch002 = startSketchOn(plane001)
|
||||
extrude001 = extrude(sketch002, length = 10)
|
||||
`
|
||||
|
||||
const FEAUTRE_TREE_SKETCH_CODE = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine([0, 4], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
2
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close(%)
|
||||
extrude001 = extrude(sketch001, length = 10)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentB001)
|
||||
|> circle({
|
||||
center = [-1, 2],
|
||||
radius = .5
|
||||
}, %)
|
||||
plane001 = offsetPlane('XZ', -5)
|
||||
sketch003 = startSketchOn(plane001)
|
||||
|> circle({ center = [0, 0], radius = 5 }, %)
|
||||
`
|
||||
|
||||
test.describe('Feature Tree pane', () => {
|
||||
test(
|
||||
'User can go to definition and go to function definition',
|
||||
@ -124,4 +148,267 @@ test.describe('Feature Tree pane', () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`User can edit sketch (but not on offset plane yet) from the feature tree`,
|
||||
{ tag: '@electron' },
|
||||
async ({ context, homePage, scene, editor, toolbar, page }) => {
|
||||
const unavailableToastMessage = page.getByText(
|
||||
'Editing sketches on faces or offset planes through the feature tree is not yet supported'
|
||||
)
|
||||
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = join(dir, 'test-sample')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.writeFile(
|
||||
join(bracketDir, 'main.kcl'),
|
||||
FEAUTRE_TREE_SKETCH_CODE,
|
||||
'utf-8'
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('setup test', async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'test-sample',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.openProject('test-sample')
|
||||
await scene.waitForExecutionDone()
|
||||
await toolbar.openFeatureTreePane()
|
||||
})
|
||||
|
||||
await test.step('On a default plane should work', async () => {
|
||||
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
|
||||
await expect(
|
||||
toolbar.exitSketchBtn,
|
||||
'We should be in sketch mode now'
|
||||
).toBeVisible()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: ["sketch001 = startSketchOn('XZ')"],
|
||||
})
|
||||
await toolbar.exitSketchBtn.click()
|
||||
})
|
||||
|
||||
await test.step('On an extrude face should *not* work', async () => {
|
||||
// Tooltip is getting in the way of clicking, so I'm first closing the pane
|
||||
await toolbar.closeFeatureTreePane()
|
||||
await (await toolbar.getFeatureTreeOperation('Sketch', 1)).dblclick()
|
||||
await expect(
|
||||
unavailableToastMessage,
|
||||
'We should see a toast message about this'
|
||||
).toBeVisible()
|
||||
await unavailableToastMessage.waitFor({ state: 'detached' })
|
||||
// TODO - turn on once we update the artifactGraph in Rust
|
||||
// to include the proper source location for the extrude face
|
||||
// await expect(
|
||||
// toolbar.exitSketchBtn,
|
||||
// 'We should be in sketch mode now'
|
||||
// ).toBeVisible()
|
||||
// await editor.expectState({
|
||||
// highlightedCode: '',
|
||||
// diagnostics: [],
|
||||
// activeLines: ['|>circle({center=[-1,2],radius=.5},%)'],
|
||||
// })
|
||||
// await toolbar.exitSketchBtn.click()
|
||||
})
|
||||
|
||||
await test.step('On an offset plane should *not* work', async () => {
|
||||
// Tooltip is getting in the way of clicking, so I'm first closing the pane
|
||||
await toolbar.closeFeatureTreePane()
|
||||
await (await toolbar.getFeatureTreeOperation('Sketch', 2)).dblclick()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: ['|>circle({center=[0,0],radius=5},%)'],
|
||||
})
|
||||
await expect(
|
||||
toolbar.exitSketchBtn,
|
||||
'We should not be in sketch mode now'
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Editing sketches on faces or offset planes through the feature tree is not yet supported'
|
||||
),
|
||||
'We should see a toast message about this'
|
||||
).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
test(`User can edit an extrude operation from the feature tree`, async ({
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
page,
|
||||
}) => {
|
||||
const initialInput = '23'
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 5 }, %)
|
||||
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
||||
const newConstantName = 'distance001'
|
||||
const expectedCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 5 }, %)
|
||||
${newConstantName} = 23
|
||||
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
|
||||
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const testDir = join(dir, 'test-sample')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.writeFile(join(testDir, 'main.kcl'), initialCode, 'utf-8')
|
||||
})
|
||||
|
||||
await test.step('setup test', async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'test-sample',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.openProject('test-sample')
|
||||
await scene.waitForExecutionDone()
|
||||
await toolbar.openFeatureTreePane()
|
||||
})
|
||||
|
||||
await test.step('Double click on the extrude operation', async () => {
|
||||
await (await toolbar.getFeatureTreeOperation('Extrude', 0))
|
||||
.first()
|
||||
.dblclick()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [
|
||||
`renamedExtrude = extrude(sketch001, length = ${initialInput})`,
|
||||
],
|
||||
})
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Extrude',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'distance',
|
||||
currentArgValue: initialInput,
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Distance: initialInput,
|
||||
},
|
||||
highlightedHeaderArg: 'distance',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Add a named constant for distance argument and submit', async () => {
|
||||
await expect(cmdBar.currentArgumentInput).toBeVisible()
|
||||
const addVariableButton = page.getByRole('button', {
|
||||
name: 'Create new variable',
|
||||
})
|
||||
await addVariableButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
// The calculated value is shown in the argument summary
|
||||
Distance: initialInput,
|
||||
},
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [
|
||||
`renamedExtrude = extrude(sketch001, length = ${newConstantName})`,
|
||||
],
|
||||
})
|
||||
await editor.expectEditor.toContain(expectedCode, {
|
||||
shouldNormalise: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
test(`User can edit an offset plane operation from the feature tree`, async ({
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const testCode = (value: string) => `p = offsetPlane('XY', ${value})`
|
||||
const initialInput = '10'
|
||||
const initialCode = testCode(initialInput)
|
||||
const newInput = '5 + 10'
|
||||
const expectedCode = testCode(newInput)
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const testDir = join(dir, 'test-sample')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.writeFile(join(testDir, 'main.kcl'), initialCode, 'utf-8')
|
||||
})
|
||||
|
||||
await test.step('setup test', async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'test-sample',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.openProject('test-sample')
|
||||
await scene.waitForExecutionDone()
|
||||
await toolbar.openFeatureTreePane()
|
||||
})
|
||||
|
||||
await test.step('Double click on the offset plane operation', async () => {
|
||||
await (await toolbar.getFeatureTreeOperation('Offset Plane', 0))
|
||||
.first()
|
||||
.dblclick()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [initialCode],
|
||||
})
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Offset plane',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'distance',
|
||||
currentArgValue: initialInput,
|
||||
headerArguments: {
|
||||
Plane: '1 plane',
|
||||
Distance: initialInput,
|
||||
},
|
||||
highlightedHeaderArg: 'distance',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Edit the distance argument and submit', async () => {
|
||||
await expect(cmdBar.currentArgumentInput).toBeVisible()
|
||||
await cmdBar.currentArgumentInput.locator('.cm-content').fill(newInput)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Plane: '1 plane',
|
||||
// We show the calculated value in the argument summary
|
||||
Distance: '15',
|
||||
},
|
||||
commandName: 'Offset plane',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [expectedCode],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -26,11 +26,18 @@ type CmdBarSerialised =
|
||||
export class CmdBarFixture {
|
||||
public page: Page
|
||||
cmdBarOpenBtn!: Locator
|
||||
cmdBarElement!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button')
|
||||
this.cmdBarElement = page.getByTestId('command-bar')
|
||||
}
|
||||
|
||||
get currentArgumentInput() {
|
||||
return this.page.getByTestId('cmd-bar-arg-value')
|
||||
}
|
||||
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
}
|
||||
|
@ -38,7 +38,10 @@ export class SceneFixture {
|
||||
public page: Page
|
||||
public streamWrapper!: Locator
|
||||
public loadingIndicator!: Locator
|
||||
private exeIndicator!: Locator
|
||||
|
||||
get exeIndicator() {
|
||||
return this.page.getByTestId('model-state-indicator-execution-done')
|
||||
}
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -64,7 +67,6 @@ export class SceneFixture {
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
this.streamWrapper = page.getByTestId('stream')
|
||||
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ export class ToolbarFixture {
|
||||
createFileBtn!: Locator
|
||||
fileCreateToast!: Locator
|
||||
filePane!: Locator
|
||||
exeIndicator!: Locator
|
||||
treeInputField!: Locator
|
||||
/** The sidebar button for the Feature Tree pane */
|
||||
featureTreeId = 'feature-tree' as const
|
||||
@ -62,15 +61,16 @@ export class ToolbarFixture {
|
||||
this.filePane = page.locator('#files-pane')
|
||||
this.featureTreePane = page.locator('#feature-tree-pane')
|
||||
this.fileCreateToast = page.getByText('Successfully created')
|
||||
this.exeIndicator = page.getByTestId(
|
||||
'model-state-indicator-receive-reliable'
|
||||
)
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
return this.page.getByTestId('app-logo')
|
||||
}
|
||||
|
||||
get exeIndicator() {
|
||||
return this.page.getByTestId('model-state-indicator-receive-reliable')
|
||||
}
|
||||
|
||||
startSketchPlaneSelection = async () =>
|
||||
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
|
||||
|
||||
@ -139,7 +139,8 @@ export class ToolbarFixture {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific operation button from the Feature Tree pane
|
||||
* Get a specific operation button from the Feature Tree pane.
|
||||
* Index is 0-based.
|
||||
*/
|
||||
async getFeatureTreeOperation(operationName: string, operationIndex: number) {
|
||||
await this.openFeatureTreePane()
|
||||
|
@ -15,6 +15,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
const selectionsSnippets = {
|
||||
@ -75,6 +76,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Binary file not shown.
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
@ -1,212 +1 @@
|
||||
[
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "80/20 Rail",
|
||||
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A Parametric Bearing Pillow Block",
|
||||
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Ball Bearing",
|
||||
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Shelf Bracket",
|
||||
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Car Wheel Assembly",
|
||||
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hollow Dodecahedron",
|
||||
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Enclosure",
|
||||
"description": "An enclosure body and sealing lid for storing items"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange with XY coordinates",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
|
||||
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Food Service Spatula",
|
||||
"description": "Use these spatulas for mixing, flipping, and scraping."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "French Press",
|
||||
"description": "A french press immersion coffee maker"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Spur Gear",
|
||||
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "100mm Gear Rack",
|
||||
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hex nut",
|
||||
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "I-beam",
|
||||
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Kitt",
|
||||
"description": "The beloved KittyCAD mascot in a voxelized style."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Lego Brick",
|
||||
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Mounting Plate",
|
||||
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Robot Arm",
|
||||
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe and Flange Assembly",
|
||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe with bend",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Poopy Shoe",
|
||||
"description": "poop shute for bambu labs printer - optimized for printing."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a cross bar",
|
||||
"description": "A guide for routing a notch into a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a slate",
|
||||
"description": "A guide for routing a slate for a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Sheet Metal Bracket",
|
||||
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Socket Head Cap Screw",
|
||||
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Walkie Talkie",
|
||||
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Washer",
|
||||
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
|
||||
}
|
||||
]
|
||||
404: Not Found
|
@ -2392,8 +2392,6 @@ export class SceneEntities {
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
||||
// calculations/pure-functions/easy to test so no excuse not to
|
||||
|
||||
function prepareTruncatedMemoryAndAst(
|
||||
|
@ -156,6 +156,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
)}
|
||||
{arg.inputType === 'kcl' &&
|
||||
!!argValue &&
|
||||
typeof argValue === 'object' &&
|
||||
'variableName' in (argValue as KclCommandValue) && (
|
||||
<>
|
||||
<CustomIcon
|
||||
|
@ -11,8 +11,14 @@ import {
|
||||
filterOperations,
|
||||
getOperationIcon,
|
||||
getOperationLabel,
|
||||
stdLibMap,
|
||||
} from 'lib/operations'
|
||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
} from 'lib/singletons'
|
||||
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { Actor, Prop } from 'xstate'
|
||||
@ -60,6 +66,7 @@ export const FeatureTreePane = () => {
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
: null
|
||||
|
||||
if (!artifact || !('codeRef' in artifact)) {
|
||||
modelingSend({
|
||||
type: 'Set selection',
|
||||
@ -311,14 +318,12 @@ const OperationItem = (props: {
|
||||
* TODO: https://github.com/KittyCAD/modeling-app/issues/4442
|
||||
*/
|
||||
function enterEditFlow() {
|
||||
if (
|
||||
props.item.type === 'StdLibCall' &&
|
||||
props.item.name === 'startSketchOn'
|
||||
) {
|
||||
if (props.item.type === 'StdLibCall') {
|
||||
props.send({
|
||||
type: 'enterEditFlow',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||
currentOperation: props.item,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -364,6 +369,14 @@ const OperationItem = (props: {
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(props.item.type === 'StdLibCall' &&
|
||||
stdLibMap[props.item.name]?.prepareToEdit
|
||||
? [
|
||||
<ContextMenuItem onClick={enterEditFlow}>
|
||||
Edit {name}
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[props.item, props.send]
|
||||
)
|
||||
|
@ -16,7 +16,8 @@ import {
|
||||
SegmentArtifact,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { CallExpression, CallExpressionKw, defaultSourceRange } from 'lang/wasm'
|
||||
|
@ -456,7 +456,7 @@ export class KclManager {
|
||||
// problem this solves, but either way we should strive to remove it.
|
||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||
([commandId, artifact]) => {
|
||||
if (!('codeRef' in artifact)) return
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return
|
||||
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
||||
this.ast,
|
||||
artifact.codeRef.pathToNode,
|
||||
|
@ -47,7 +47,7 @@ import {
|
||||
removeSingleConstraint,
|
||||
transformAstSketchLines,
|
||||
} from './std/sketchcombos'
|
||||
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { SimplifiedArgDetails } from './std/stdTypes'
|
||||
@ -284,12 +284,19 @@ export function mutateObjExpProp(
|
||||
return false
|
||||
}
|
||||
|
||||
export function extrudeSketch(
|
||||
node: Node<Program>,
|
||||
pathToNode: PathToNode,
|
||||
export function extrudeSketch({
|
||||
node,
|
||||
pathToNode,
|
||||
shouldPipe = false,
|
||||
distance: Expr = createLiteral(4)
|
||||
):
|
||||
distance = createLiteral(4),
|
||||
extrudeName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
pathToNode: PathToNode
|
||||
shouldPipe?: boolean
|
||||
distance: Expr
|
||||
extrudeName?: string
|
||||
}):
|
||||
| {
|
||||
modifiedAst: Node<Program>
|
||||
pathToNode: PathToNode
|
||||
@ -357,7 +364,8 @@ export function extrudeSketch(
|
||||
|
||||
// We're not creating a pipe expression,
|
||||
// but rather a separate constant for the extrusion
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
||||
const name =
|
||||
extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
|
||||
const sketchIndexInPathToNode =
|
||||
@ -629,14 +637,19 @@ export function sketchOnExtrudedFace(
|
||||
export function addOffsetPlane({
|
||||
node,
|
||||
defaultPlane,
|
||||
insertIndex,
|
||||
offset,
|
||||
planeName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
defaultPlane: DefaultPlaneStr
|
||||
insertIndex?: number
|
||||
offset: Expr
|
||||
planeName?: string
|
||||
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
const newPlaneName = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
|
||||
const newPlaneName =
|
||||
planeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
|
||||
|
||||
const newPlane = createVariableDeclaration(
|
||||
newPlaneName,
|
||||
@ -646,10 +659,19 @@ export function addOffsetPlane({
|
||||
])
|
||||
)
|
||||
|
||||
modifiedAst.body.push(newPlane)
|
||||
const insertAt =
|
||||
insertIndex !== undefined
|
||||
? insertIndex
|
||||
: modifiedAst.body.length
|
||||
? modifiedAst.body.length
|
||||
: 0
|
||||
|
||||
modifiedAst.body.length
|
||||
? modifiedAst.body.splice(insertAt, 0, newPlane)
|
||||
: modifiedAst.body.push(newPlane)
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
[insertAt, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
|
@ -293,7 +293,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||
const selection: Selections = {
|
||||
graphSelections: segmentRanges.map((segmentRange) => {
|
||||
const maybeArtifact = [...artifactGraph].find(([, a]) => {
|
||||
if (!('codeRef' in a)) return false
|
||||
if (!('codeRef' in a && a.codeRef)) return false
|
||||
return isOverlap(a.codeRef.range, segmentRange)
|
||||
})
|
||||
return {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { err } from 'lib/trap'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
|
||||
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 568 KiB After Width: | Height: | Size: 569 KiB |
@ -26,9 +26,11 @@ import {
|
||||
MAKE_TOAST_MESSAGES,
|
||||
} from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { defaultPlaneStrToKey } from 'lib/planes'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
@ -2170,6 +2172,16 @@ export class EngineCommandManager extends EventTarget {
|
||||
)
|
||||
}
|
||||
|
||||
getDefaultPlaneId(name: DefaultPlaneStr): string | Error {
|
||||
const key = defaultPlaneStrToKey(name)
|
||||
if (!this.defaultPlanes) {
|
||||
return new Error('Default planes not initialized')
|
||||
} else if (err(key)) {
|
||||
return key
|
||||
}
|
||||
return this.defaultPlanes[key]
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean) {
|
||||
if (this.engineConnection === undefined) return
|
||||
|
||||
@ -2228,7 +2240,11 @@ export class EngineCommandManager extends EventTarget {
|
||||
commandTypeToTarget: string
|
||||
): string | undefined {
|
||||
for (const [artifactId, artifact] of this.artifactGraph) {
|
||||
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
|
||||
if (
|
||||
'codeRef' in artifact &&
|
||||
artifact.codeRef &&
|
||||
isOverlap(range, artifact.codeRef.range)
|
||||
) {
|
||||
if (commandTypeToTarget === artifact.type) return artifactId
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ export type ModelingCommandSchema = {
|
||||
machine: components['schemas']['MachineInfoResponse']
|
||||
}
|
||||
Extrude: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
distance: KclCommandValue
|
||||
@ -69,6 +71,8 @@ export type ModelingCommandSchema = {
|
||||
length: KclCommandValue
|
||||
}
|
||||
'Offset plane': {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
plane: Selections
|
||||
distance: KclCommandValue
|
||||
}
|
||||
@ -279,6 +283,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
icon: 'extrude',
|
||||
needsReview: true,
|
||||
args: {
|
||||
nodeToEdit: {
|
||||
description:
|
||||
'Path to the node in the AST to edit. Never shown to the user.',
|
||||
skip: true,
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
},
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
@ -415,6 +426,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
description: 'Offset a plane.',
|
||||
icon: 'plane',
|
||||
args: {
|
||||
nodeToEdit: {
|
||||
description:
|
||||
'Path to the node in the AST to edit. Never shown to the user.',
|
||||
skip: true,
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
},
|
||||
plane: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['plane'],
|
||||
|
69
src/lib/kclHelpers.test.ts
Normal file
69
src/lib/kclHelpers.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { ParseResult, ProgramMemory } from 'lang/wasm'
|
||||
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
||||
|
||||
describe('KCL expression calculations', () => {
|
||||
it('calculates a simple expression', async () => {
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + 2',
|
||||
programMemory: ProgramMemory.empty(),
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual).not.toHaveProperty('errors')
|
||||
expect(coercedActual.valueAsString).toEqual('3')
|
||||
expect(coercedActual?.astNode).toBeDefined()
|
||||
})
|
||||
it('calculates a simple expression with a variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('x', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('3')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('returns NAN for an invalid expression', async () => {
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory: ProgramMemory.empty(),
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('NAN')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('returns NAN for an expression with an invalid variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('y', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('NAN')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('calculates a more complex expression with a variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('x', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '(1 + x * x) * 2',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('10')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
})
|
98
src/lib/kclHelpers.ts
Normal file
98
src/lib/kclHelpers.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { err } from './trap'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { parse, ProgramMemory, programMemoryInit, resultIsOk } from 'lang/wasm'
|
||||
import { PrevVariable } from 'lang/queryAst'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import { KclExpression } from './commandTypes'
|
||||
|
||||
const DUMMY_VARIABLE_NAME = '__result__'
|
||||
|
||||
export function programMemoryFromVariables(
|
||||
variables: PrevVariable<string | number>[]
|
||||
): ProgramMemory | Error {
|
||||
const memory = programMemoryInit()
|
||||
if (err(memory)) return memory
|
||||
for (const { key, value } of variables) {
|
||||
const error = memory.set(
|
||||
key,
|
||||
typeof value === 'number'
|
||||
? {
|
||||
type: 'Number',
|
||||
value,
|
||||
__meta: [],
|
||||
}
|
||||
: {
|
||||
type: 'String',
|
||||
value,
|
||||
__meta: [],
|
||||
}
|
||||
)
|
||||
if (err(error)) return error
|
||||
}
|
||||
return memory
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the value of the KCL expression,
|
||||
* given the value and the variables that are available
|
||||
*/
|
||||
export async function getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
}: {
|
||||
value: string
|
||||
programMemory: ProgramMemory
|
||||
}) {
|
||||
// Create a one-line program that assigns the value to a variable
|
||||
const dummyProgramCode = `const ${DUMMY_VARIABLE_NAME} = ${value}`
|
||||
const pResult = parse(dummyProgramCode)
|
||||
if (err(pResult) || !resultIsOk(pResult)) return pResult
|
||||
const ast = pResult.program
|
||||
|
||||
// Execute the program without hitting the engine
|
||||
const { execState } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
programMemoryOverride: programMemory,
|
||||
})
|
||||
|
||||
// Find the variable declaration for the result
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declaration.id?.name === DUMMY_VARIABLE_NAME
|
||||
)
|
||||
const variableDeclaratorAstNode =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declaration.init
|
||||
const resultRawValue = execState.memory?.get(DUMMY_VARIABLE_NAME)?.value
|
||||
|
||||
return {
|
||||
astNode: variableDeclaratorAstNode,
|
||||
valueAsString:
|
||||
typeof resultRawValue === 'number' ? String(resultRawValue) : 'NAN',
|
||||
}
|
||||
}
|
||||
|
||||
export async function stringToKclExpression({
|
||||
value,
|
||||
programMemory,
|
||||
}: {
|
||||
value: string
|
||||
programMemory: ProgramMemory
|
||||
}) {
|
||||
const calculatedResult = await getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
})
|
||||
if (err(calculatedResult) || 'errors' in calculatedResult) {
|
||||
return calculatedResult
|
||||
} else if (!calculatedResult.astNode) {
|
||||
return new Error('Failed to calculate KCL expression')
|
||||
}
|
||||
return {
|
||||
valueAst: calculatedResult.astNode,
|
||||
valueCalculated: calculatedResult.valueAsString,
|
||||
valueText: value,
|
||||
} satisfies KclExpression
|
||||
}
|
@ -1,19 +1,212 @@
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { Artifact, getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { codeManager, engineCommandManager, kclManager } from './singletons'
|
||||
import { err } from './trap'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { sourceRangeFromRust } from 'lang/wasm'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { stringToKclExpression } from './kclHelpers'
|
||||
import { ModelingCommandSchema } from './commandBarConfigs/modelingCommandConfig'
|
||||
import { isDefaultPlaneStr } from './planes'
|
||||
import { Selections } from './selections'
|
||||
|
||||
type ExecuteCommandEvent = CommandBarMachineEvent & {
|
||||
type: 'Find and select command'
|
||||
}
|
||||
type ExecuteCommandEventPayload = ExecuteCommandEvent['data']
|
||||
type PrepareToEditFailurePayload = { reason: string }
|
||||
type PrepareToEditCallback = (
|
||||
props: Omit<EnterEditFlowProps, 'commandBarSend'>
|
||||
) =>
|
||||
| ExecuteCommandEventPayload
|
||||
| Promise<ExecuteCommandEventPayload | PrepareToEditFailurePayload>
|
||||
|
||||
interface StdLibCallInfo {
|
||||
label: string
|
||||
icon: CustomIconName
|
||||
/**
|
||||
* There are operations which are honored by the feature tree
|
||||
* that do not yet have a corresponding modeling command.
|
||||
*/
|
||||
prepareToEdit?:
|
||||
| ExecuteCommandEventPayload
|
||||
| PrepareToEditCallback
|
||||
| PrepareToEditFailurePayload
|
||||
}
|
||||
|
||||
const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
/**
|
||||
* Gather up the argument values for the Extrude command
|
||||
* to be used in the command bar edit flow.
|
||||
*/
|
||||
const prepareToEditExtrude: PrepareToEditCallback =
|
||||
async function prepareToEditExtrude({ operation, artifact }) {
|
||||
const baseCommand = {
|
||||
name: 'Extrude',
|
||||
groupId: 'modeling',
|
||||
}
|
||||
if (
|
||||
!artifact ||
|
||||
!('pathId' in artifact) ||
|
||||
operation.type !== 'StdLibCall'
|
||||
) {
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
// We have to go a little roundabout to get from the original artifact
|
||||
// to the solid2DId that we need to pass to the Extrude command.
|
||||
const pathArtifact = getArtifactOfTypes(
|
||||
{
|
||||
key: artifact.pathId,
|
||||
types: ['path'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (
|
||||
err(pathArtifact) ||
|
||||
pathArtifact.type !== 'path' ||
|
||||
!pathArtifact.solid2dId
|
||||
)
|
||||
return baseCommand
|
||||
const solid2DArtifact = getArtifactOfTypes(
|
||||
{
|
||||
key: pathArtifact.solid2dId,
|
||||
types: ['solid2d'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') {
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
// Convert the length argument from a string to a KCL expression
|
||||
const distanceResult = await stringToKclExpression({
|
||||
value: codeManager.code.slice(
|
||||
operation.labeledArgs?.['length']?.sourceRange[0],
|
||||
operation.labeledArgs?.['length']?.sourceRange[1]
|
||||
),
|
||||
programMemory: kclManager.programMemory.clone(),
|
||||
})
|
||||
if (err(distanceResult) || 'errors' in distanceResult) {
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
// Assemble the default argument values for the Extrude command,
|
||||
// with `nodeToEdit` set, which will let the Extrude actor know
|
||||
// to edit the node that corresponds to the StdLibCall.
|
||||
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
|
||||
selection: {
|
||||
graphSelections: [
|
||||
{
|
||||
artifact: solid2DArtifact,
|
||||
codeRef: pathArtifact.codeRef,
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
},
|
||||
distance: distanceResult,
|
||||
nodeToEdit: getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRangeFromRust(operation.sourceRange)
|
||||
),
|
||||
}
|
||||
return {
|
||||
...baseCommand,
|
||||
argDefaultValues,
|
||||
}
|
||||
}
|
||||
|
||||
const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
|
||||
operation,
|
||||
}) => {
|
||||
const baseCommand = {
|
||||
name: 'Offset plane',
|
||||
groupId: 'modeling',
|
||||
}
|
||||
if (
|
||||
operation.type !== 'StdLibCall' ||
|
||||
!operation.labeledArgs ||
|
||||
!('std_plane' in operation.labeledArgs) ||
|
||||
!operation.labeledArgs.std_plane ||
|
||||
!('offset' in operation.labeledArgs) ||
|
||||
!operation.labeledArgs.offset
|
||||
) {
|
||||
return baseCommand
|
||||
}
|
||||
// TODO: Implement conversion to arbitrary plane selection
|
||||
// once the Offset Plane command supports it.
|
||||
const planeName = codeManager.code
|
||||
.slice(
|
||||
operation.labeledArgs.std_plane.sourceRange[0],
|
||||
operation.labeledArgs.std_plane.sourceRange[1]
|
||||
)
|
||||
.replaceAll(`'`, ``)
|
||||
|
||||
if (!isDefaultPlaneStr(planeName)) {
|
||||
// TODO: error handling
|
||||
return baseCommand
|
||||
}
|
||||
const planeId = engineCommandManager.getDefaultPlaneId(planeName)
|
||||
if (err(planeId)) {
|
||||
// TODO: error handling
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
const plane: Selections = {
|
||||
graphSelections: [],
|
||||
otherSelections: [
|
||||
{
|
||||
name: planeName,
|
||||
id: planeId,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Convert the distance argument from a string to a KCL expression
|
||||
const distanceResult = await stringToKclExpression({
|
||||
value: codeManager.code.slice(
|
||||
operation.labeledArgs.offset.sourceRange[0],
|
||||
operation.labeledArgs.offset.sourceRange[1]
|
||||
),
|
||||
programMemory: kclManager.programMemory.clone(),
|
||||
})
|
||||
|
||||
if (err(distanceResult) || 'errors' in distanceResult) {
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
// Assemble the default argument values for the Offset Plane command,
|
||||
// with `nodeToEdit` set, which will let the Offset Plane actor know
|
||||
// to edit the node that corresponds to the StdLibCall.
|
||||
const argDefaultValues: ModelingCommandSchema['Offset plane'] = {
|
||||
distance: distanceResult,
|
||||
plane,
|
||||
nodeToEdit: getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRangeFromRust(operation.sourceRange)
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
...baseCommand,
|
||||
argDefaultValues,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of standard library calls to their corresponding information
|
||||
* for use in the feature tree UI.
|
||||
*/
|
||||
export const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
chamfer: {
|
||||
label: 'Chamfer',
|
||||
icon: 'chamfer3d',
|
||||
// modelingEvent: 'Chamfer',
|
||||
},
|
||||
extrude: {
|
||||
label: 'Extrude',
|
||||
icon: 'extrude',
|
||||
prepareToEdit: prepareToEditExtrude,
|
||||
},
|
||||
fillet: {
|
||||
label: 'Fillet',
|
||||
@ -42,6 +235,7 @@ const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
offsetPlane: {
|
||||
label: 'Offset Plane',
|
||||
icon: 'plane',
|
||||
prepareToEdit: prepareToEditOffsetPlane,
|
||||
},
|
||||
patternCircular2d: {
|
||||
label: 'Circular Pattern',
|
||||
@ -70,6 +264,21 @@ const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
startSketchOn: {
|
||||
label: 'Sketch',
|
||||
icon: 'sketch',
|
||||
// TODO: fix matching sketches-on-faces and offset planes back to their
|
||||
// original plane artifacts in order to edit them.
|
||||
async prepareToEdit({ artifact }) {
|
||||
if (artifact) {
|
||||
return {
|
||||
name: 'Enter sketch',
|
||||
groupId: 'modeling',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
reason:
|
||||
'Editing sketches on faces or offset planes through the feature tree is not yet supported. Please double-click the path in the scene for now.',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
sweep: {
|
||||
label: 'Sweep',
|
||||
@ -182,3 +391,47 @@ function isNotUserFunctionWithNoOperations(
|
||||
function isNotUserFunctionReturn(ops: Operation[]): Operation[] {
|
||||
return ops.filter((op) => op.type !== 'UserDefinedFunctionReturn')
|
||||
}
|
||||
|
||||
export interface EnterEditFlowProps {
|
||||
operation: Operation
|
||||
artifact?: Artifact
|
||||
}
|
||||
|
||||
export async function enterEditFlow({
|
||||
operation,
|
||||
artifact,
|
||||
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
||||
if (operation.type !== 'StdLibCall') {
|
||||
return new Error(
|
||||
'Feature tree editing not yet supported for user-defined functions. Please edit in the code editor.'
|
||||
)
|
||||
}
|
||||
const stdLibInfo = stdLibMap[operation.name]
|
||||
|
||||
if (stdLibInfo && stdLibInfo.prepareToEdit) {
|
||||
if (typeof stdLibInfo.prepareToEdit === 'function') {
|
||||
const eventPayload = await stdLibInfo.prepareToEdit({
|
||||
operation,
|
||||
artifact,
|
||||
})
|
||||
if ('reason' in eventPayload) {
|
||||
return new Error(eventPayload.reason)
|
||||
}
|
||||
return {
|
||||
type: 'Find and select command',
|
||||
data: eventPayload,
|
||||
}
|
||||
} else {
|
||||
return 'reason' in stdLibInfo.prepareToEdit
|
||||
? new Error(stdLibInfo.prepareToEdit.reason)
|
||||
: {
|
||||
type: 'Find and select command',
|
||||
data: stdLibInfo.prepareToEdit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Error(
|
||||
'Feature tree editing not yet supported for this operation. Please edit in the code editor.'
|
||||
)
|
||||
}
|
||||
|
29
src/lib/planes.ts
Normal file
29
src/lib/planes.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
// KCL string representation of default planes
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
||||
export function defaultPlaneStrToKey(
|
||||
plane: DefaultPlaneStr
|
||||
): keyof DefaultPlanes | Error {
|
||||
switch (plane) {
|
||||
case 'XY':
|
||||
return 'xy'
|
||||
case 'XZ':
|
||||
return 'xz'
|
||||
case 'YZ':
|
||||
return 'yz'
|
||||
case '-XY':
|
||||
return 'negXy'
|
||||
case '-XZ':
|
||||
return 'negXz'
|
||||
case '-YZ':
|
||||
return 'negYz'
|
||||
default:
|
||||
return new Error(`Invalid plane string: ${plane}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function isDefaultPlaneStr(plane: string): plane is DefaultPlaneStr {
|
||||
return ['XY', 'XZ', 'YZ', '-XY', '-XZ', '-YZ'].includes(plane)
|
||||
}
|
@ -22,7 +22,6 @@ import { getNodeFromPath, isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { CommandArgument } from './commandTypes'
|
||||
import {
|
||||
DefaultPlaneStr,
|
||||
getParentGroup,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
@ -43,6 +42,7 @@ import {
|
||||
ArtifactId,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { DefaultPlaneStr } from './planes'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -614,7 +614,7 @@ export function codeToIdSelections(
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
|
||||
.map(([id, artifact]) => {
|
||||
if (!('codeRef' in artifact)) return null
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return null
|
||||
return isOverlap(artifact.codeRef.range, selection.range)
|
||||
? {
|
||||
artifact,
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { findUniqueName } from 'lang/modifyAst'
|
||||
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||
import { ProgramMemory, Expr, parse, resultIsOk } from 'lang/wasm'
|
||||
import { Expr } from 'lang/wasm'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import {
|
||||
getCalculatedKclExpressionValue,
|
||||
programMemoryFromVariables,
|
||||
} from './kclHelpers'
|
||||
|
||||
const isValidVariableName = (name: string) =>
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||
@ -86,37 +88,25 @@ export function useCalculateKclExpression({
|
||||
|
||||
useEffect(() => {
|
||||
const execAstAndSetResult = async () => {
|
||||
const _code = `const __result__ = ${value}`
|
||||
const pResult = parse(_code)
|
||||
if (err(pResult) || !resultIsOk(pResult)) return
|
||||
const ast = pResult.program
|
||||
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
type: 'String',
|
||||
value,
|
||||
__meta: [],
|
||||
})
|
||||
if (trap(error, { suppress: true })) return
|
||||
}
|
||||
const { execState } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
// We make sure to send an empty program memory to denote we mean mock mode.
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
})
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declaration.id?.name === '__result__'
|
||||
const programMemory = programMemoryFromVariables(
|
||||
availableVarInfo.variables
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declaration.init
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
if (programMemory instanceof Error) {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
return
|
||||
}
|
||||
const result = await getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
})
|
||||
if (result instanceof Error || 'errors' in result) {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
return
|
||||
}
|
||||
setCalcResult(result?.valueAsString || 'NAN')
|
||||
result?.astNode && setValueNode(result.astNode)
|
||||
}
|
||||
if (!value) return
|
||||
execAstAndSetResult().catch(() => {
|
||||
|
@ -249,7 +249,10 @@ export const commandBarMachine = setup({
|
||||
},
|
||||
guards: {
|
||||
'Command needs review': ({ context }) =>
|
||||
context.selectedCommand?.needsReview || false,
|
||||
context.selectedCommand?.needsReview ||
|
||||
('nodeToEdit' in context.argumentsToSubmit &&
|
||||
context.argumentsToSubmit.nodeToEdit !== undefined) ||
|
||||
false,
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { getArtifactFromRange } from 'lang/std/artifactGraph'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { assign, setup } from 'xstate'
|
||||
import { enterEditFlow, EnterEditFlowProps } from 'lib/operations'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
import { commandBarActor } from './commandBarMachine'
|
||||
|
||||
type FeatureTreeEvent =
|
||||
| {
|
||||
@ -12,27 +19,67 @@ type FeatureTreeEvent =
|
||||
}
|
||||
| {
|
||||
type: 'enterEditFlow'
|
||||
data: { targetSourceRange: SourceRange }
|
||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||
}
|
||||
| { type: 'goToError' }
|
||||
| { type: 'codePaneOpened' }
|
||||
| { type: 'selected' }
|
||||
| { type: 'done' }
|
||||
| { type: 'xstate.error.actor.prepareEditCommand'; error: Error }
|
||||
|
||||
type FeatureTreeContext = {
|
||||
targetSourceRange?: SourceRange
|
||||
currentOperation?: Operation
|
||||
}
|
||||
|
||||
export const featureTreeMachine = setup({
|
||||
types: {
|
||||
context: {} as { targetSourceRange?: SourceRange },
|
||||
input: {} as FeatureTreeContext,
|
||||
context: {} as FeatureTreeContext,
|
||||
events: {} as FeatureTreeEvent,
|
||||
},
|
||||
guards: {
|
||||
codePaneIsOpen: () => false,
|
||||
},
|
||||
actors: {
|
||||
prepareEditCommand: fromPromise(
|
||||
({
|
||||
input,
|
||||
}: {
|
||||
input: EnterEditFlowProps & {
|
||||
commandBarSend: (typeof commandBarActor)['send']
|
||||
}
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { commandBarSend, ...editFlowProps } = input
|
||||
enterEditFlow(editFlowProps)
|
||||
.then((result) => {
|
||||
if (err(result)) {
|
||||
reject(result)
|
||||
return
|
||||
}
|
||||
input.commandBarSend(result)
|
||||
resolve(result)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
saveTargetSourceRange: assign({
|
||||
targetSourceRange: ({ event }) =>
|
||||
'data' in event ? event.data.targetSourceRange : undefined,
|
||||
'data' in event && !err(event.data)
|
||||
? event.data.targetSourceRange
|
||||
: undefined,
|
||||
}),
|
||||
clearTargetSourceRange: assign({
|
||||
saveCurrentOperation: assign({
|
||||
currentOperation: ({ event }) =>
|
||||
'data' in event && 'currentOperation' in event.data
|
||||
? event.data.currentOperation
|
||||
: undefined,
|
||||
}),
|
||||
clearContext: assign({
|
||||
targetSourceRange: undefined,
|
||||
}),
|
||||
sendSelectionEvent: () => {},
|
||||
@ -41,9 +88,10 @@ export const featureTreeMachine = setup({
|
||||
scrollToError: () => {},
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFn7EAbAFYAHAHYAnGoOm9OgMw6tAGhABPRBsvF+a6wYBM-A-0uWepYGAL4h9qiYuAREZJQ0sGBULBgA8qJgOJgysgLCSCDiktJyCsoIKipabmo6-Co2Wip+-DoG9k4IWpp+Ae6W-N16KpZeYRHo2HiEYCTkVNRgshiZAKIQUgBiFHQA7nkKRVI5ZapergN6el46515GVdYdiN4GbjpGXiNGV2pan+MQJEpjFZsR6KRZFBGKwOFwcDxiIlktIodRkWAUpADgUjiV5AVyipWipiGZupYVCY1D5+F5ngg-jpiJ8DFo1OoDA13GNwkDJtEZiQIVCYWxONwSBA5DQcWIJMdSoTVG0yTodGoPAZKfwjP52o5EHVNIYvNd9M0zJZAcDBbERdDmOL4Yi6BlZJCoABhOgQMAABTQshoLF9AaDYHSS2xQkOCvxpy6lWIbS0Bm8JkM+ksDKZLK8bL0-S8NOCNoF01iGJSnqRSUxqKg6PrWIgcsK8ZOyq6aj0avTbU1Hg0KgZzVcrU+Xn+9Q+dPLUUrYOrjeI0uD1HbeK7oCJBj7xkafhMX28agZJeqHz0RhstTUD21WgXIKFxCWKxwnvWWx2uzrKKes2KIxvk8rFDuShGpY1RaKMRiHr4va9gyegPsQQRmiYVK+GYL52mCH6ZN+GwYNsexrjKm6xrinZKruqhaLBmr8PUvj6F4nGoeoxDEpYCFMVSowfPhS7CnQnqMKsOA4HQODEG6Syej6fqBhuoaqRGUbBm2NHgYqBIMV0-EproxLpoE7hWGOnFeGSxgaPwei6EYmaiaCczxLQDB0NJsk4FudGGVBFRpsQwRGGmgx6n4cH0oaFSVH21i6LqD5mtYejuW+DpSTJcmURugUQfRIX6MQfwcpqDxpUETwJSoXxqMQ04PjoSXspYtRhHyshhvABS2mJcYlcF5QALR2Al43TtoVJDD46ZfJS1p8kNHlxFQI0GYmNJjn8c21PuIzjuq2X2hJopOnCkrbQm3ZdXZNhsl1fzOSW1kJdYzKeK5VQqJh+7nWCuXXRKCIkCunp3ZB5RFq46ofOq6geDeo4JZqdlaEWt66l8jXfcD4mSWDLpSjKMOlUSzSwXS2qztVfy5jS2g43UnyVOcZ1rRWG2g7C4Ouu6ylhmpYCU2NiD8Zoz1wZq2NaB9OYYyz2O6gE3TtR8XVEwBDbQ7Ro2JtYT1pnLb2K7UyudIrFU3h8ZoamafhZTzi4bVDUJ6zWUIS7trGmS98vvVb+1GBhjUPtqer3KxOi657UCFeLhs7d2FmHe1ARmFr54NdqLUFrZnLBFUutEV+UI-mRf5+w9NgYZSNyBHB6rxTbcHhTY5wmDVMGrRM7tvhXJG-hRid10ZGjNUEjVWM533t4gaHh8aFgaOc+7XOXyzEVXpHkf+64p-p91T744VBEE7h0oEGpTZ0Dx2Sbhi9r4ozNLroN+XJk8hTBV51SRQGE7JiS9ErJjgnSRWC0bCu0Hq+C6JMf7yUUh6KEKlwzBj-uUKqKYgFQNAYrMcVI+yVFcqxfwFhAhf0uo6FByccHLyaC1JygRp5FkagaTo5CyFUj1KxO+gReRhCAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFn7EAbAFYAHAHYAnGoOm9OgMw6tAGhABPRBsvF+a6wYBM-A-0uWepYGAL4h9qiYuAREZJQ0sGBULBgA8qJgOJgysgLCSCDiktJyCsoIKipabmo6-Co2Wip+-DoG9k4IWpp+Ae6W-N16KpZeYRHo2HiEYCTkVNRgshiZAKIQUgBiFHQA7nkKRVI5ZapergN6el46515GVdYdiN4GbjpGXiNGV2pan+MQJEpjFZnEFvRGKscDg6DgDgUjiV5AVylU3sEjFo-P9fINRs8KpU9MRrLp+CZPkF9IDgdEZiR6KRZFBGKwOFwcDxiIlktIWdReWAUpAEWIJMdSqjVK0VMQzN1LCoTGofPwvIS-jpiJ8DFo1OoDA13GNwkDJvTYkyWWy2JxuCQIHIaGLChLkacKm15TodGoPAYlRT-O1HIg6ppDF5rvpmmZLLSLdMrXRmazmHbOdy6BlZGmAMJ0CBgAAKaFkNBYRdL5bA6SWoqEh3dJ2lXUqxDa2O8JkM+ksmt0Oq8er0-S8quCiaiybBQpSaZ5SWF-KgguXIogrqRrdA5X1JIMbSPZn9lg0KkJzVcrU+XlxDSM6unIIZS75i6dFeo25bUr3qgGIeRiNH4JhfN4aiEhO1QfHoIH6n6DyBloL6WmCSwrDgabrFsOy7O+K5puufKNvk4rFLuSjhv62hBKY1h-PRUFhggwxeJ2Pa6D4hj1KEZp0rOJCYZkOEbBg2x7MQX4uk2iJ-iiAEVFo1T6ho9S+PoXjaYSwxqMQKj+EYepNEYowfGhQnECJ2EsrhEn4cQoh4KIaB4PZhYALaeeWEDUDJZCyAAbnQADWJCCaCwnLKJdniZJBHOWArnueJXk+bIEAIMyIUsNkch5L+lH-tRCAGtqxlaFcNgqGO3YsZ0ZlGDq+ggWO7j+NYllRdZMW2VA9kJU5LluWAHl0N5vmLDCcJORQmDIHCnnEJFb42WJeFSUlKVjWlE0ZVlOV0HlyKFXJFGSoppXlTqo4WE0dUGGYhJGD8OrKhYwwgYx-ETDOPXWum0KwjgxA5ksBbVmW35VsW0N1rmZHNsVV37pYzU2DohlPYE7hWFe2kcU9Jj8Pwei6K9ah6N1b6A1CM2gzJP7nW6KOevoxB-Aa-oPBSVMBDoBPqMQ95qCBxL6ueOhhGasjVvABSrUQyOXZ6AC0disWrfjaFVpPWNc8FfAmAlJj18xgCrHptqqV5-No6htMMSqBr6NMpmmtocg6VtUeU54cTYernkxWgTvjrHWNqYvBOegbaX4v3mv9tOpjaGbe1yJDzquvsleUY6uL6Hy+uoHjwZerH+hxesgRSXwqOcbumynHvp+y9pZ9Jzp56jqjNKp6qBvUfpi38mqqnRFJ1J8lRN9LLevm36Yd1mJDg3mLKFnDta9566OaEHWgh1VYe1AOVeT7XATdFjHznu7c4brn8ls221iB9ix-+qf4cX50YdtA3HOIMEY5MqaP2zs-RcOc0x7xtqTTsX8T7kz-nbZq7ExaBifPcUmC8-pLyfh+Fk3cKzwKUjjB2tQRjBGoX6K8gYRYjkJoaYIVRIG9SwhtByexyGlUsPqYgVxmimBwU+BooYAG3FJMMCk9EqoTiTkrDCfVuFDVgSyPh5QDQcWEWYYw6pxFtF0o3EWGhPh1ANEaCyi90LRS4XFTaBEZJaOcA0Tm55Rj+isdYdwukjxuE+CYLStwY4cPWo4nhiURqpSkOlXyriECBjlIGM8A9DJkw1KxYY1RAh3gCIEbx1NbFWTpnQYGcJEkCNgr6LEAw-T3jDleSomgzI3F0AbLsHCykVNBhvSGO8yGv1VjbYWxc6mtFVCpLJnRKhvTmU+fWFhAjdLTkDBmpDLbDOtkpcmcoeKFPcGORukj+7zOVIs-wyzrgyxCEAA */
|
||||
id: 'featureTree',
|
||||
description: 'Workflows for interacting with the feature tree pane',
|
||||
context: ({ input }) => input,
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
@ -59,7 +107,7 @@ export const featureTreeMachine = setup({
|
||||
|
||||
enterEditFlow: {
|
||||
target: 'enteringEditFlow',
|
||||
actions: 'saveTargetSourceRange',
|
||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||
},
|
||||
|
||||
goToError: 'goingToError',
|
||||
@ -79,7 +127,7 @@ export const featureTreeMachine = setup({
|
||||
},
|
||||
|
||||
done: {
|
||||
entry: ['clearTargetSourceRange'],
|
||||
entry: ['clearContext'],
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
|
||||
@ -107,7 +155,7 @@ export const featureTreeMachine = setup({
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
entry: 'clearTargetSourceRange',
|
||||
entry: 'clearContext',
|
||||
},
|
||||
},
|
||||
|
||||
@ -118,18 +166,54 @@ export const featureTreeMachine = setup({
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
selected: 'done',
|
||||
selected: {
|
||||
target: 'prepareEditCommand',
|
||||
reenter: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
|
||||
prepareEditCommand: {
|
||||
invoke: {
|
||||
src: 'prepareEditCommand',
|
||||
input: ({ context }) => {
|
||||
const artifact = context.targetSourceRange
|
||||
? getArtifactFromRange(
|
||||
context.targetSourceRange,
|
||||
engineCommandManager.artifactGraph
|
||||
) ?? undefined
|
||||
: undefined
|
||||
return {
|
||||
// currentOperation is guaranteed to be defined here
|
||||
operation: context.currentOperation!,
|
||||
artifact,
|
||||
commandBarSend: commandBarActor.send,
|
||||
}
|
||||
},
|
||||
onDone: {
|
||||
target: 'done',
|
||||
reenter: true,
|
||||
},
|
||||
onError: {
|
||||
target: 'done',
|
||||
reenter: true,
|
||||
actions: ({ event }) => {
|
||||
if ('error' in event && err(event.error)) {
|
||||
toast.error(event.error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'selecting',
|
||||
entry: 'sendSelectionEvent',
|
||||
exit: ['clearTargetSourceRange', 'sendEditFlowStart'],
|
||||
exit: ['clearContext'],
|
||||
},
|
||||
|
||||
goingToError: {
|
||||
|
@ -70,7 +70,8 @@ import {
|
||||
} from 'components/Toolbar/SetAbsDistance'
|
||||
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
||||
@ -1452,24 +1453,49 @@ export const modelingMachine = setup({
|
||||
unknown,
|
||||
ModelingCommandSchema['Extrude'] | undefined
|
||||
>(async ({ input }) => {
|
||||
if (!input) return Promise.reject('No input provided')
|
||||
const { selection, distance } = input
|
||||
if (!input) return new Error('No input provided')
|
||||
const { selection, distance, nodeToEdit } = input
|
||||
const isEditing =
|
||||
nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number'
|
||||
let ast = structuredClone(kclManager.ast)
|
||||
let extrudeName: string | undefined = undefined
|
||||
|
||||
// If this is an edit flow, first we're going to remove the old extrusion
|
||||
if (isEditing) {
|
||||
// Extract the plane name from the node to edit
|
||||
const extrudeNameNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
nodeToEdit,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(extrudeNameNode)) {
|
||||
console.error('Error extracting plane name')
|
||||
} else {
|
||||
extrudeName = extrudeNameNode.node.declaration.id.name
|
||||
}
|
||||
|
||||
// Removing the old extrusion statement
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(nodeToEdit[1][0] as number, 1)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
// Add an extrude statement to the AST
|
||||
const extrudeSketchRes = extrudeSketch(
|
||||
ast,
|
||||
const extrudeSketchRes = extrudeSketch({
|
||||
node: ast,
|
||||
pathToNode,
|
||||
false,
|
||||
shouldPipe: false,
|
||||
distance:
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst
|
||||
)
|
||||
if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
|
||||
: distance.valueAst,
|
||||
extrudeName,
|
||||
})
|
||||
if (err(extrudeSketchRes)) return extrudeSketchRes
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
||||
|
||||
// Insert the distance variable if the user has provided a variable name
|
||||
@ -1513,30 +1539,37 @@ export const modelingMachine = setup({
|
||||
if (!input) return new Error('No input provided')
|
||||
// Extract inputs
|
||||
const ast = kclManager.ast
|
||||
const { plane: selection, distance } = input
|
||||
const { plane: selection, distance, nodeToEdit } = input
|
||||
|
||||
let insertIndex: number | undefined = undefined
|
||||
let planeName: string | undefined = undefined
|
||||
|
||||
// If this is an edit flow, first we're going to remove the old plane
|
||||
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
|
||||
// Extract the plane name from the node to edit
|
||||
const planeNameNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
nodeToEdit,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(planeNameNode)) {
|
||||
console.error('Error extracting plane name')
|
||||
} else {
|
||||
planeName = planeNameNode.node.declaration.id.name
|
||||
}
|
||||
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(nodeToEdit[1][0], 1)
|
||||
ast.body = newBody
|
||||
insertIndex = nodeToEdit[1][0]
|
||||
}
|
||||
|
||||
// Extract the default plane from selection
|
||||
const plane = selection.otherSelections[0]
|
||||
if (!(plane && plane instanceof Object && 'name' in plane))
|
||||
return trap('No plane selected')
|
||||
|
||||
// Insert the distance variable if it exists
|
||||
if (
|
||||
'variableName' in distance &&
|
||||
distance.variableName &&
|
||||
distance.insertIndex !== undefined
|
||||
) {
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(
|
||||
distance.insertIndex,
|
||||
0,
|
||||
distance.variableDeclarationAst
|
||||
)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
// Get the default plane name from the selection
|
||||
|
||||
const offsetPlaneResult = addOffsetPlane({
|
||||
node: ast,
|
||||
defaultPlane: plane.name,
|
||||
@ -1544,8 +1577,27 @@ export const modelingMachine = setup({
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst,
|
||||
insertIndex,
|
||||
planeName,
|
||||
})
|
||||
|
||||
// Insert the distance variable if the user has provided a variable name
|
||||
if (
|
||||
'variableName' in distance &&
|
||||
distance.variableName &&
|
||||
typeof offsetPlaneResult.pathToNode[1][0] === 'number'
|
||||
) {
|
||||
const insertIndex = Math.min(
|
||||
offsetPlaneResult.pathToNode[1][0],
|
||||
distance.insertIndex
|
||||
)
|
||||
const newBody = [...offsetPlaneResult.modifiedAst.body]
|
||||
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
|
||||
offsetPlaneResult.modifiedAst.body = newBody
|
||||
// Since we inserted a new variable, we need to update the path to the extrude argument
|
||||
offsetPlaneResult.pathToNode[1][0]++
|
||||
}
|
||||
|
||||
const updateAstResult = await kclManager.updateAst(
|
||||
offsetPlaneResult.modifiedAst,
|
||||
true,
|
||||
|
Reference in New Issue
Block a user