Merge remote-tracking branch 'origin/main' into paultag/refgraph
This commit is contained in:
		| @ -1,3 +1,3 @@ | ||||
| [codespell] | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser | ||||
| skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo | ||||
|  | ||||
							
								
								
									
										44
									
								
								.github/workflows/cargo-bench.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										44
									
								
								.github/workflows/cargo-bench.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,44 +0,0 @@ | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - '**.rs' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-bench.yml | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - '**.rs' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-bench.yml | ||||
|   workflow_dispatch: | ||||
| permissions: read-all | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
|   cancel-in-progress: true | ||||
| name: cargo bench | ||||
| jobs: | ||||
|   cargo-bench: | ||||
|     name: Benchmark with iai | ||||
|     runs-on: ubuntu-latest-8-cores | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: dtolnay/rust-toolchain@stable | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           cargo install cargo-criterion | ||||
|           sudo apt update | ||||
|           sudo apt install -y valgrind | ||||
|       - name: Rust Cache | ||||
|         uses: Swatinem/rust-cache@v2.6.1 | ||||
|       - name: Benchmark kcl library | ||||
|         shell: bash | ||||
|         run: |- | ||||
|           cd src/wasm-lib/kcl; cargo bench --all-features -- iai | ||||
|         env: | ||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} | ||||
|  | ||||
							
								
								
									
										20587
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						
									
										20587
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										28
									
								
								docs/kcl/types/Face.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								docs/kcl/types/Face.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| --- | ||||
| title: "Face" | ||||
| excerpt: "A face." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A face. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `id` |`string`| The id of the face. | No | | ||||
| | `value` |`string`| The tag of the face. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -20,6 +20,7 @@ A helix. | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -20,6 +20,7 @@ A helix. | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -168,7 +168,6 @@ Any KCL value. | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A plane. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
| @ -181,17 +180,10 @@ A plane. | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | ||||
| | `id` |`string`| The id of the plane. | No | | ||||
| | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | | ||||
| | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
| | `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A face. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
| @ -203,14 +195,8 @@ A face. | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Face`|  | No | | ||||
| | `id` |`string`| The id of the face. | No | | ||||
| | `value` |`string`| The tag of the face. | No | | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
| | `type` |enum: [`Face`](/docs/kcl/types/Face)|  | No | | ||||
| | `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| @ -246,7 +232,6 @@ A face. | ||||
|  | ||||
|  | ||||
| ---- | ||||
| An solid is a collection of extrude surfaces. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
| @ -259,14 +244,7 @@ An solid is a collection of extrude surfaces. | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | ||||
| | `id` |`string`| The id of the solid. | No | | ||||
| | `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No | | ||||
| | `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No | | ||||
| | `height` |`number`| The height of the solid. | No | | ||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||
| | `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| @ -286,7 +264,6 @@ An solid is a collection of extrude surfaces. | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A helix. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
| @ -299,11 +276,7 @@ A helix. | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Helix`](/docs/kcl/types/Helix)|  | No | | ||||
| | `value` |`string`| The id of the helix. | No | | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
| | `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| @ -22,6 +22,7 @@ A plane. | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -21,6 +21,7 @@ A sketch is a collection of paths. | ||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -30,6 +30,7 @@ A sketch is a collection of paths. | ||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -31,6 +31,7 @@ A plane. | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -54,6 +55,7 @@ A face. | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -23,6 +23,7 @@ An solid is a collection of extrude surfaces. | ||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -32,6 +32,7 @@ An solid is a collection of extrude surfaces. | ||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										107
									
								
								docs/kcl/types/UnitLen.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								docs/kcl/types/UnitLen.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| --- | ||||
| title: "UnitLen" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Mm`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Cm`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `M`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Inches`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Feet`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Yards`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -280,7 +280,7 @@ test( | ||||
|  | ||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||
|       await expect(page.getByText('New Project')).toBeVisible() | ||||
|       await expect(page.getByText('Create project')).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Opening the router-template project should load', async () => { | ||||
|  | ||||
| @ -38,14 +38,14 @@ test.describe('Debug pane', () => { | ||||
|       // Set the code in the code editor. | ||||
|       await u.codeLocator.click() | ||||
|       await page.keyboard.type(code, { delay: 0 }) | ||||
|       // Scroll to the feature tree. | ||||
|       // Scroll to the artifact graph. | ||||
|       await tree.scrollIntoViewIfNeeded() | ||||
|       // Expand the feature tree. | ||||
|       await tree.getByText('Feature Tree').click() | ||||
|       // Expand the artifact graph. | ||||
|       await tree.getByText('Artifact Graph').click() | ||||
|       // Just expanded the details, making the element taller, so scroll again. | ||||
|       await tree.getByText('Plane').first().scrollIntoViewIfNeeded() | ||||
|     }) | ||||
|     // Extract the artifact IDs from the debug feature tree. | ||||
|     // Extract the artifact IDs from the debug artifact graph. | ||||
|     const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) | ||||
|     // The artifact ID should include a UUID. | ||||
|     expect(initialSegmentIds).toMatch( | ||||
|  | ||||
| @ -135,4 +135,20 @@ export class CmdBarFixture { | ||||
|       await promptEditCommand.first().click() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get cmdSearchInput() { | ||||
|     return this.page.getByTestId('cmd-bar-search') | ||||
|   } | ||||
|  | ||||
|   get argumentInput() { | ||||
|     return this.page.getByTestId('cmd-bar-arg-value') | ||||
|   } | ||||
|  | ||||
|   get cmdOptions() { | ||||
|     return this.page.getByTestId('cmd-bar-option') | ||||
|   } | ||||
|  | ||||
|   chooseCommand = async (commandName: string) => { | ||||
|     await this.cmdOptions.getByText(commandName).click() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -103,7 +103,7 @@ export class HomePageFixture { | ||||
|       .toEqual(expectedState) | ||||
|   } | ||||
|  | ||||
|   createAndGoToProject = async (projectTitle: string) => { | ||||
|   createAndGoToProject = async (projectTitle = 'project-$nnn') => { | ||||
|     await expect(this.projectSection).not.toHaveText('Loading your Projects...') | ||||
|     await this.projectButtonNew.click() | ||||
|     await this.projectTextName.click() | ||||
|  | ||||
| @ -63,6 +63,10 @@ export class ToolbarFixture { | ||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||
|   } | ||||
|  | ||||
|   get logoLink() { | ||||
|     return this.page.getByTestId('app-logo') | ||||
|   } | ||||
|  | ||||
|   startSketchPlaneSelection = async () => | ||||
|     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) | ||||
|  | ||||
|  | ||||
| @ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ') | ||||
|     await toolbar.sweepButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'profile', | ||||
|       currentArgKey: 'target', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Path: '', | ||||
|         Profile: '', | ||||
|         Target: '', | ||||
|         Trajectory: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'profile', | ||||
|       highlightedHeaderArg: 'target', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch1() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'path', | ||||
|       currentArgKey: 'trajectory', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Path: '', | ||||
|         Profile: '1 face', | ||||
|         Target: '1 face', | ||||
|         Trajectory: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'path', | ||||
|       highlightedHeaderArg: 'trajectory', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch2() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       headerArguments: { | ||||
|         Path: '1 face', | ||||
|         Profile: '1 face', | ||||
|       }, | ||||
|       stage: 'review', | ||||
|     }) | ||||
|     await page.waitForTimeout(500) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await page.waitForTimeout(500) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
| @ -1020,6 +1014,75 @@ sketch002 = startSketchOn('XZ') | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Sweep point-and-click failing validation`, async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   const initialCode = `sketch001 = startSketchOn('YZ') | ||||
|   |> circle({ | ||||
|        center = [0, 0], | ||||
|        radius = 500 | ||||
|      }, %) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> xLine(-500, %) | ||||
|   |> lineTo([-2000, 500], %) | ||||
| ` | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|   await scene.waitForExecutionDone() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 700, y: 250 } | ||||
|   const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|   const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y) | ||||
|  | ||||
|   await test.step(`Look for sketch001`, async () => { | ||||
|     await toolbar.closePane('code') | ||||
|     await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Go through the command bar flow and fail validation with a toast`, async () => { | ||||
|     await toolbar.sweepButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'target', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Target: '', | ||||
|         Trajectory: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'target', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch1() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'trajectory', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Target: '1 face', | ||||
|         Trajectory: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'trajectory', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch2() | ||||
|     await page.waitForTimeout(500) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await expect( | ||||
|       page.getByText('Unable to sweep with the provided selection') | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Fillet point-and-click`, async ({ | ||||
|   context, | ||||
|   page, | ||||
| @ -1503,6 +1566,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | ||||
|         await clickOnCap() | ||||
|         await page.waitForTimeout(500) | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await page.waitForTimeout(500) | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
| @ -1523,6 +1587,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | ||||
|       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { | ||||
|         await toolbar.shellButton.click() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await page.waitForTimeout(500) | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
| @ -1604,6 +1669,7 @@ extrude001 = extrude(40, sketch001) | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.up('Shift') | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await page.waitForTimeout(500) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'review', | ||||
| @ -1727,3 +1793,61 @@ shellSketchOnFacesCases.forEach((initialCode, index) => { | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Shell dry-run validation rejects sweeps`, async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   const initialCode = `sketch001 = startSketchOn('YZ') | ||||
|   |> circle({ | ||||
|        center = [0, 0], | ||||
|        radius = 500 | ||||
|      }, %) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> xLine(-2000, %) | ||||
| sweep001 = sweep({ path = sketch002 }, sketch001) | ||||
| ` | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|   await scene.waitForExecutionDone() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 500, y: 250 } | ||||
|   const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|  | ||||
|   await test.step(`Confirm sweep exists`, async () => { | ||||
|     await toolbar.closePane('code') | ||||
|     await scene.expectPixelColor([231, 231, 231], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Go through the Shell flow and fail validation with a toast`, async () => { | ||||
|     await toolbar.shellButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'arguments', | ||||
|       currentArgKey: 'selection', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Selection: '', | ||||
|         Thickness: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'selection', | ||||
|       commandName: 'Shell', | ||||
|     }) | ||||
|     await clickOnSweep() | ||||
|     await page.waitForTimeout(500) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await expect( | ||||
|       page.getByText('Unable to shell with the provided selection') | ||||
|     ).toBeVisible() | ||||
|     await page.waitForTimeout(1000) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -172,7 +172,7 @@ test( | ||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||
|       await expect(page.getByText('broken-code')).toBeVisible() | ||||
|       await expect(page.getByText('bracket')).toBeVisible() | ||||
|       await expect(page.getByText('New Project')).toBeVisible() | ||||
|       await expect(page.getByText('Create project')).toBeVisible() | ||||
|     }) | ||||
|     await test.step('opening broken code project should clear the scene and show the error', async () => { | ||||
|       // Go back home. | ||||
| @ -253,7 +253,7 @@ test( | ||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||
|       await expect(page.getByText('empty')).toBeVisible() | ||||
|       await expect(page.getByText('bracket')).toBeVisible() | ||||
|       await expect(page.getByText('New Project')).toBeVisible() | ||||
|       await expect(page.getByText('Create project')).toBeVisible() | ||||
|     }) | ||||
|     await test.step('opening empty code project should clear the scene', async () => { | ||||
|       // Go back home. | ||||
| @ -985,6 +985,126 @@ test.describe(`Project management commands`, () => { | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
|   test(`Create a new project with a colliding name`, async ({ | ||||
|     context, | ||||
|     homePage, | ||||
|     toolbar, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     const projectName = 'test-project' | ||||
|     await test.step(`Setup`, async () => { | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const projectDir = path.join(dir, projectName) | ||||
|         await Promise.all([fsp.mkdir(projectDir, { recursive: true })]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             executorInputPath('router-template-slate.kcl'), | ||||
|             path.join(projectDir, 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }) | ||||
|       await homePage.expectState({ | ||||
|         projectCards: [ | ||||
|           { | ||||
|             title: projectName, | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|         ], | ||||
|         sortBy: 'last-modified-desc', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Create a new project with the same name', async () => { | ||||
|       await cmdBar.openCmdBar() | ||||
|       await cmdBar.chooseCommand('create project') | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'arguments', | ||||
|         commandName: 'Create project', | ||||
|         currentArgKey: 'name', | ||||
|         currentArgValue: '', | ||||
|         headerArguments: { | ||||
|           Name: '', | ||||
|         }, | ||||
|         highlightedHeaderArg: 'name', | ||||
|       }) | ||||
|       await cmdBar.argumentInput.fill(projectName) | ||||
|       await cmdBar.progressCmdBar() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Check the project was created with a non-colliding name`, async () => { | ||||
|       await toolbar.logoLink.click() | ||||
|       await homePage.expectState({ | ||||
|         projectCards: [ | ||||
|           { | ||||
|             title: projectName + '-1', | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|           { | ||||
|             title: projectName, | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|         ], | ||||
|         sortBy: 'last-modified-desc', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Create another project with the same name', async () => { | ||||
|       await cmdBar.openCmdBar() | ||||
|       await cmdBar.chooseCommand('create project') | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'arguments', | ||||
|         commandName: 'Create project', | ||||
|         currentArgKey: 'name', | ||||
|         currentArgValue: '', | ||||
|         headerArguments: { | ||||
|           Name: '', | ||||
|         }, | ||||
|         highlightedHeaderArg: 'name', | ||||
|       }) | ||||
|       await cmdBar.argumentInput.fill(projectName) | ||||
|       await cmdBar.progressCmdBar() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Check the second project was created with a non-colliding name`, async () => { | ||||
|       await toolbar.logoLink.click() | ||||
|       await homePage.expectState({ | ||||
|         projectCards: [ | ||||
|           { | ||||
|             title: projectName + '-2', | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|           { | ||||
|             title: projectName + '-1', | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|           { | ||||
|             title: projectName, | ||||
|             fileCount: 1, | ||||
|           }, | ||||
|         ], | ||||
|         sortBy: 'last-modified-desc', | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Create a few projects using the default project name`, async ({ | ||||
|   homePage, | ||||
|   toolbar, | ||||
| }) => { | ||||
|   for (let i = 0; i < 12; i++) { | ||||
|     await test.step(`Create project ${i}`, async () => { | ||||
|       await homePage.expectState({ | ||||
|         projectCards: Array.from({ length: i }, (_, i) => ({ | ||||
|           title: `project-${i.toString().padStart(3, '0')}`, | ||||
|           fileCount: 1, | ||||
|         })).toReversed(), | ||||
|         sortBy: 'last-modified-desc', | ||||
|       }) | ||||
|       await homePage.createAndGoToProject() | ||||
|       await toolbar.logoLink.click() | ||||
|     }) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| test( | ||||
| @ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`) | ||||
|     await page.getByTestId('app-logo').click() | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'New project' }) | ||||
|       page.getByRole('button', { name: 'Create project' }) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     for (let i = 1; i <= 10; i++) { | ||||
| @ -1465,7 +1585,7 @@ test( | ||||
|  | ||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||
|       await expect(page.getByText('New Project')).toBeVisible() | ||||
|       await expect(page.getByText('Create project')).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Opening the router-template project should load the stream', async () => { | ||||
| @ -1494,7 +1614,7 @@ test( | ||||
|  | ||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||
|       await expect(page.getByText('New Project')).toBeVisible() | ||||
|       await expect(page.getByText('Create project')).toBeVisible() | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB | 
| @ -1078,7 +1078,7 @@ export async function createProject({ | ||||
|   returnHome?: boolean | ||||
| }) { | ||||
|   await test.step(`Create project and navigate to it`, async () => { | ||||
|     await page.getByRole('button', { name: 'New project' }).click() | ||||
|     await page.getByRole('button', { name: 'Create project' }).click() | ||||
|     await page.getByRole('textbox', { name: 'Name' }).fill(name) | ||||
|     await page.getByRole('button', { name: 'Continue' }).click() | ||||
|  | ||||
|  | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @ -113,9 +113,9 @@ | ||||
|     "test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts", | ||||
|     "test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts", | ||||
|     "test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'", | ||||
|     "test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"", | ||||
|     "test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'", | ||||
|     "test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'", | ||||
|     "test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet", | ||||
|     "test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet", | ||||
|     "test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet", | ||||
|     "test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'", | ||||
|     "test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"", | ||||
|     "test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'", | ||||
| @ -154,7 +154,6 @@ | ||||
|     "@playwright/test": "^1.49.0", | ||||
|     "@testing-library/jest-dom": "^5.14.1", | ||||
|     "@testing-library/react": "^15.0.2", | ||||
|     "@types/d3-force": "^3.0.10", | ||||
|     "@types/diff": "^6.0.0", | ||||
|     "@types/electron": "^1.6.10", | ||||
|     "@types/isomorphic-fetch": "^0.0.39", | ||||
| @ -175,7 +174,6 @@ | ||||
|     "@vitest/web-worker": "^1.5.0", | ||||
|     "@xstate/cli": "^0.5.17", | ||||
|     "autoprefixer": "^10.4.19", | ||||
|     "d3-force": "^3.0.0", | ||||
|     "electron": "32.1.2", | ||||
|     "electron-builder": "24.13.3", | ||||
|     "electron-notarize": "1.2.2", | ||||
| @ -203,7 +201,7 @@ | ||||
|     "ts-node": "^10.0.0", | ||||
|     "typescript": "^5.7.3", | ||||
|     "typescript-eslint": "^8.19.1", | ||||
|     "vite": "^5.4.6", | ||||
|     "vite": "^5.4.12", | ||||
|     "vite-plugin-package-version": "^1.1.0", | ||||
|     "vite-tsconfig-paths": "^4.3.2", | ||||
|     "vitest": "^1.6.0", | ||||
|  | ||||
| @ -31,7 +31,6 @@ import { | ||||
|   settingsLoader, | ||||
|   telemetryLoader, | ||||
| } from 'lib/routeLoaders' | ||||
| import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider' | ||||
| import SettingsAuthProvider from 'components/SettingsAuthProvider' | ||||
| import LspProvider from 'components/LspProvider' | ||||
| import { KclContextProvider } from 'lang/KclProvider' | ||||
| @ -58,23 +57,21 @@ const router = createRouter([ | ||||
|     /* Make sure auth is the outermost provider or else we will have | ||||
|      * inefficient re-renders, use the react profiler to see. */ | ||||
|     element: ( | ||||
|       <CommandBarProvider> | ||||
|         <RouteProvider> | ||||
|           <SettingsAuthProvider> | ||||
|             <LspProvider> | ||||
|               <ProjectsContextProvider> | ||||
|                 <KclContextProvider> | ||||
|                   <AppStateProvider> | ||||
|                     <MachineManagerProvider> | ||||
|                       <Outlet /> | ||||
|                     </MachineManagerProvider> | ||||
|                   </AppStateProvider> | ||||
|                 </KclContextProvider> | ||||
|               </ProjectsContextProvider> | ||||
|             </LspProvider> | ||||
|           </SettingsAuthProvider> | ||||
|         </RouteProvider> | ||||
|       </CommandBarProvider> | ||||
|       <RouteProvider> | ||||
|         <SettingsAuthProvider> | ||||
|           <LspProvider> | ||||
|             <ProjectsContextProvider> | ||||
|               <KclContextProvider> | ||||
|                 <AppStateProvider> | ||||
|                   <MachineManagerProvider> | ||||
|                     <Outlet /> | ||||
|                   </MachineManagerProvider> | ||||
|                 </AppStateProvider> | ||||
|               </KclContextProvider> | ||||
|             </ProjectsContextProvider> | ||||
|           </LspProvider> | ||||
|         </SettingsAuthProvider> | ||||
|       </RouteProvider> | ||||
|     ), | ||||
|     errorElement: <ErrorPage />, | ||||
|     children: [ | ||||
|  | ||||
							
								
								
									
										221
									
								
								src/Toolbar.tsx
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								src/Toolbar.tsx
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | ||||
| import { useRef, useMemo, memo } from 'react' | ||||
| import { useRef, useMemo, memo, useCallback, useState } from 'react' | ||||
| import { isCursorInSketchCommandRange } from 'lang/util' | ||||
| import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||
| import { ActionButton } from 'components/ActionButton' | ||||
| @ -22,20 +21,19 @@ import { | ||||
| } from 'lib/toolbar' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| export function Toolbar({ | ||||
|   className = '', | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLElement>) { | ||||
|   const { state, send, context } = useModelingContext() | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const iconClassName = | ||||
|     'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit' | ||||
|   const bgClassName = '!bg-transparent' | ||||
|   const buttonBgClassName = | ||||
|     'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10' | ||||
|   const buttonBorderClassName = | ||||
|     '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' | ||||
|   const buttonBorderClassName = '!border-transparent' | ||||
|  | ||||
|   const sketchPathId = useMemo(() => { | ||||
|     if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) | ||||
| @ -50,6 +48,7 @@ export function Toolbar({ | ||||
|   const { overallState } = useNetworkContext() | ||||
|   const { isExecuting } = useKclContext() | ||||
|   const { isStreamReady } = useAppState() | ||||
|   const [showRichContent, setShowRichContent] = useState(false) | ||||
|  | ||||
|   const disableAllButtons = | ||||
|     (overallState !== NetworkHealthState.Ok && | ||||
| @ -71,12 +70,45 @@ export function Toolbar({ | ||||
|     () => ({ | ||||
|       modelingState: state, | ||||
|       modelingSend: send, | ||||
|       commandBarSend, | ||||
|       sketchPathId, | ||||
|     }), | ||||
|     [state, send, commandBarSend, sketchPathId] | ||||
|     [state, send, commandBarActor.send, sketchPathId] | ||||
|   ) | ||||
|  | ||||
|   const tooltipContentClassName = !showRichContent | ||||
|     ? '' | ||||
|     : '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch' | ||||
|   const richContentTimeout = useRef<number | null>(null) | ||||
|   const richContentClearTimeout = useRef<number | null>(null) | ||||
|   // On mouse enter, show rich content after a 1s delay | ||||
|   const handleMouseEnter = useCallback(() => { | ||||
|     // Cancel the clear timeout if it's already set | ||||
|     if (richContentClearTimeout.current) { | ||||
|       clearTimeout(richContentClearTimeout.current) | ||||
|     } | ||||
|     // Start our own timeout to show the rich content | ||||
|     richContentTimeout.current = window.setTimeout(() => { | ||||
|       setShowRichContent(true) | ||||
|       if (richContentClearTimeout.current) { | ||||
|         clearTimeout(richContentClearTimeout.current) | ||||
|       } | ||||
|     }, 1000) | ||||
|   }, [setShowRichContent]) | ||||
|   // On mouse leave, clear the timeout and hide rich content | ||||
|   const handleMouseLeave = useCallback(() => { | ||||
|     // Clear the timeout to show rich content | ||||
|     if (richContentTimeout.current) { | ||||
|       clearTimeout(richContentTimeout.current) | ||||
|     } | ||||
|     // Start a timeout to hide the rich content | ||||
|     richContentClearTimeout.current = window.setTimeout(() => { | ||||
|       setShowRichContent(false) | ||||
|       if (richContentClearTimeout.current) { | ||||
|         clearTimeout(richContentClearTimeout.current) | ||||
|       } | ||||
|     }, 500) | ||||
|   }, [setShowRichContent]) | ||||
|  | ||||
|   /** | ||||
|    * Resolve all the callbacks and values for the current mode, | ||||
|    * so we don't need to worry about the other modes | ||||
| @ -174,43 +206,64 @@ export function Toolbar({ | ||||
|                   status: itemConfig.status, | ||||
|                 }))} | ||||
|               > | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   id={maybeIconConfig[0].id} | ||||
|                   data-testid={maybeIconConfig[0].id} | ||||
|                   iconStart={{ | ||||
|                     icon: maybeIconConfig[0].icon, | ||||
|                     className: iconClassName, | ||||
|                     bgClassName: bgClassName, | ||||
|                   }} | ||||
|                   className={ | ||||
|                     '!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' + | ||||
|                     buttonBgClassName | ||||
|                   } | ||||
|                   aria-pressed={maybeIconConfig[0].isActive} | ||||
|                   disabled={ | ||||
|                     disableAllButtons || | ||||
|                     maybeIconConfig[0].status !== 'available' || | ||||
|                     maybeIconConfig[0].disabled | ||||
|                   } | ||||
|                   name={maybeIconConfig[0].title} | ||||
|                   // aria-description is still in ARIA 1.3 draft. | ||||
|                   // eslint-disable-next-line jsx-a11y/aria-props | ||||
|                   aria-description={maybeIconConfig[0].description} | ||||
|                   onClick={() => | ||||
|                     maybeIconConfig[0].onClick(configCallbackProps) | ||||
|                   } | ||||
|                 <div | ||||
|                   className="contents" | ||||
|                   // Mouse events do not fire on disabled buttons | ||||
|                   onMouseEnter={handleMouseEnter} | ||||
|                   onMouseLeave={handleMouseLeave} | ||||
|                 > | ||||
|                   <span | ||||
|                     className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''} | ||||
|                   <ActionButton | ||||
|                     Element="button" | ||||
|                     id={maybeIconConfig[0].id} | ||||
|                     data-testid={maybeIconConfig[0].id} | ||||
|                     iconStart={{ | ||||
|                       icon: maybeIconConfig[0].icon, | ||||
|                       className: iconClassName, | ||||
|                       bgClassName: bgClassName, | ||||
|                     }} | ||||
|                     className={ | ||||
|                       '!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' + | ||||
|                       buttonBgClassName | ||||
|                     } | ||||
|                     aria-pressed={maybeIconConfig[0].isActive} | ||||
|                     disabled={ | ||||
|                       disableAllButtons || | ||||
|                       maybeIconConfig[0].status !== 'available' || | ||||
|                       maybeIconConfig[0].disabled | ||||
|                     } | ||||
|                     name={maybeIconConfig[0].title} | ||||
|                     // aria-description is still in ARIA 1.3 draft. | ||||
|                     // eslint-disable-next-line jsx-a11y/aria-props | ||||
|                     aria-description={maybeIconConfig[0].description} | ||||
|                     onClick={() => | ||||
|                       maybeIconConfig[0].onClick(configCallbackProps) | ||||
|                     } | ||||
|                   > | ||||
|                     {maybeIconConfig[0].title} | ||||
|                   </span> | ||||
|                 </ActionButton> | ||||
|                 <ToolbarItemTooltip | ||||
|                   itemConfig={maybeIconConfig[0]} | ||||
|                   configCallbackProps={configCallbackProps} | ||||
|                 /> | ||||
|                     <span | ||||
|                       className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''} | ||||
|                     > | ||||
|                       {maybeIconConfig[0].title} | ||||
|                     </span> | ||||
|                     <ToolbarItemTooltip | ||||
|                       itemConfig={maybeIconConfig[0]} | ||||
|                       configCallbackProps={configCallbackProps} | ||||
|                       wrapperClassName="ui-open:!hidden" | ||||
|                       contentClassName={tooltipContentClassName} | ||||
|                     > | ||||
|                       {showRichContent ? ( | ||||
|                         <ToolbarItemTooltipRichContent | ||||
|                           itemConfig={maybeIconConfig[0]} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <ToolbarItemTooltipShortContent | ||||
|                           status={maybeIconConfig[0].status} | ||||
|                           title={maybeIconConfig[0].title} | ||||
|                           hotkey={maybeIconConfig[0].hotkey} | ||||
|                         /> | ||||
|                       )} | ||||
|                     </ToolbarItemTooltip> | ||||
|                   </ActionButton> | ||||
|                 </div> | ||||
|               </ActionButtonDropdown> | ||||
|             ) | ||||
|           } | ||||
| @ -218,7 +271,13 @@ export function Toolbar({ | ||||
|  | ||||
|           // A single button | ||||
|           return ( | ||||
|             <div className="relative" key={itemConfig.id}> | ||||
|             <div | ||||
|               className="relative" | ||||
|               key={itemConfig.id} | ||||
|               // Mouse events do not fire on disabled buttons | ||||
|               onMouseEnter={handleMouseEnter} | ||||
|               onMouseLeave={handleMouseLeave} | ||||
|             > | ||||
|               <ActionButton | ||||
|                 Element="button" | ||||
|                 key={itemConfig.id} | ||||
| @ -255,7 +314,18 @@ export function Toolbar({ | ||||
|               <ToolbarItemTooltip | ||||
|                 itemConfig={itemConfig} | ||||
|                 configCallbackProps={configCallbackProps} | ||||
|               /> | ||||
|                 contentClassName={tooltipContentClassName} | ||||
|               > | ||||
|                 {showRichContent ? ( | ||||
|                   <ToolbarItemTooltipRichContent itemConfig={itemConfig} /> | ||||
|                 ) : ( | ||||
|                   <ToolbarItemTooltipShortContent | ||||
|                     status={itemConfig.status} | ||||
|                     title={itemConfig.title} | ||||
|                     hotkey={itemConfig.hotkey} | ||||
|                   /> | ||||
|                 )} | ||||
|               </ToolbarItemTooltip> | ||||
|             </div> | ||||
|           ) | ||||
|         })} | ||||
| @ -269,6 +339,12 @@ export function Toolbar({ | ||||
|   ) | ||||
| } | ||||
|  | ||||
| interface ToolbarItemContentsProps extends React.PropsWithChildren { | ||||
|   itemConfig: ToolbarItemResolved | ||||
|   configCallbackProps: ToolbarItemCallbackProps | ||||
|   wrapperClassName?: string | ||||
|   contentClassName?: string | ||||
| } | ||||
| /** | ||||
|  * The single button and dropdown button share content, so we extract it here | ||||
|  * It contains a tooltip with the title, description, and links | ||||
| @ -277,12 +353,10 @@ export function Toolbar({ | ||||
| const ToolbarItemTooltip = memo(function ToolbarItemContents({ | ||||
|   itemConfig, | ||||
|   configCallbackProps, | ||||
| }: { | ||||
|   itemConfig: ToolbarItemResolved | ||||
|   configCallbackProps: ToolbarItemCallbackProps | ||||
| }) { | ||||
|   const { state } = useModelingContext() | ||||
|  | ||||
|   wrapperClassName = '', | ||||
|   contentClassName = '', | ||||
|   children, | ||||
| }: ToolbarItemContentsProps) { | ||||
|   useHotkeys( | ||||
|     itemConfig.hotkey || '', | ||||
|     () => { | ||||
| @ -305,11 +379,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({ | ||||
|           ? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties) | ||||
|           : {} | ||||
|       } | ||||
|       hoverOnly | ||||
|       position="bottom" | ||||
|       wrapperClassName="!p-4 !pointer-events-auto" | ||||
|       contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" | ||||
|       wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName} | ||||
|       contentClassName={contentClassName} | ||||
|       delay={0} | ||||
|     > | ||||
|       {children} | ||||
|     </Tooltip> | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| const ToolbarItemTooltipShortContent = ({ | ||||
|   status, | ||||
|   title, | ||||
|   hotkey, | ||||
| }: { | ||||
|   status: string | ||||
|   title: string | ||||
|   hotkey?: string | string[] | ||||
| }) => ( | ||||
|   <span | ||||
|     className={`text-sm ${ | ||||
|       status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : '' | ||||
|     }`} | ||||
|   > | ||||
|     {title} | ||||
|     {hotkey && ( | ||||
|       <kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd> | ||||
|     )} | ||||
|   </span> | ||||
| ) | ||||
|  | ||||
| const ToolbarItemTooltipRichContent = ({ | ||||
|   itemConfig, | ||||
| }: { | ||||
|   itemConfig: ToolbarItemResolved | ||||
| }) => { | ||||
|   const { state } = useModelingContext() | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50"> | ||||
|         {itemConfig.icon && ( | ||||
|           <CustomIcon className="w-5 h-5" name={itemConfig.icon} /> | ||||
|         )} | ||||
|         <span | ||||
|           className={`text-sm flex-1 ${ | ||||
|             itemConfig.status !== 'available' | ||||
| @ -378,6 +491,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({ | ||||
|           </ul> | ||||
|         </> | ||||
|       )} | ||||
|     </Tooltip> | ||||
|     </> | ||||
|   ) | ||||
| }) | ||||
| } | ||||
|  | ||||
| @ -25,13 +25,13 @@ import { | ||||
|   CallExpression, | ||||
|   PathToNode, | ||||
|   Program, | ||||
|   SourceRange, | ||||
|   Expr, | ||||
|   parse, | ||||
|   recast, | ||||
|   defaultSourceRange, | ||||
|   resultIsOk, | ||||
|   ProgramMemory, | ||||
|   topLevelRange, | ||||
| } from 'lang/wasm' | ||||
| import { CustomIcon, CustomIconName } from 'components/CustomIcon' | ||||
| import { ConstrainInfo } from 'lang/std/stdTypes' | ||||
| @ -46,8 +46,8 @@ import { | ||||
| } from 'lang/modifyAst' | ||||
| import { ActionButton } from 'components/ActionButton' | ||||
| import { err, reportRejection, trap } from 'lib/trap' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { | ||||
|   const [isCamMoving, setIsCamMoving] = useState(false) | ||||
| @ -510,7 +510,6 @@ const ConstraintSymbol = ({ | ||||
|   constrainInfo: ConstrainInfo | ||||
|   verticalPosition: 'top' | 'bottom' | ||||
| }) => { | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const { context } = useModelingContext() | ||||
|   const varNameMap: { | ||||
|     [key in ConstrainInfo['type']]: { | ||||
| @ -600,8 +599,8 @@ const ConstraintSymbol = ({ | ||||
|   if (err(_node)) return | ||||
|   const node = _node.node | ||||
|  | ||||
|   const range: SourceRange = node | ||||
|     ? [node.start, node.end, true] | ||||
|   const range = node | ||||
|     ? topLevelRange(node.start, node.end) | ||||
|     : defaultSourceRange() | ||||
|  | ||||
|   if (_type === 'intersectionTag') return null | ||||
| @ -630,7 +629,7 @@ const ConstraintSymbol = ({ | ||||
|         // disabled={implicitDesc} TODO why does this change styles that are hard to override? | ||||
|         onClick={toSync(async () => { | ||||
|           if (!isConstrained) { | ||||
|             commandBarSend({ | ||||
|             commandBarActor.send({ | ||||
|               type: 'Find and select command', | ||||
|               data: { | ||||
|                 name: 'Constrain with named value', | ||||
| @ -756,7 +755,6 @@ export const CamDebugSettings = () => { | ||||
|     sceneInfra.camControls.reactCameraProperties | ||||
|   ) | ||||
|   const [fov, setFov] = useState(12) | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings) | ||||
| @ -775,7 +773,7 @@ export const CamDebugSettings = () => { | ||||
|         type="checkbox" | ||||
|         checked={camSettings.type === 'perspective'} | ||||
|         onChange={() => | ||||
|           commandBarSend({ | ||||
|           commandBarActor.send({ | ||||
|             type: 'Find and select command', | ||||
|             data: { | ||||
|               groupId: 'settings', | ||||
|  | ||||
| @ -59,6 +59,7 @@ import { | ||||
|   sourceRangeFromRust, | ||||
|   resultIsOk, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from 'lang/wasm' | ||||
| import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib' | ||||
| import { | ||||
| @ -628,7 +629,7 @@ export class SceneEntities { | ||||
|  | ||||
|       const startRange = _node1.node.start | ||||
|       const endRange = _node1.node.end | ||||
|       const sourceRange: SourceRange = [startRange, endRange, true] | ||||
|       const sourceRange = topLevelRange(startRange, endRange) | ||||
|       const selection: Selections = computeSelectionFromSourceRangeAndAST( | ||||
|         sourceRange, | ||||
|         maybeModdedAst | ||||
| @ -1397,23 +1398,23 @@ export class SceneEntities { | ||||
|  | ||||
|       const arg0 = arg(kclCircle3PointArgs[0]) | ||||
|       if (!arg0) return kclManager.ast | ||||
|       arg0[0].value = points[0].x | ||||
|       arg0[0].value = { value: points[0].x, suffix: 'None' } | ||||
|       arg0[0].raw = points[0].x.toString() | ||||
|       arg0[1].value = points[0].y | ||||
|       arg0[1].value = { value: points[0].y, suffix: 'None' } | ||||
|       arg0[1].raw = points[0].y.toString() | ||||
|  | ||||
|       const arg1 = arg(kclCircle3PointArgs[1]) | ||||
|       if (!arg1) return kclManager.ast | ||||
|       arg1[0].value = points[1].x | ||||
|       arg1[0].value = { value: points[1].x, suffix: 'None' } | ||||
|       arg1[0].raw = points[1].x.toString() | ||||
|       arg1[1].value = points[1].y | ||||
|       arg1[1].value = { value: points[1].y, suffix: 'None' } | ||||
|       arg1[1].raw = points[1].y.toString() | ||||
|  | ||||
|       const arg2 = arg(kclCircle3PointArgs[2]) | ||||
|       if (!arg2) return kclManager.ast | ||||
|       arg2[0].value = points[2].x | ||||
|       arg2[0].value = { value: points[2].x, suffix: 'None' } | ||||
|       arg2[0].raw = points[2].x.toString() | ||||
|       arg2[1].value = points[2].y | ||||
|       arg2[1].value = { value: points[2].y, suffix: 'None' } | ||||
|       arg2[1].raw = points[2].y.toString() | ||||
|  | ||||
|       const astSnapshot = structuredClone(kclManager.ast) | ||||
| @ -2012,7 +2013,7 @@ export class SceneEntities { | ||||
|         kclManager.programMemory, | ||||
|         { | ||||
|           type: 'sourceRange', | ||||
|           sourceRange: [node.start, node.end, true], | ||||
|           sourceRange: topLevelRange(node.start, node.end), | ||||
|         }, | ||||
|         getChangeSketchInput() | ||||
|       ) | ||||
| @ -2050,8 +2051,8 @@ export class SceneEntities { | ||||
|       ) | ||||
|       if (!(sk instanceof Reason)) { | ||||
|         sketch = sk | ||||
|       } else if ((maybeSketch as Solid).sketch) { | ||||
|         sketch = (maybeSketch as Solid).sketch | ||||
|       } else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) { | ||||
|         sketch = (maybeSketch.value as Solid).sketch | ||||
|       } | ||||
|       if (!sketch) return | ||||
|  | ||||
| @ -2263,7 +2264,7 @@ export class SceneEntities { | ||||
|           ) | ||||
|           if (trap(_node, { suppress: true })) return | ||||
|           const node = _node.node | ||||
|           editorManager.setHighlightRange([[node.start, node.end, true]]) | ||||
|           editorManager.setHighlightRange([topLevelRange(node.start, node.end)]) | ||||
|           const yellow = 0xffff00 | ||||
|           colorSegment(selected, yellow) | ||||
|           const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) | ||||
| @ -2540,7 +2541,7 @@ export function sketchFromPathToNode({ | ||||
|   const varDec = _varDec.node | ||||
|   const result = programMemory.get(varDec?.id?.name || '') | ||||
|   if (result?.type === 'Solid') { | ||||
|     return result.sketch | ||||
|     return result.value.sketch | ||||
|   } | ||||
|   const sg = sketchFromKclValue(result, varDec?.id?.name) | ||||
|   if (err(sg)) { | ||||
|  | ||||
| @ -61,6 +61,7 @@ import { SegmentInputs } from 'lang/std/stdTypes' | ||||
| import { err } from 'lib/trap' | ||||
| import { editorManager, sceneInfra } from 'lib/singletons' | ||||
| import { Selections } from 'lib/selections' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| interface CreateSegmentArgs { | ||||
|   input: SegmentInputs | ||||
| @ -847,7 +848,7 @@ function createLengthIndicator({ | ||||
|     }) | ||||
|  | ||||
|     // Command Bar | ||||
|     editorManager.commandBarSend({ | ||||
|     commandBarActor.send({ | ||||
|       type: 'Find and select command', | ||||
|       data: { | ||||
|         name: 'Constrain length', | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| import { Popover } from '@headlessui/react' | ||||
| import { ActionButtonProps } from './ActionButton' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import Tooltip from './Tooltip' | ||||
|  | ||||
| type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { | ||||
|   name?: string | ||||
|   dropdownTooltipText?: string | ||||
|   splitMenuItems: { | ||||
|     id: string | ||||
|     label: string | ||||
| @ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { | ||||
| export function ActionButtonDropdown({ | ||||
|   splitMenuItems, | ||||
|   className, | ||||
|   dropdownTooltipText = 'More tools', | ||||
|   children, | ||||
|   ...props | ||||
| }: ActionButtonSplitProps) { | ||||
| @ -26,7 +29,14 @@ export function ActionButtonDropdown({ | ||||
|       {({ close }) => ( | ||||
|         <> | ||||
|           {children} | ||||
|           <Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary"> | ||||
|           <Popover.Button | ||||
|             className={ | ||||
|               '!border-transparent dark:!border-transparent ' + | ||||
|               'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' + | ||||
|               'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' + | ||||
|               'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary' | ||||
|             } | ||||
|           > | ||||
|             <CustomIcon | ||||
|               name="caretDown" | ||||
|               className={ | ||||
| @ -37,6 +47,14 @@ export function ActionButtonDropdown({ | ||||
|             <span className="sr-only"> | ||||
|               {props.name ? props.name + ': ' : ''}open menu | ||||
|             </span> | ||||
|             <Tooltip | ||||
|               delay={0} | ||||
|               position="bottom" | ||||
|               hoverOnly | ||||
|               wrapperClassName="ui-open:!hidden" | ||||
|             > | ||||
|               {dropdownTooltipText} | ||||
|             </Tooltip> | ||||
|           </Popover.Button> | ||||
|           <Popover.Panel | ||||
|             as="ul" | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react' | ||||
| import { trap } from 'lib/trap' | ||||
| import { codeToIdSelections } from 'lib/selections' | ||||
| import { codeRefFromRange } from 'lang/std/artifactGraph' | ||||
| import { defaultSourceRange } from 'lang/wasm' | ||||
| import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm' | ||||
|  | ||||
| export function AstExplorer() { | ||||
|   const { context } = useModelingContext() | ||||
| @ -118,19 +118,19 @@ function DisplayObj({ | ||||
|         hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' | ||||
|       }`} | ||||
|       onMouseEnter={(e) => { | ||||
|         editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]]) | ||||
|         editorManager.setHighlightRange([ | ||||
|           topLevelRange(obj?.start || 0, obj.end), | ||||
|         ]) | ||||
|         e.stopPropagation() | ||||
|       }} | ||||
|       onMouseMove={(e) => { | ||||
|         e.stopPropagation() | ||||
|         editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]]) | ||||
|         editorManager.setHighlightRange([ | ||||
|           topLevelRange(obj?.start || 0, obj.end), | ||||
|         ]) | ||||
|       }} | ||||
|       onClick={(e) => { | ||||
|         const range: [number, number, boolean] = [ | ||||
|           obj?.start || 0, | ||||
|           obj.end || 0, | ||||
|           true, | ||||
|         ] | ||||
|         const range = topLevelRange(obj?.start || 0, obj.end || 0) | ||||
|         const idInfo = codeToIdSelections([ | ||||
|           { codeRef: codeRefFromRange(range, kclManager.ast) }, | ||||
|         ])[0] | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| import { Combobox } from '@headlessui/react' | ||||
| import { useSelector } from '@xstate/react' | ||||
| import Fuse from 'fuse.js' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import { useEffect, useMemo, useRef, useState } from 'react' | ||||
| import { AnyStateMachine, StateFrom } from 'xstate' | ||||
|  | ||||
| @ -23,7 +23,7 @@ function CommandArgOptionInput({ | ||||
|   placeholder?: string | ||||
| }) { | ||||
|   const actorContext = useSelector(arg.machineActor, contextSelector) | ||||
|   const { commandBarSend, commandBarState } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const resolvedOptions = useMemo( | ||||
|     () => | ||||
|       typeof arg.options === 'function' | ||||
| @ -134,6 +134,7 @@ function CommandArgOptionInput({ | ||||
|           </label> | ||||
|           <Combobox.Input | ||||
|             id="option-input" | ||||
|             data-testid="cmd-bar-arg-value" | ||||
|             ref={inputRef} | ||||
|             onChange={(event) => | ||||
|               !event.target.disabled && setQuery(event.target.value) | ||||
| @ -141,7 +142,7 @@ function CommandArgOptionInput({ | ||||
|             className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none" | ||||
|             onKeyDown={(event) => { | ||||
|               if (event.metaKey && event.key === 'k') | ||||
|                 commandBarSend({ type: 'Close' }) | ||||
|                 commandBarActor.send({ type: 'Close' }) | ||||
|               if (event.key === 'Backspace' && !event.currentTarget.value) { | ||||
|                 stepBack() | ||||
|               } | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { Dialog, Popover, Transition } from '@headlessui/react' | ||||
| import { Fragment, useEffect } from 'react' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import CommandBarArgument from './CommandBarArgument' | ||||
| import CommandComboBox from '../CommandComboBox' | ||||
| import CommandBarReview from './CommandBarReview' | ||||
| @ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom' | ||||
| import useHotkeyWrapper from 'lib/hotkeyWrapper' | ||||
| import { CustomIcon } from 'components/CustomIcon' | ||||
| import Tooltip from 'components/Tooltip' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
|  | ||||
| export const COMMAND_PALETTE_HOTKEY = 'mod+k' | ||||
|  | ||||
| export const CommandBar = () => { | ||||
|   const { pathname } = useLocation() | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const { | ||||
|     context: { selectedCommand, currentArgument, commands }, | ||||
|   } = commandBarState | ||||
| @ -23,16 +23,16 @@ export const CommandBar = () => { | ||||
|   // Close the command bar when navigating | ||||
|   useEffect(() => { | ||||
|     if (commandBarState.matches('Closed')) return | ||||
|     commandBarSend({ type: 'Close' }) | ||||
|     commandBarActor.send({ type: 'Close' }) | ||||
|   }, [pathname]) | ||||
|  | ||||
|   // Hook up keyboard shortcuts | ||||
|   useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => { | ||||
|     if (commandBarState.context.commands.length === 0) return | ||||
|     if (commandBarState.matches('Closed')) { | ||||
|       commandBarSend({ type: 'Open' }) | ||||
|       commandBarActor.send({ type: 'Open' }) | ||||
|     } else { | ||||
|       commandBarSend({ type: 'Close' }) | ||||
|       commandBarActor.send({ type: 'Close' }) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
| @ -52,14 +52,14 @@ export const CommandBar = () => { | ||||
|           ...entries[entries.length - 1][1], | ||||
|         } | ||||
|  | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Edit argument', | ||||
|           data: { | ||||
|             arg: currentArg, | ||||
|           }, | ||||
|         }) | ||||
|       } else { | ||||
|         commandBarSend({ type: 'Deselect command' }) | ||||
|         commandBarActor.send({ type: 'Deselect command' }) | ||||
|       } | ||||
|     } else { | ||||
|       const entries = Object.entries(selectedCommand?.args || {}) | ||||
| @ -68,9 +68,9 @@ export const CommandBar = () => { | ||||
|       ) | ||||
|  | ||||
|       if (index === 0) { | ||||
|         commandBarSend({ type: 'Deselect command' }) | ||||
|         commandBarActor.send({ type: 'Deselect command' }) | ||||
|       } else { | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Change current argument', | ||||
|           data: { | ||||
|             arg: { name: entries[index - 1][0], ...entries[index - 1][1] }, | ||||
| @ -85,14 +85,14 @@ export const CommandBar = () => { | ||||
|       show={!commandBarState.matches('Closed') || false} | ||||
|       afterLeave={() => { | ||||
|         if (selectedCommand?.onCancel) selectedCommand.onCancel() | ||||
|         commandBarSend({ type: 'Clear' }) | ||||
|         commandBarActor.send({ type: 'Clear' }) | ||||
|       }} | ||||
|       as={Fragment} | ||||
|     > | ||||
|       <WrapperComponent | ||||
|         open={!commandBarState.matches('Closed') || isSelectionArgument} | ||||
|         onClose={() => { | ||||
|           commandBarSend({ type: 'Close' }) | ||||
|           commandBarActor.send({ type: 'Close' }) | ||||
|         }} | ||||
|         className={ | ||||
|           'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' + | ||||
| @ -122,7 +122,7 @@ export const CommandBar = () => { | ||||
|               ) | ||||
|             )} | ||||
|             <button | ||||
|               onClick={() => commandBarSend({ type: 'Close' })} | ||||
|               onClick={() => commandBarActor.send({ type: 'Close' })} | ||||
|               className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent" | ||||
|             > | ||||
|               <CustomIcon | ||||
|  | ||||
| @ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput' | ||||
| import CommandBarBasicInput from './CommandBarBasicInput' | ||||
| import CommandBarSelectionInput from './CommandBarSelectionInput' | ||||
| import { CommandArgument } from 'lib/commandTypes' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import CommandBarHeader from './CommandBarHeader' | ||||
| import CommandBarKclInput from './CommandBarKclInput' | ||||
| import CommandBarTextareaInput from './CommandBarTextareaInput' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
|  | ||||
| function CommandBarArgument({ stepBack }: { stepBack: () => void }) { | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const { | ||||
|     context: { currentArgument }, | ||||
|   } = commandBarState | ||||
| @ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) { | ||||
|   function onSubmit(data: unknown) { | ||||
|     if (!currentArgument) return | ||||
|  | ||||
|     commandBarSend({ | ||||
|     commandBarActor.send({ | ||||
|       type: 'Submit argument', | ||||
|       data: { | ||||
|         [currentArgument.name]: data, | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CommandArgument } from 'lib/commandTypes' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import { useEffect, useRef } from 'react' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
|  | ||||
| @ -15,8 +15,8 @@ function CommandBarBasicInput({ | ||||
|   stepBack: () => void | ||||
|   onSubmit: (event: unknown) => void | ||||
| }) { | ||||
|   const { commandBarSend, commandBarState } = useCommandsContext() | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) | ||||
|   const commandBarState = useCommandBarState() | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' })) | ||||
|   const inputRef = useRef<HTMLInputElement>(null) | ||||
|  | ||||
|   useEffect(() => { | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CustomIcon } from '../CustomIcon' | ||||
| import React, { useState } from 'react' | ||||
| import { ActionButton } from '../ActionButton' | ||||
| @ -7,9 +6,10 @@ import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes' | ||||
| import Tooltip from 'components/Tooltip' | ||||
| import { roundOff } from 'lib/utils' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
|  | ||||
| function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const { | ||||
|     context: { selectedCommand, currentArgument, argumentsToSubmit }, | ||||
|   } = commandBarState | ||||
| @ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { | ||||
|         ] | ||||
|         const arg = selectedCommand?.args[argName] | ||||
|         if (!argName || !arg) return | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Change current argument', | ||||
|           data: { arg: { ...arg, name: argName } }, | ||||
|         }) | ||||
| @ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { | ||||
|                     } | ||||
|                     disabled={!isReviewing && currentArgument?.name === argName} | ||||
|                     onClick={() => { | ||||
|                       commandBarSend({ | ||||
|                       commandBarActor.send({ | ||||
|                         type: isReviewing | ||||
|                           ? 'Edit argument' | ||||
|                           : 'Change current argument', | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { | ||||
| } from '@codemirror/autocomplete' | ||||
| import { EditorView, keymap, ViewUpdate } from '@codemirror/view' | ||||
| import { CustomIcon } from 'components/CustomIcon' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { CommandArgument, KclCommandValue } from 'lib/commandTypes' | ||||
| import { getSystemTheme } from 'lib/theme' | ||||
| @ -20,6 +19,7 @@ import styles from './CommandBarKclInput.module.css' | ||||
| import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' | ||||
| import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor' | ||||
| import { useSelector } from '@xstate/react' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
|  | ||||
| const machineContextSelector = (snapshot?: { | ||||
|   context: Record<string, unknown> | ||||
| @ -37,7 +37,7 @@ function CommandBarKclInput({ | ||||
|   stepBack: () => void | ||||
|   onSubmit: (event: unknown) => void | ||||
| }) { | ||||
|   const { commandBarSend, commandBarState } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const previouslySetValue = commandBarState.context.argumentsToSubmit[ | ||||
|     arg.name | ||||
|   ] as KclCommandValue | undefined | ||||
| @ -82,7 +82,7 @@ function CommandBarKclInput({ | ||||
|       false | ||||
|   ) | ||||
|   const [canSubmit, setCanSubmit] = useState(true) | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' })) | ||||
|   const editorRef = useRef<HTMLDivElement>(null) | ||||
|  | ||||
|   const { | ||||
|  | ||||
| @ -1,43 +0,0 @@ | ||||
| import { createActorContext } from '@xstate/react' | ||||
| import { editorManager } from 'lib/singletons' | ||||
| import { commandBarMachine } from 'machines/commandBarMachine' | ||||
| import { useEffect } from 'react' | ||||
|  | ||||
| export const CommandsContext = createActorContext( | ||||
|   commandBarMachine.provide({ | ||||
|     guards: { | ||||
|       'Command has no arguments': ({ context }) => { | ||||
|         return ( | ||||
|           !context.selectedCommand?.args || | ||||
|           Object.keys(context.selectedCommand?.args).length === 0 | ||||
|         ) | ||||
|       }, | ||||
|       'All arguments are skippable': ({ context }) => { | ||||
|         return Object.values(context.selectedCommand!.args!).every( | ||||
|           (argConfig) => argConfig.skip | ||||
|         ) | ||||
|       }, | ||||
|     }, | ||||
|   }) | ||||
| ) | ||||
|  | ||||
| export const CommandBarProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   return ( | ||||
|     <CommandsContext.Provider> | ||||
|       <CommandBarProviderInner>{children}</CommandBarProviderInner> | ||||
|     </CommandsContext.Provider> | ||||
|   ) | ||||
| } | ||||
| function CommandBarProviderInner({ children }: { children: React.ReactNode }) { | ||||
|   const commandBarActor = CommandsContext.useActorRef() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     editorManager.setCommandBarSend(commandBarActor.send) | ||||
|   }) | ||||
|  | ||||
|   return children | ||||
| } | ||||
| @ -1,9 +1,9 @@ | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import CommandBarHeader from './CommandBarHeader' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
|  | ||||
| function CommandBarReview({ stepBack }: { stepBack: () => void }) { | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const { | ||||
|     context: { argumentsToSubmit, selectedCommand }, | ||||
|   } = commandBarState | ||||
| @ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) { | ||||
|           parseInt(b.keys[0], 10) - 1 | ||||
|         ] | ||||
|         const arg = selectedCommand?.args[argName] | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Edit argument', | ||||
|           data: { arg: { ...arg, name: argName } }, | ||||
|         }) | ||||
| @ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) { | ||||
|  | ||||
|   function submitCommand(e: React.FormEvent<HTMLFormElement>) { | ||||
|     e.preventDefault() | ||||
|     commandBarSend({ | ||||
|     commandBarActor.send({ | ||||
|       type: 'Submit command', | ||||
|       output: argumentsToSubmit, | ||||
|     }) | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { useSelector } from '@xstate/react' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { Artifact } from 'lang/std/artifactGraph' | ||||
| import { CommandArgument } from 'lib/commandTypes' | ||||
| import { | ||||
| @ -10,6 +9,7 @@ import { | ||||
| import { kclManager } from 'lib/singletons' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { toSync } from 'lib/utils' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import { modelingMachine } from 'machines/modelingMachine' | ||||
| import { useEffect, useMemo, useRef, useState } from 'react' | ||||
| import { StateFrom } from 'xstate' | ||||
| @ -17,7 +17,7 @@ import { StateFrom } from 'xstate' | ||||
| const semanticEntityNames: { | ||||
|   [key: string]: Array<Artifact['type'] | 'defaultPlane'> | ||||
| } = { | ||||
|   face: ['wall', 'cap', 'solid2D'], | ||||
|   face: ['wall', 'cap', 'solid2d'], | ||||
|   edge: ['segment', 'sweepEdge', 'edgeCutEdge'], | ||||
|   point: [], | ||||
|   plane: ['defaultPlane'], | ||||
| @ -49,7 +49,7 @@ function CommandBarSelectionInput({ | ||||
|   onSubmit: (data: unknown) => void | ||||
| }) { | ||||
|   const inputRef = useRef<HTMLInputElement>(null) | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const [hasSubmitted, setHasSubmitted] = useState(false) | ||||
|   const selection = useSelector(arg.machineActor, selectionSelector) | ||||
|   const selectionsByType = useMemo(() => { | ||||
| @ -145,7 +145,7 @@ function CommandBarSelectionInput({ | ||||
|             if (event.key === 'Backspace') { | ||||
|               stepBack() | ||||
|             } else if (event.key === 'Escape') { | ||||
|               commandBarSend({ type: 'Close' }) | ||||
|               commandBarActor.send({ type: 'Close' }) | ||||
|             } | ||||
|           }} | ||||
|           onChange={handleChange} | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CommandArgument } from 'lib/commandTypes' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import { RefObject, useEffect, useRef } from 'react' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
|  | ||||
| @ -15,8 +15,8 @@ function CommandBarTextareaInput({ | ||||
|   stepBack: () => void | ||||
|   onSubmit: (event: unknown) => void | ||||
| }) { | ||||
|   const { commandBarSend, commandBarState } = useCommandsContext() | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) | ||||
|   const commandBarState = useCommandBarState() | ||||
|   useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' })) | ||||
|   const formRef = useRef<HTMLFormElement>(null) | ||||
|   const inputRef = useRef<HTMLTextAreaElement>(null) | ||||
|   useTextareaAutoGrow(inputRef) | ||||
|  | ||||
| @ -1,16 +1,15 @@ | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import usePlatform from 'hooks/usePlatform' | ||||
| import { hotkeyDisplay } from 'lib/hotkeyWrapper' | ||||
| import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| export function CommandBarOpenButton() { | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const platform = usePlatform() | ||||
|  | ||||
|   return ( | ||||
|     <button | ||||
|       className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit" | ||||
|       onClick={() => commandBarSend({ type: 'Open' })} | ||||
|       onClick={() => commandBarActor.send({ type: 'Open' })} | ||||
|       data-testid="command-bar-open-button" | ||||
|     > | ||||
|       <span>Commands</span> | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import { Combobox } from '@headlessui/react' | ||||
| import Fuse from 'fuse.js' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { Command } from 'lib/commandTypes' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import { getActorNextEvents } from 'lib/utils' | ||||
| import { sortCommands } from 'lib/commandUtils' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| function CommandComboBox({ | ||||
|   options, | ||||
| @ -14,7 +14,6 @@ function CommandComboBox({ | ||||
|   options: Command[] | ||||
|   placeholder?: string | ||||
| }) { | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const [query, setQuery] = useState('') | ||||
|   const [filteredOptions, setFilteredOptions] = useState<typeof options>() | ||||
|  | ||||
| @ -41,7 +40,7 @@ function CommandComboBox({ | ||||
|   }, [query]) | ||||
|  | ||||
|   function handleSelection(command: Command) { | ||||
|     commandBarSend({ type: 'Select command', data: { command } }) | ||||
|     commandBarActor.send({ type: 'Select command', data: { command } }) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
| @ -52,6 +51,7 @@ function CommandComboBox({ | ||||
|           className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit" | ||||
|         /> | ||||
|         <Combobox.Input | ||||
|           data-testid="cmd-bar-search" | ||||
|           onChange={(event) => setQuery(event.target.value)} | ||||
|           className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none" | ||||
|           onKeyDown={(event) => { | ||||
| @ -60,7 +60,7 @@ function CommandComboBox({ | ||||
|               (event.key === 'Backspace' && !event.currentTarget.value) | ||||
|             ) { | ||||
|               event.preventDefault() | ||||
|               commandBarSend({ type: 'Close' }) | ||||
|               commandBarActor.send({ type: 'Close' }) | ||||
|             } | ||||
|           }} | ||||
|           placeholder={ | ||||
| @ -85,6 +85,7 @@ function CommandComboBox({ | ||||
|             value={option} | ||||
|             className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50" | ||||
|             disabled={optionIsDisabled(option)} | ||||
|             data-testid={`cmd-bar-option`} | ||||
|           > | ||||
|             {'icon' in option && option.icon && ( | ||||
|               <CustomIcon name={option.icon} className="w-5 h-5" /> | ||||
|  | ||||
| @ -1,24 +1,21 @@ | ||||
| import { useMemo } from 'react' | ||||
| import { engineCommandManager } from 'lib/singletons' | ||||
| import { | ||||
|   ArtifactGraph, | ||||
|   expandPlane, | ||||
|   PlaneArtifactRich, | ||||
| } from 'lang/std/artifactGraph' | ||||
| import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph' | ||||
| import { ArtifactGraph } from 'lang/wasm' | ||||
| import { DebugDisplayArray, GenericObj } from './DebugDisplayObj' | ||||
| 
 | ||||
| export function DebugFeatureTree() { | ||||
|   const featureTree = useMemo(() => { | ||||
| export function DebugArtifactGraph() { | ||||
|   const artifactGraphTree = useMemo(() => { | ||||
|     return computeTree(engineCommandManager.artifactGraph) | ||||
|   }, [engineCommandManager.artifactGraph]) | ||||
| 
 | ||||
|   const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode'] | ||||
|   return ( | ||||
|     <details data-testid="debug-feature-tree" className="relative"> | ||||
|       <summary>Feature Tree</summary> | ||||
|       {featureTree.length > 0 ? ( | ||||
|       <summary>Artifact Graph</summary> | ||||
|       {artifactGraphTree.length > 0 ? ( | ||||
|         <pre className="text-xs"> | ||||
|           <DebugDisplayArray arr={featureTree} filterKeys={filterKeys} /> | ||||
|           <DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} /> | ||||
|         </pre> | ||||
|       ) : ( | ||||
|         <p>(Empty)</p> | ||||
| @ -12,7 +12,6 @@ import { | ||||
|   StateFrom, | ||||
|   fromPromise, | ||||
| } from 'xstate' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { fileMachine } from 'machines/fileMachine' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { | ||||
| @ -30,6 +29,7 @@ import { | ||||
| } from 'lib/getKclSamplesManifest' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { markOnce } from 'lib/performance' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -47,7 +47,6 @@ export const FileMachineProvider = ({ | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   const navigate = useNavigate() | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const { settings } = useSettingsAuthContext() | ||||
|   const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData | ||||
|   const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>( | ||||
| @ -90,7 +89,7 @@ export const FileMachineProvider = ({ | ||||
|         navigateToFile: ({ context, event }) => { | ||||
|           if (event.type !== 'xstate.done.actor.create-and-open-file') return | ||||
|           if (event.output && 'name' in event.output) { | ||||
|             commandBarSend({ type: 'Close' }) | ||||
|             commandBarActor.send({ type: 'Close' }) | ||||
|             navigate( | ||||
|               `..${PATHS.FILE}/${encodeURIComponent( | ||||
|                 context.selectedDirectory + | ||||
| @ -336,15 +335,18 @@ export const FileMachineProvider = ({ | ||||
|   ) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } }) | ||||
|     commandBarActor.send({ | ||||
|       type: 'Add commands', | ||||
|       data: { commands: kclCommandMemo }, | ||||
|     }) | ||||
|  | ||||
|     return () => { | ||||
|       commandBarSend({ | ||||
|       commandBarActor.send({ | ||||
|         type: 'Remove commands', | ||||
|         data: { commands: kclCommandMemo }, | ||||
|       }) | ||||
|     } | ||||
|   }, [commandBarSend, kclCommandMemo]) | ||||
|   }, [commandBarActor.send, kclCommandMemo]) | ||||
|  | ||||
|   return ( | ||||
|     <FileContext.Provider | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import { createContext, useEffect, useState } from 'react' | ||||
|  | ||||
| import { engineCommandManager } from 'lib/singletons' | ||||
| import { CommandsContext } from 'components/CommandBar/CommandBarProvider' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { components } from 'lib/machine-api' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { toSync } from 'lib/utils' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| export type MachinesListing = Array< | ||||
|   components['schemas']['MachineInfoResponse'] | ||||
| @ -42,8 +42,6 @@ export const MachineManagerProvider = ({ | ||||
|     components['schemas']['MachineInfoResponse'] | null | ||||
|   >(null) | ||||
|  | ||||
|   const commandBarActor = CommandsContext.useActorRef() | ||||
|  | ||||
|   // Get the reason message for why there are no machines. | ||||
|   const noMachinesReason = (): string | undefined => { | ||||
|     if (machines.length > 0) { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useMachine } from '@xstate/react' | ||||
| import { useMachine, useSelector } from '@xstate/react' | ||||
| import React, { | ||||
|   createContext, | ||||
|   useEffect, | ||||
| @ -11,6 +11,7 @@ import { | ||||
|   AnyStateMachine, | ||||
|   ContextFrom, | ||||
|   Prop, | ||||
|   SnapshotFrom, | ||||
|   StateFrom, | ||||
|   assign, | ||||
|   fromPromise, | ||||
| @ -78,7 +79,6 @@ import toast from 'react-hot-toast' | ||||
| import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||
| import { err, reportRejection, trap } from 'lib/trap' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { | ||||
|   ExportIntent, | ||||
|   EngineConnectionStateType, | ||||
| @ -91,6 +91,7 @@ import { IndexLoaderData } from 'lib/types' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { promptToEditFlow } from 'lib/promptToEdit' | ||||
| import { kclEditorActor } from 'machines/kclEditorMachine' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -102,6 +103,10 @@ export const ModelingMachineContext = createContext( | ||||
|   {} as MachineContext<typeof modelingMachine> | ||||
| ) | ||||
|  | ||||
| const commandBarIsClosedSelector = ( | ||||
|   state: SnapshotFrom<typeof commandBarActor> | ||||
| ) => state.matches('Closed') | ||||
|  | ||||
| export const ModelingMachineProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
| @ -132,8 +137,10 @@ export const ModelingMachineProvider = ({ | ||||
|   let [searchParams] = useSearchParams() | ||||
|   const pool = searchParams.get('pool') | ||||
|  | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|  | ||||
|   const isCommandBarClosed = useSelector( | ||||
|     commandBarActor, | ||||
|     commandBarIsClosedSelector | ||||
|   ) | ||||
|   // Settings machine setup | ||||
|   // const retrievedSettings = useRef( | ||||
|   // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}' | ||||
| @ -388,7 +395,16 @@ export const ModelingMachineProvider = ({ | ||||
|             } | ||||
|  | ||||
|             if (setSelections.selectionType === 'completeSelection') { | ||||
|               editorManager.selectRange(setSelections.selection) | ||||
|               const codeMirrorSelection = editorManager.createEditorSelection( | ||||
|                 setSelections.selection | ||||
|               ) | ||||
|               kclEditorActor.send({ | ||||
|                 type: 'setLastSelectionEvent', | ||||
|                 data: { | ||||
|                   codeMirrorSelection, | ||||
|                   scrollIntoView: false, | ||||
|                 }, | ||||
|               }) | ||||
|               if (!sketchDetails) | ||||
|                 return { | ||||
|                   selectionRanges: setSelections.selection, | ||||
| @ -529,7 +545,6 @@ export const ModelingMachineProvider = ({ | ||||
|             trimmedPrompt, | ||||
|             fileMachineSend, | ||||
|             navigate, | ||||
|             commandBarSend, | ||||
|             context, | ||||
|             token, | ||||
|             settings: { | ||||
| @ -543,7 +558,7 @@ export const ModelingMachineProvider = ({ | ||||
|         'has valid selection for deletion': ({ | ||||
|           context: { selectionRanges }, | ||||
|         }) => { | ||||
|           if (!commandBarState.matches('Closed')) return false | ||||
|           if (!isCommandBarClosed) return false | ||||
|           if (selectionRanges.graphSelections.length <= 0) return false | ||||
|           return true | ||||
|         }, | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { DebugFeatureTree } from 'components/DebugFeatureTree' | ||||
| import { DebugArtifactGraph } from 'components/DebugArtifactGraph' | ||||
| import { AstExplorer } from '../../AstExplorer' | ||||
| import { EngineCommands } from '../../EngineCommands' | ||||
| import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp' | ||||
| @ -14,7 +14,7 @@ export const DebugPane = () => { | ||||
|           <EngineCommands /> | ||||
|           <CamDebugSettings /> | ||||
|           <AstExplorer /> | ||||
|           <DebugFeatureTree /> | ||||
|           <DebugArtifactGraph /> | ||||
|         </div> | ||||
|       </section> | ||||
|     </div> | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
|   @apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90; | ||||
|   @apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit; | ||||
|   @apply transition-colors ease-out; | ||||
|   @apply m-0; | ||||
| } | ||||
|  | ||||
| :global(.dark) .button { | ||||
|  | ||||
| @ -9,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||
| import { kclManager } from 'lib/singletons' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| export const KclEditorMenu = ({ children }: PropsWithChildren) => { | ||||
|   const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = | ||||
|     useConvertToVariable() | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|  | ||||
|   return ( | ||||
|     <Menu> | ||||
| @ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => { | ||||
|           <Menu.Item> | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 commandBarSend({ | ||||
|                 commandBarActor.send({ | ||||
|                   type: 'Find and select command', | ||||
|                   data: { | ||||
|                     groupId: 'code', | ||||
|  | ||||
| @ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => { | ||||
|     ) { | ||||
|       const sk = sketchFromKclValueOptional(val, key) | ||||
|       if (val.type === 'Solid') { | ||||
|         processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => { | ||||
|           return rest | ||||
|         }) | ||||
|         processedMemory[key] = val.value.value.map( | ||||
|           ({ ...rest }: ExtrudeSurface) => { | ||||
|             return rest | ||||
|           } | ||||
|         ) | ||||
|       } else if (!(sk instanceof Reason)) { | ||||
|         processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => { | ||||
|           return rest | ||||
|  | ||||
| @ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { CustomIconName } from 'components/CustomIcon' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { IconDefinition } from '@fortawesome/free-solid-svg-icons' | ||||
| import { useKclContext } from 'lang/KclProvider' | ||||
| import { MachineManagerContext } from 'components/MachineManagerProvider' | ||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | ||||
| import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| interface ModelingSidebarProps { | ||||
|   paneOpacity: '' | 'opacity-20' | 'opacity-40' | ||||
| @ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' { | ||||
|  | ||||
| export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { | ||||
|   const machineManager = useContext(MachineManagerContext) | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const kclContext = useKclContext() | ||||
|   const { settings } = useSettingsAuthContext() | ||||
|   const onboardingStatus = settings.context.app.onboardingStatus | ||||
| @ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { | ||||
|       icon: 'floppyDiskArrow', | ||||
|       keybinding: 'Ctrl + Shift + E', | ||||
|       action: () => | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Find and select command', | ||||
|           data: { name: 'Export', groupId: 'modeling' }, | ||||
|         }), | ||||
| @ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { | ||||
|       keybinding: 'Ctrl + Shift + M', | ||||
|       // eslint-disable-next-line @typescript-eslint/no-misused-promises | ||||
|       action: async () => { | ||||
|         commandBarSend({ | ||||
|         commandBarActor.send({ | ||||
|           type: 'Find and select command', | ||||
|           data: { name: 'Make', groupId: 'modeling' }, | ||||
|         }) | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import { fireEvent, render, screen } from '@testing-library/react' | ||||
| import { BrowserRouter } from 'react-router-dom' | ||||
| import { SettingsAuthProviderJest } from './SettingsAuthProvider' | ||||
| import { CommandBarProvider } from './CommandBar/CommandBarProvider' | ||||
| import { | ||||
|   NETWORK_HEALTH_TEXT, | ||||
|   NetworkHealthIndicator, | ||||
| @ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { | ||||
|   // wrap in router and xState context | ||||
|   return ( | ||||
|     <BrowserRouter> | ||||
|       <CommandBarProvider> | ||||
|         <SettingsAuthProviderJest>{children}</SettingsAuthProviderJest> | ||||
|       </CommandBarProvider> | ||||
|       <SettingsAuthProviderJest>{children}</SettingsAuthProviderJest> | ||||
|     </BrowserRouter> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react' | ||||
| import { BrowserRouter } from 'react-router-dom' | ||||
| import ProjectSidebarMenu from './ProjectSidebarMenu' | ||||
| import { SettingsAuthProviderJest } from './SettingsAuthProvider' | ||||
| import { CommandBarProvider } from './CommandBar/CommandBarProvider' | ||||
| import { Project } from 'lib/project' | ||||
|  | ||||
| const now = new Date() | ||||
| @ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => { | ||||
|   test('Disables popover menu by default', () => { | ||||
|     render( | ||||
|       <BrowserRouter> | ||||
|         <CommandBarProvider> | ||||
|           <SettingsAuthProviderJest> | ||||
|             <ProjectSidebarMenu project={projectWellFormed} /> | ||||
|           </SettingsAuthProviderJest> | ||||
|         </CommandBarProvider> | ||||
|         <SettingsAuthProviderJest> | ||||
|           <ProjectSidebarMenu project={projectWellFormed} /> | ||||
|         </SettingsAuthProviderJest> | ||||
|       </BrowserRouter> | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' | ||||
| import { Fragment, useMemo, useContext } from 'react' | ||||
| import { Logo } from './Logo' | ||||
| import { APP_NAME } from 'lib/constants' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import { useLspContext } from './LspProvider' | ||||
| import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
| @ -15,6 +14,9 @@ import { MachineManagerContext } from 'components/MachineManagerProvider' | ||||
| import usePlatform from 'hooks/usePlatform' | ||||
| import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' | ||||
| import Tooltip from './Tooltip' | ||||
| import { SnapshotFrom } from 'xstate' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
| import { useSelector } from '@xstate/react' | ||||
|  | ||||
| const ProjectSidebarMenu = ({ | ||||
|   project, | ||||
| @ -84,6 +86,9 @@ function AppLogoLink({ | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) => | ||||
|   state.context.commands | ||||
|  | ||||
| function ProjectMenuPopover({ | ||||
|   project, | ||||
|   file, | ||||
| @ -96,16 +101,14 @@ function ProjectMenuPopover({ | ||||
|   const navigate = useNavigate() | ||||
|   const filePath = useAbsoluteFilePath() | ||||
|   const machineManager = useContext(MachineManagerContext) | ||||
|   const commands = useSelector(commandBarActor, commandsSelector) | ||||
|  | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const { onProjectClose } = useLspContext() | ||||
|   const exportCommandInfo = { name: 'Export', groupId: 'modeling' } | ||||
|   const makeCommandInfo = { name: 'Make', groupId: 'modeling' } | ||||
|   const findCommand = (obj: { name: string; groupId: string }) => | ||||
|     Boolean( | ||||
|       commandBarState.context.commands.find( | ||||
|         (c) => c.name === obj.name && c.groupId === obj.groupId | ||||
|       ) | ||||
|       commands.find((c) => c.name === obj.name && c.groupId === obj.groupId) | ||||
|     ) | ||||
|   const machineCount = machineManager.machines.length | ||||
|  | ||||
| @ -150,7 +153,7 @@ function ProjectMenuPopover({ | ||||
|           ), | ||||
|           disabled: !findCommand(exportCommandInfo), | ||||
|           onClick: () => | ||||
|             commandBarSend({ | ||||
|             commandBarActor.send({ | ||||
|               type: 'Find and select command', | ||||
|               data: exportCommandInfo, | ||||
|             }), | ||||
| @ -175,7 +178,7 @@ function ProjectMenuPopover({ | ||||
|           ), | ||||
|           disabled: !findCommand(makeCommandInfo) || machineCount === 0, | ||||
|           onClick: () => { | ||||
|             commandBarSend({ | ||||
|             commandBarActor.send({ | ||||
|               type: 'Find and select command', | ||||
|               data: makeCommandInfo, | ||||
|             }) | ||||
| @ -200,7 +203,7 @@ function ProjectMenuPopover({ | ||||
|     [ | ||||
|       platform, | ||||
|       findCommand, | ||||
|       commandBarSend, | ||||
|       commandBarActor.send, | ||||
|       engineCommandManager, | ||||
|       onProjectClose, | ||||
|       isDesktop, | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { useMachine } from '@xstate/react' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' | ||||
| import { useProjectsLoader } from 'hooks/useProjectsLoader' | ||||
| import { projectsMachine } from 'machines/projectsMachine' | ||||
| @ -18,11 +17,13 @@ import { | ||||
|   getNextProjectIndex, | ||||
|   interpolateProjectNameWithIndex, | ||||
|   doesProjectNameNeedInterpolated, | ||||
|   getUniqueProjectName, | ||||
| } from 'lib/desktopFS' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import useStateMachineCommands from 'hooks/useStateMachineCommands' | ||||
| import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state?: StateFrom<T> | ||||
| @ -72,7 +73,6 @@ const ProjectsContextDesktop = ({ | ||||
| }) => { | ||||
|   const navigate = useNavigate() | ||||
|   const location = useLocation() | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const { onProjectOpen } = useLspContext() | ||||
|   const { | ||||
|     settings: { context: settings }, | ||||
| @ -125,7 +125,7 @@ const ProjectsContextDesktop = ({ | ||||
|               }, | ||||
|               null | ||||
|             ) | ||||
|             commandBarSend({ type: 'Close' }) | ||||
|             commandBarActor.send({ type: 'Close' }) | ||||
|             const newPathName = `${PATHS.FILE}/${encodeURIComponent( | ||||
|               projectPath | ||||
|             )}` | ||||
| @ -195,16 +195,12 @@ const ProjectsContextDesktop = ({ | ||||
|               : settings.projects.defaultProjectName.current | ||||
|           ).trim() | ||||
|  | ||||
|           if (doesProjectNameNeedInterpolated(name)) { | ||||
|             const nextIndex = getNextProjectIndex(name, input.projects) | ||||
|             name = interpolateProjectNameWithIndex(name, nextIndex) | ||||
|           } | ||||
|  | ||||
|           await createNewProjectDirectory(name) | ||||
|           const uniqueName = getUniqueProjectName(name, input.projects) | ||||
|           await createNewProjectDirectory(uniqueName) | ||||
|  | ||||
|           return { | ||||
|             message: `Successfully created "${name}"`, | ||||
|             name, | ||||
|             message: `Successfully created "${uniqueName}"`, | ||||
|             name: uniqueName, | ||||
|           } | ||||
|         }), | ||||
|         renameProject: fromPromise(async ({ input }) => { | ||||
|  | ||||
| @ -29,7 +29,6 @@ import { | ||||
|   createSettingsCommand, | ||||
|   settingsWithCommandConfigs, | ||||
| } from 'lib/commandBarConfigs/settingsCommandConfig' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { Command } from 'lib/commandTypes' | ||||
| import { BaseUnit } from 'lib/settings/settingsTypes' | ||||
| import { | ||||
| @ -42,6 +41,7 @@ import { isDesktop } from 'lib/isDesktop' | ||||
| import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' | ||||
| import { codeManager } from 'lib/singletons' | ||||
| import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -109,7 +109,6 @@ export const SettingsAuthProviderBase = ({ | ||||
| }) => { | ||||
|   const location = useLocation() | ||||
|   const navigate = useNavigate() | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const [settingsPath, setSettingsPath] = useState<string | undefined>( | ||||
|     undefined | ||||
|   ) | ||||
| @ -278,10 +277,10 @@ export const SettingsAuthProviderBase = ({ | ||||
|       ) | ||||
|       .filter((c) => c !== null) as Command[] | ||||
|  | ||||
|     commandBarSend({ type: 'Add commands', data: { commands: commands } }) | ||||
|     commandBarActor.send({ type: 'Add commands', data: { commands: commands } }) | ||||
|  | ||||
|     return () => { | ||||
|       commandBarSend({ | ||||
|       commandBarActor.send({ | ||||
|         type: 'Remove commands', | ||||
|         data: { commands }, | ||||
|       }) | ||||
| @ -290,7 +289,7 @@ export const SettingsAuthProviderBase = ({ | ||||
|     settingsState, | ||||
|     settingsSend, | ||||
|     settingsActor, | ||||
|     commandBarSend, | ||||
|     commandBarActor.send, | ||||
|     settingsWithCommandConfigs, | ||||
|   ]) | ||||
|  | ||||
| @ -303,7 +302,7 @@ export const SettingsAuthProviderBase = ({ | ||||
|       encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH) | ||||
|     const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } = | ||||
|       createRouteCommands(navigate, location, filePath) | ||||
|     commandBarSend({ | ||||
|     commandBarActor.send({ | ||||
|       type: 'Remove commands', | ||||
|       data: { | ||||
|         commands: [ | ||||
| @ -314,12 +313,12 @@ export const SettingsAuthProviderBase = ({ | ||||
|       }, | ||||
|     }) | ||||
|     if (location.pathname === PATHS.HOME) { | ||||
|       commandBarSend({ | ||||
|       commandBarActor.send({ | ||||
|         type: 'Add commands', | ||||
|         data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] }, | ||||
|       }) | ||||
|     } else if (location.pathname.includes(PATHS.FILE)) { | ||||
|       commandBarSend({ | ||||
|       commandBarActor.send({ | ||||
|         type: 'Add commands', | ||||
|         data: { | ||||
|           commands: [ | ||||
|  | ||||
| @ -17,10 +17,11 @@ import { | ||||
| import { useRouteLoaderData } from 'react-router-dom' | ||||
| import { PATHS } from 'lib/paths' | ||||
| import { IndexLoaderData } from 'lib/types' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { err, reportRejection } from 'lib/trap' | ||||
| import { getArtifactOfTypes } from 'lang/std/artifactGraph' | ||||
| import { ViewControlContextMenu } from './ViewControlMenu' | ||||
| import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' | ||||
| import { useSelector } from '@xstate/react' | ||||
|  | ||||
| enum StreamState { | ||||
|   Playing = 'playing', | ||||
| @ -35,7 +36,7 @@ export const Stream = () => { | ||||
|   const videoRef = useRef<HTMLVideoElement>(null) | ||||
|   const { settings } = useSettingsAuthContext() | ||||
|   const { state, send } = useModelingContext() | ||||
|   const { commandBarState } = useCommandsContext() | ||||
|   const commandBarState = useCommandBarState() | ||||
|   const { mediaStream } = useAppStream() | ||||
|   const { overallState, immediateState } = useNetworkContext() | ||||
|   const [streamState, setStreamState] = useState(StreamState.Unset) | ||||
| @ -301,7 +302,7 @@ export const Stream = () => { | ||||
|           return | ||||
|         } | ||||
|         const path = getArtifactOfTypes( | ||||
|           { key: entity_id, types: ['path', 'solid2D', 'segment'] }, | ||||
|           { key: entity_id, types: ['path', 'solid2d', 'segment'] }, | ||||
|           engineCommandManager.artifactGraph | ||||
|         ) | ||||
|         if (err(path)) { | ||||
|  | ||||
| @ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm' | ||||
| import { sendTelemetry } from 'lib/textToCad' | ||||
| import { Themes } from 'lib/theme' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { commandBarMachine } from 'machines/commandBarMachine' | ||||
| import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine' | ||||
| import { EventFrom } from 'xstate' | ||||
| import { fileMachine } from 'machines/fileMachine' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| @ -43,15 +43,10 @@ export function ToastTextToCadError({ | ||||
|   toastId, | ||||
|   message, | ||||
|   prompt, | ||||
|   commandBarSend, | ||||
| }: { | ||||
|   toastId: string | ||||
|   message: string | ||||
|   prompt: string | ||||
|   commandBarSend: ( | ||||
|     event: EventFrom<typeof commandBarMachine>, | ||||
|     data?: unknown | ||||
|   ) => void | ||||
| }) { | ||||
|   return ( | ||||
|     <div className="flex flex-col justify-between gap-6"> | ||||
| @ -81,7 +76,7 @@ export function ToastTextToCadError({ | ||||
|           }} | ||||
|           name="Edit prompt" | ||||
|           onClick={() => { | ||||
|             commandBarSend({ | ||||
|             commandBarActor.send({ | ||||
|               type: 'Find and select command', | ||||
|               data: { | ||||
|                 groupId: 'modeling', | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { toolTips } from 'lang/langHelpers' | ||||
| import { Selection, Selections } from 'lib/selections' | ||||
| import { PathToNode, Program, Expr } from '../../lang/wasm' | ||||
| import { PathToNode, Program, Expr, topLevelRange } from '../../lang/wasm' | ||||
| import { getNodeFromPath } from '../../lang/queryAst' | ||||
| import { | ||||
|   PathToNodeMap, | ||||
| @ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({ | ||||
|         graphSelections: nodes.map( | ||||
|           (node): Selection => ({ | ||||
|             codeRef: codeRefFromRange( | ||||
|               [node.start, node.end, true], | ||||
|               topLevelRange(node.start, node.end), | ||||
|               kclManager.ast | ||||
|             ), | ||||
|           }) | ||||
|  | ||||
| @ -8,7 +8,6 @@ import { | ||||
| } from 'react-router-dom' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { SettingsAuthProviderJest } from './SettingsAuthProvider' | ||||
| import { CommandBarProvider } from './CommandBar/CommandBarProvider' | ||||
|  | ||||
| type User = Models['User_type'] | ||||
|  | ||||
| @ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { | ||||
|       <Route | ||||
|         path="/file/:id" | ||||
|         element={ | ||||
|           <CommandBarProvider> | ||||
|             <SettingsAuthProviderJest>{children}</SettingsAuthProviderJest> | ||||
|           </CommandBarProvider> | ||||
|           <SettingsAuthProviderJest>{children}</SettingsAuthProviderJest> | ||||
|         } | ||||
|       /> | ||||
|     ), | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
| import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine' | ||||
| import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections' | ||||
| import { undo, redo } from '@codemirror/commands' | ||||
| import { CommandBarMachineEvent } from 'machines/commandBarMachine' | ||||
| import { addLineHighlight, addLineHighlightEvent } from './highlightextension' | ||||
| import { | ||||
|   Diagnostic, | ||||
| @ -52,9 +51,6 @@ export default class EditorManager { | ||||
|   private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} | ||||
|   private _modelingState: StateFrom<typeof modelingMachine> | null = null | ||||
|  | ||||
|   private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void = | ||||
|     () => {} | ||||
|  | ||||
|   private _convertToVariableEnabled: boolean = false | ||||
|   private _convertToVariableCallback: () => void = () => {} | ||||
|  | ||||
| @ -161,14 +157,6 @@ export default class EditorManager { | ||||
|     this._modelingState = state | ||||
|   } | ||||
|  | ||||
|   setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) { | ||||
|     this._commandBarSend = send | ||||
|   } | ||||
|  | ||||
|   commandBarSend(eventInfo: CommandBarMachineEvent): void { | ||||
|     return this._commandBarSend(eventInfo) | ||||
|   } | ||||
|  | ||||
|   get highlightRange(): Array<[number, number]> { | ||||
|     return this._highlightRange | ||||
|   } | ||||
| @ -315,6 +303,21 @@ export default class EditorManager { | ||||
|     if (selections?.graphSelections?.length === 0) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     if (!this._editorView) { | ||||
|       return | ||||
|     } | ||||
|     const codeBaseSelections = this.createEditorSelection(selections) | ||||
|     this._editorView.dispatch({ | ||||
|       selection: codeBaseSelections, | ||||
|       annotations: [ | ||||
|         updateOutsideEditorEvent, | ||||
|         Transaction.addToHistory.of(false), | ||||
|       ], | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   createEditorSelection(selections: Selections) { | ||||
|     let codeBasedSelections = [] | ||||
|     for (const selection of selections.graphSelections) { | ||||
|       const safeEnd = Math.min( | ||||
| @ -331,18 +334,7 @@ export default class EditorManager { | ||||
|         .range[1] | ||||
|     const safeEnd = Math.min(end, this._editorView?.state.doc.length || end) | ||||
|     codeBasedSelections.push(EditorSelection.cursor(safeEnd)) | ||||
|  | ||||
|     if (!this._editorView) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     this._editorView.dispatch({ | ||||
|       selection: EditorSelection.create(codeBasedSelections, 1), | ||||
|       annotations: [ | ||||
|         updateOutsideEditorEvent, | ||||
|         Transaction.addToHistory.of(false), | ||||
|       ], | ||||
|     }) | ||||
|     return EditorSelection.create(codeBasedSelections, 1) | ||||
|   } | ||||
|  | ||||
|   // We will ONLY get here if the user called a select event. | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| import { CommandsContext } from 'components/CommandBar/CommandBarProvider' | ||||
|  | ||||
| export const useCommandsContext = () => { | ||||
|   const commandBarActor = CommandsContext.useActorRef() | ||||
|   const commandBarState = CommandsContext.useSelector((state) => state) | ||||
|   return { | ||||
|     commandBarSend: commandBarActor.send, | ||||
|     commandBarState, | ||||
|   } | ||||
| } | ||||
| @ -1,7 +1,6 @@ | ||||
| import { useEffect } from 'react' | ||||
| import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate' | ||||
| import { createMachineCommand } from '../lib/createMachineCommand' | ||||
| import { useCommandsContext } from './useCommandsContext' | ||||
| import { modelingMachine } from 'machines/modelingMachine' | ||||
| import { authMachine } from 'machines/authMachine' | ||||
| import { settingsMachine } from 'machines/settingsMachine' | ||||
| @ -15,6 +14,7 @@ import { useKclContext } from 'lang/KclProvider' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||
| import { useAppState } from 'AppState' | ||||
| import { commandBarActor } from 'machines/commandBarMachine' | ||||
|  | ||||
| // This might not be necessary, AnyStateMachine from xstate is working | ||||
| export type AllMachines = | ||||
| @ -48,7 +48,6 @@ export default function useStateMachineCommands< | ||||
|   allCommandsRequireNetwork = false, | ||||
|   onCancel, | ||||
| }: UseStateMachineCommandsArgs<T, S>) { | ||||
|   const { commandBarSend } = useCommandsContext() | ||||
|   const { overallState } = useNetworkContext() | ||||
|   const { isExecuting } = useKclContext() | ||||
|   const { isStreamReady } = useAppState() | ||||
| @ -76,10 +75,13 @@ export default function useStateMachineCommands< | ||||
|       }) | ||||
|       .filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls | ||||
|  | ||||
|     commandBarSend({ type: 'Add commands', data: { commands: newCommands } }) | ||||
|     commandBarActor.send({ | ||||
|       type: 'Add commands', | ||||
|       data: { commands: newCommands }, | ||||
|     }) | ||||
|  | ||||
|     return () => { | ||||
|       commandBarSend({ | ||||
|       commandBarActor.send({ | ||||
|         type: 'Remove commands', | ||||
|         data: { commands: newCommands }, | ||||
|       }) | ||||
|  | ||||
| @ -22,6 +22,7 @@ import { | ||||
|   ProgramMemory, | ||||
|   recast, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from 'lang/wasm' | ||||
| import { getNodeFromPath } from './queryAst' | ||||
| import { codeManager, editorManager, sceneInfra } from 'lib/singletons' | ||||
| @ -376,11 +377,7 @@ export class KclManager { | ||||
|     } | ||||
|     this.ast = { ...ast } | ||||
|     // updateArtifactGraph relies on updated executeState/programMemory | ||||
|     await this.engineCommandManager.updateArtifactGraph( | ||||
|       this.ast, | ||||
|       execState.artifactCommands, | ||||
|       execState.artifacts | ||||
|     ) | ||||
|     this.engineCommandManager.updateArtifactGraph(execState.artifactGraph) | ||||
|     this._executeCallback() | ||||
|     if (!isInterrupted) { | ||||
|       sceneInfra.modelingSend({ type: 'code edit during sketch' }) | ||||
| @ -473,7 +470,7 @@ export class KclManager { | ||||
|           ...artifact, | ||||
|           codeRef: { | ||||
|             ...artifact.codeRef, | ||||
|             range: [node.start, node.end, true], | ||||
|             range: topLevelRange(node.start, node.end), | ||||
|           }, | ||||
|         }) | ||||
|       } | ||||
| @ -594,7 +591,7 @@ export class KclManager { | ||||
|         if (start && end) { | ||||
|           returnVal.graphSelections.push({ | ||||
|             codeRef: { | ||||
|               range: [start, end, true], | ||||
|               range: topLevelRange(start, end), | ||||
|               pathToNode: path, | ||||
|             }, | ||||
|           }) | ||||
|  | ||||
| @ -24,7 +24,10 @@ describe('testing AST', () => { | ||||
|             type: 'Literal', | ||||
|             start: 0, | ||||
|             end: 1, | ||||
|             value: 5, | ||||
|             value: { | ||||
|               suffix: 'None', | ||||
|               value: 5, | ||||
|             }, | ||||
|             raw: '5', | ||||
|           }, | ||||
|           operator: '+', | ||||
| @ -32,7 +35,10 @@ describe('testing AST', () => { | ||||
|             type: 'Literal', | ||||
|             start: 3, | ||||
|             end: 4, | ||||
|             value: 6, | ||||
|             value: { | ||||
|               suffix: 'None', | ||||
|               value: 6, | ||||
|             }, | ||||
|             raw: '6', | ||||
|           }, | ||||
|         }, | ||||
|  | ||||
| @ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY') | ||||
|           }, | ||||
|         ], | ||||
|         id: expect.any(String), | ||||
|         units: { | ||||
|           type: 'Mm', | ||||
|         }, | ||||
|         __meta: [{ sourceRange: [46, 71, 0] }], | ||||
|       }, | ||||
|     }) | ||||
| @ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY') | ||||
|     const sketch001 = execState.memory.get('mySketch001') | ||||
|     expect(sketch001).toEqual({ | ||||
|       type: 'Solid', | ||||
|       id: expect.any(String), | ||||
|       value: [ | ||||
|         { | ||||
|           type: 'extrudePlane', | ||||
|           faceId: expect.any(String), | ||||
|           tag: null, | ||||
|           id: expect.any(String), | ||||
|           sourceRange: [77, 102, 0], | ||||
|         }, | ||||
|         { | ||||
|           type: 'extrudePlane', | ||||
|           faceId: expect.any(String), | ||||
|           tag: null, | ||||
|           id: expect.any(String), | ||||
|           sourceRange: [108, 132, 0], | ||||
|         }, | ||||
|       ], | ||||
|       sketch: { | ||||
|       value: { | ||||
|         type: 'Solid', | ||||
|         id: expect.any(String), | ||||
|         __meta: expect.any(Array), | ||||
|         on: expect.any(Object), | ||||
|         start: expect.any(Object), | ||||
|         type: 'Sketch', | ||||
|         paths: [ | ||||
|         value: [ | ||||
|           { | ||||
|             type: 'ToPoint', | ||||
|             from: [0, 0], | ||||
|             to: [-1.59, -1.54], | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             __geoMeta: { | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [77, 102, 0], | ||||
|             }, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [77, 102, 0], | ||||
|           }, | ||||
|           { | ||||
|             type: 'ToPoint', | ||||
|             from: [-1.59, -1.54], | ||||
|             to: [0.46, -5.82], | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             __geoMeta: { | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [108, 132, 0], | ||||
|             }, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [108, 132, 0], | ||||
|           }, | ||||
|         ], | ||||
|         sketch: { | ||||
|           id: expect.any(String), | ||||
|           units: { | ||||
|             type: 'Mm', | ||||
|           }, | ||||
|           __meta: expect.any(Array), | ||||
|           on: expect.any(Object), | ||||
|           start: expect.any(Object), | ||||
|           type: 'Sketch', | ||||
|           paths: [ | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [0, 0], | ||||
|               to: [-1.59, -1.54], | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [77, 102, 0], | ||||
|               }, | ||||
|             }, | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [-1.59, -1.54], | ||||
|               to: [0.46, -5.82], | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [108, 132, 0], | ||||
|               }, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|         height: 2, | ||||
|         startCapId: expect.any(String), | ||||
|         endCapId: expect.any(String), | ||||
|         units: { | ||||
|           type: 'Mm', | ||||
|         }, | ||||
|         __meta: [{ sourceRange: [46, 71, 0] }], | ||||
|       }, | ||||
|       height: 2, | ||||
|       startCapId: expect.any(String), | ||||
|       endCapId: expect.any(String), | ||||
|       __meta: [{ sourceRange: [46, 71, 0] }], | ||||
|     }) | ||||
|   }) | ||||
|   test('sketch extrude and sketch on one of the faces', async () => { | ||||
| @ -154,187 +166,205 @@ const sk2 = startSketchOn('XY') | ||||
|     expect(geos).toEqual([ | ||||
|       { | ||||
|         type: 'Solid', | ||||
|         id: expect.any(String), | ||||
|         value: [ | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [69, 89, 0], | ||||
|           }, | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: { | ||||
|               end: 116, | ||||
|               start: 114, | ||||
|               type: 'TagDeclarator', | ||||
|               value: 'p', | ||||
|             }, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [95, 117, 0], | ||||
|           }, | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [123, 142, 0], | ||||
|           }, | ||||
|         ], | ||||
|         sketch: { | ||||
|         value: { | ||||
|           type: 'Solid', | ||||
|           id: expect.any(String), | ||||
|           __meta: expect.any(Array), | ||||
|           on: expect.any(Object), | ||||
|           start: expect.any(Object), | ||||
|           type: 'Sketch', | ||||
|           tags: { | ||||
|             p: { | ||||
|               __meta: [ | ||||
|                 { | ||||
|                   sourceRange: [114, 116, 0], | ||||
|                 }, | ||||
|               ], | ||||
|               type: 'TagIdentifier', | ||||
|               value: 'p', | ||||
|               info: expect.any(Object), | ||||
|             }, | ||||
|           }, | ||||
|           paths: [ | ||||
|           value: [ | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [0, 0], | ||||
|               to: [-2.5, 0], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [69, 89, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [69, 89, 0], | ||||
|             }, | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [-2.5, 0], | ||||
|               to: [0, 10], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: { | ||||
|                 end: 116, | ||||
|                 start: 114, | ||||
|                 type: 'TagDeclarator', | ||||
|                 value: 'p', | ||||
|               }, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [95, 117, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [95, 117, 0], | ||||
|             }, | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [0, 10], | ||||
|               to: [2.5, 0], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [123, 142, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [123, 142, 0], | ||||
|             }, | ||||
|           ], | ||||
|           sketch: { | ||||
|             id: expect.any(String), | ||||
|             __meta: expect.any(Array), | ||||
|             on: expect.any(Object), | ||||
|             start: expect.any(Object), | ||||
|             type: 'Sketch', | ||||
|             units: { | ||||
|               type: 'Mm', | ||||
|             }, | ||||
|             tags: { | ||||
|               p: { | ||||
|                 __meta: [ | ||||
|                   { | ||||
|                     sourceRange: [114, 116, 0], | ||||
|                   }, | ||||
|                 ], | ||||
|                 type: 'TagIdentifier', | ||||
|                 value: 'p', | ||||
|                 info: expect.any(Object), | ||||
|               }, | ||||
|             }, | ||||
|             paths: [ | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [0, 0], | ||||
|                 to: [-2.5, 0], | ||||
|                 tag: null, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [69, 89, 0], | ||||
|                 }, | ||||
|               }, | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [-2.5, 0], | ||||
|                 to: [0, 10], | ||||
|                 tag: { | ||||
|                   end: 116, | ||||
|                   start: 114, | ||||
|                   type: 'TagDeclarator', | ||||
|                   value: 'p', | ||||
|                 }, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [95, 117, 0], | ||||
|                 }, | ||||
|               }, | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [0, 10], | ||||
|                 to: [2.5, 0], | ||||
|                 tag: null, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [123, 142, 0], | ||||
|                 }, | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|           height: 2, | ||||
|           startCapId: expect.any(String), | ||||
|           endCapId: expect.any(String), | ||||
|           units: { | ||||
|             type: 'Mm', | ||||
|           }, | ||||
|           __meta: [{ sourceRange: [38, 63, 0] }], | ||||
|         }, | ||||
|         height: 2, | ||||
|         startCapId: expect.any(String), | ||||
|         endCapId: expect.any(String), | ||||
|         __meta: [{ sourceRange: [38, 63, 0] }], | ||||
|       }, | ||||
|       { | ||||
|         type: 'Solid', | ||||
|         id: expect.any(String), | ||||
|         value: [ | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [373, 393, 0], | ||||
|           }, | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: { | ||||
|               end: 419, | ||||
|               start: 417, | ||||
|               type: 'TagDeclarator', | ||||
|               value: 'o', | ||||
|             }, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [399, 420, 0], | ||||
|           }, | ||||
|           { | ||||
|             type: 'extrudePlane', | ||||
|             faceId: expect.any(String), | ||||
|             tag: null, | ||||
|             id: expect.any(String), | ||||
|             sourceRange: [426, 445, 0], | ||||
|           }, | ||||
|         ], | ||||
|         sketch: { | ||||
|         value: { | ||||
|           type: 'Solid', | ||||
|           id: expect.any(String), | ||||
|           __meta: expect.any(Array), | ||||
|           on: expect.any(Object), | ||||
|           start: expect.any(Object), | ||||
|           type: 'Sketch', | ||||
|           tags: { | ||||
|             o: { | ||||
|               __meta: [ | ||||
|                 { | ||||
|                   sourceRange: [417, 419, 0], | ||||
|                 }, | ||||
|               ], | ||||
|               type: 'TagIdentifier', | ||||
|               value: 'o', | ||||
|               info: expect.any(Object), | ||||
|             }, | ||||
|           }, | ||||
|           paths: [ | ||||
|           value: [ | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [0, 0], | ||||
|               to: [-2.5, 0], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [373, 393, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [373, 393, 0], | ||||
|             }, | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [-2.5, 0], | ||||
|               to: [0, 3], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: { | ||||
|                 end: 419, | ||||
|                 start: 417, | ||||
|                 type: 'TagDeclarator', | ||||
|                 value: 'o', | ||||
|               }, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [399, 420, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [399, 420, 0], | ||||
|             }, | ||||
|             { | ||||
|               type: 'ToPoint', | ||||
|               from: [0, 3], | ||||
|               to: [2.5, 0], | ||||
|               type: 'extrudePlane', | ||||
|               faceId: expect.any(String), | ||||
|               tag: null, | ||||
|               __geoMeta: { | ||||
|                 id: expect.any(String), | ||||
|                 sourceRange: [426, 445, 0], | ||||
|               }, | ||||
|               id: expect.any(String), | ||||
|               sourceRange: [426, 445, 0], | ||||
|             }, | ||||
|           ], | ||||
|           sketch: { | ||||
|             id: expect.any(String), | ||||
|             units: { | ||||
|               type: 'Mm', | ||||
|             }, | ||||
|             __meta: expect.any(Array), | ||||
|             on: expect.any(Object), | ||||
|             start: expect.any(Object), | ||||
|             type: 'Sketch', | ||||
|             tags: { | ||||
|               o: { | ||||
|                 __meta: [ | ||||
|                   { | ||||
|                     sourceRange: [417, 419, 0], | ||||
|                   }, | ||||
|                 ], | ||||
|                 type: 'TagIdentifier', | ||||
|                 value: 'o', | ||||
|                 info: expect.any(Object), | ||||
|               }, | ||||
|             }, | ||||
|             paths: [ | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [0, 0], | ||||
|                 to: [-2.5, 0], | ||||
|                 tag: null, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [373, 393, 0], | ||||
|                 }, | ||||
|               }, | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [-2.5, 0], | ||||
|                 to: [0, 3], | ||||
|                 tag: { | ||||
|                   end: 419, | ||||
|                   start: 417, | ||||
|                   type: 'TagDeclarator', | ||||
|                   value: 'o', | ||||
|                 }, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [399, 420, 0], | ||||
|                 }, | ||||
|               }, | ||||
|               { | ||||
|                 type: 'ToPoint', | ||||
|                 from: [0, 3], | ||||
|                 to: [2.5, 0], | ||||
|                 tag: null, | ||||
|                 __geoMeta: { | ||||
|                   id: expect.any(String), | ||||
|                   sourceRange: [426, 445, 0], | ||||
|                 }, | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|           height: 2, | ||||
|           startCapId: expect.any(String), | ||||
|           endCapId: expect.any(String), | ||||
|           __meta: [{ sourceRange: [342, 367, 0] }], | ||||
|           units: { | ||||
|             type: 'Mm', | ||||
|           }, | ||||
|         }, | ||||
|         height: 2, | ||||
|         startCapId: expect.any(String), | ||||
|         endCapId: expect.any(String), | ||||
|         __meta: [{ sourceRange: [342, 367, 0] }], | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { kclErrorsToDiagnostics, KCLError } from './errors' | ||||
| import { defaultArtifactGraph, topLevelRange } from 'lang/wasm' | ||||
|  | ||||
| describe('test kclErrToDiagnostic', () => { | ||||
|   it('converts KCL errors to CodeMirror diagnostics', () => { | ||||
| @ -8,18 +9,20 @@ describe('test kclErrToDiagnostic', () => { | ||||
|         message: '', | ||||
|         kind: 'semantic', | ||||
|         msg: 'Semantic error', | ||||
|         sourceRange: [0, 1, true], | ||||
|         sourceRange: topLevelRange(0, 1), | ||||
|         operations: [], | ||||
|         artifactCommands: [], | ||||
|         artifactGraph: defaultArtifactGraph(), | ||||
|       }, | ||||
|       { | ||||
|         name: '', | ||||
|         message: '', | ||||
|         kind: 'type', | ||||
|         msg: 'Type error', | ||||
|         sourceRange: [4, 5, true], | ||||
|         sourceRange: topLevelRange(4, 5), | ||||
|         operations: [], | ||||
|         artifactCommands: [], | ||||
|         artifactGraph: defaultArtifactGraph(), | ||||
|       }, | ||||
|     ] | ||||
|     const diagnostics = kclErrorsToDiagnostics(errors) | ||||
|  | ||||
| @ -5,7 +5,13 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client' | ||||
| import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' | ||||
| import { Text } from '@codemirror/state' | ||||
| import { EditorView } from 'codemirror' | ||||
| import { ArtifactCommand, SourceRange } from 'lang/wasm' | ||||
| import { | ||||
|   ArtifactCommand, | ||||
|   ArtifactGraph, | ||||
|   defaultArtifactGraph, | ||||
|   isTopLevelModule, | ||||
|   SourceRange, | ||||
| } from 'lang/wasm' | ||||
| import { Operation } from 'wasm-lib/kcl/bindings/Operation' | ||||
|  | ||||
| type ExtractKind<T> = T extends { kind: infer K } ? K : never | ||||
| @ -15,13 +21,15 @@ export class KCLError extends Error { | ||||
|   msg: string | ||||
|   operations: Operation[] | ||||
|   artifactCommands: ArtifactCommand[] | ||||
|   artifactGraph: ArtifactGraph | ||||
|  | ||||
|   constructor( | ||||
|     kind: ExtractKind<RustKclError> | 'name', | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super() | ||||
|     this.kind = kind | ||||
| @ -29,6 +37,7 @@ export class KCLError extends Error { | ||||
|     this.sourceRange = sourceRange | ||||
|     this.operations = operations | ||||
|     this.artifactCommands = artifactCommands | ||||
|     this.artifactGraph = artifactGraph | ||||
|     Object.setPrototypeOf(this, KCLError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -38,9 +47,17 @@ export class KCLLexicalError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('lexical', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'lexical', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLSyntaxError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -50,9 +67,17 @@ export class KCLInternalError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('internal', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'internal', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLSyntaxError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -62,9 +87,17 @@ export class KCLSyntaxError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('syntax', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'syntax', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLSyntaxError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -74,9 +107,17 @@ export class KCLSemanticError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('semantic', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'semantic', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLSemanticError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -86,9 +127,10 @@ export class KCLTypeError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('type', msg, sourceRange, operations, artifactCommands) | ||||
|     super('type', msg, sourceRange, operations, artifactCommands, artifactGraph) | ||||
|     Object.setPrototypeOf(this, KCLTypeError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -98,9 +140,17 @@ export class KCLUnimplementedError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('unimplemented', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'unimplemented', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLUnimplementedError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -110,9 +160,17 @@ export class KCLUnexpectedError extends KCLError { | ||||
|     msg: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super('unexpected', msg, sourceRange, operations, artifactCommands) | ||||
|     super( | ||||
|       'unexpected', | ||||
|       msg, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLUnexpectedError.prototype) | ||||
|   } | ||||
| } | ||||
| @ -122,14 +180,16 @@ export class KCLValueAlreadyDefined extends KCLError { | ||||
|     key: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super( | ||||
|       'name', | ||||
|       `Key ${key} was already defined elsewhere`, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) | ||||
|   } | ||||
| @ -140,14 +200,16 @@ export class KCLUndefinedValueError extends KCLError { | ||||
|     key: string, | ||||
|     sourceRange: SourceRange, | ||||
|     operations: Operation[], | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     artifactGraph: ArtifactGraph | ||||
|   ) { | ||||
|     super( | ||||
|       'name', | ||||
|       `Key ${key} has not been defined`, | ||||
|       sourceRange, | ||||
|       operations, | ||||
|       artifactCommands | ||||
|       artifactCommands, | ||||
|       artifactGraph | ||||
|     ) | ||||
|     Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) | ||||
|   } | ||||
| @ -167,9 +229,10 @@ export function lspDiagnosticsToKclErrors( | ||||
|         new KCLError( | ||||
|           'unexpected', | ||||
|           message, | ||||
|           [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true], | ||||
|           [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0], | ||||
|           [], | ||||
|           [] | ||||
|           [], | ||||
|           defaultArtifactGraph() | ||||
|         ) | ||||
|     ) | ||||
|     .sort((a, b) => { | ||||
| @ -193,7 +256,7 @@ export function kclErrorsToDiagnostics( | ||||
|   errors: KCLError[] | ||||
| ): CodeMirrorDiagnostic[] { | ||||
|   return errors | ||||
|     ?.filter((err) => err.sourceRange[2]) | ||||
|     ?.filter((err) => isTopLevelModule(err.sourceRange)) | ||||
|     .map((err) => { | ||||
|       return { | ||||
|         from: err.sourceRange[0], | ||||
| @ -208,7 +271,7 @@ export function complilationErrorsToDiagnostics( | ||||
|   errors: CompilationError[] | ||||
| ): CodeMirrorDiagnostic[] { | ||||
|   return errors | ||||
|     ?.filter((err) => err.sourceRange[2] === 0) | ||||
|     ?.filter((err) => isTopLevelModule(err.sourceRange)) | ||||
|     .map((err) => { | ||||
|       let severity: any = 'error' | ||||
|       if (err.severity === 'Warning') { | ||||
|  | ||||
| @ -6,6 +6,8 @@ import { | ||||
|   Sketch, | ||||
|   initPromise, | ||||
|   sketchFromKclValue, | ||||
|   defaultArtifactGraph, | ||||
|   topLevelRange, | ||||
| } from './wasm' | ||||
| import { enginelessExecutor } from '../lib/testHelpers' | ||||
| import { KCLError } from './errors' | ||||
| @ -219,6 +221,9 @@ const newVar = myVar + 1` | ||||
|           }, | ||||
|         ], | ||||
|         id: expect.any(String), | ||||
|         units: { | ||||
|           type: 'Mm', | ||||
|         }, | ||||
|         __meta: [{ sourceRange: [39, 63, 0] }], | ||||
|       }, | ||||
|     }) | ||||
| @ -480,9 +485,10 @@ const theExtrude = startSketchOn('XY') | ||||
|       new KCLError( | ||||
|         'undefined_value', | ||||
|         'memory item key `myVarZ` is not defined', | ||||
|         [129, 135, true], | ||||
|         topLevelRange(129, 135), | ||||
|         [], | ||||
|         [] | ||||
|         [], | ||||
|         defaultArtifactGraph() | ||||
|       ) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
| @ -1,5 +1,12 @@ | ||||
| import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst' | ||||
| import { Identifier, assertParse, initPromise, Parameter } from './wasm' | ||||
| import { | ||||
|   Identifier, | ||||
|   assertParse, | ||||
|   initPromise, | ||||
|   Parameter, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from './wasm' | ||||
| import { err } from 'lib/trap' | ||||
|  | ||||
| beforeAll(async () => { | ||||
| @ -17,11 +24,10 @@ const sk3 = startSketchAt([0, 0]) | ||||
| ` | ||||
|     const subStr = 'lineTo([3, 4], %, $yo)' | ||||
|     const lineToSubstringIndex = code.indexOf(subStr) | ||||
|     const sourceRange: [number, number, boolean] = [ | ||||
|     const sourceRange = topLevelRange( | ||||
|       lineToSubstringIndex, | ||||
|       lineToSubstringIndex + subStr.length, | ||||
|       true, | ||||
|     ] | ||||
|       lineToSubstringIndex + subStr.length | ||||
|     ) | ||||
|  | ||||
|     const ast = assertParse(code) | ||||
|     const nodePath = getNodePathFromSourceRange(ast, sourceRange) | ||||
| @ -29,7 +35,7 @@ const sk3 = startSketchAt([0, 0]) | ||||
|     if (err(_node)) throw _node | ||||
|     const { node } = _node | ||||
|  | ||||
|     expect([node.start, node.end, true]).toEqual(sourceRange) | ||||
|     expect(topLevelRange(node.start, node.end)).toEqual(sourceRange) | ||||
|     expect(node.type).toBe('CallExpression') | ||||
|   }) | ||||
|   it('gets path right for function definition params', () => { | ||||
| @ -45,11 +51,7 @@ const sk3 = startSketchAt([0, 0]) | ||||
| const b1 = cube([0,0], 10)` | ||||
|     const subStr = 'pos, scale' | ||||
|     const subStrIndex = code.indexOf(subStr) | ||||
|     const sourceRange: [number, number, boolean] = [ | ||||
|       subStrIndex, | ||||
|       subStrIndex + 'pos'.length, | ||||
|       true, | ||||
|     ] | ||||
|     const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length) | ||||
|  | ||||
|     const ast = assertParse(code) | ||||
|     const nodePath = getNodePathFromSourceRange(ast, sourceRange) | ||||
| @ -81,11 +83,7 @@ const b1 = cube([0,0], 10)` | ||||
| const b1 = cube([0,0], 10)` | ||||
|     const subStr = 'scale, 0' | ||||
|     const subStrIndex = code.indexOf(subStr) | ||||
|     const sourceRange: [number, number, boolean] = [ | ||||
|       subStrIndex, | ||||
|       subStrIndex + 'scale'.length, | ||||
|       true, | ||||
|     ] | ||||
|     const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'scale'.length) | ||||
|  | ||||
|     const ast = assertParse(code) | ||||
|     const nodePath = getNodePathFromSourceRange(ast, sourceRange) | ||||
|  | ||||
| @ -1,4 +1,11 @@ | ||||
| import { assertParse, recast, initPromise, Identifier } from './wasm' | ||||
| import { | ||||
|   assertParse, | ||||
|   recast, | ||||
|   initPromise, | ||||
|   Identifier, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from './wasm' | ||||
| import { | ||||
|   createLiteral, | ||||
|   createIdentifier, | ||||
| @ -32,7 +39,7 @@ describe('Testing createLiteral', () => { | ||||
|   it('should create a literal', () => { | ||||
|     const result = createLiteral(5) | ||||
|     expect(result.type).toBe('Literal') | ||||
|     expect(result.value).toBe(5) | ||||
|     expect((result as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
| describe('Testing createIdentifier', () => { | ||||
| @ -49,7 +56,7 @@ describe('Testing createCallExpression', () => { | ||||
|     expect(result.callee.type).toBe('Identifier') | ||||
|     expect(result.callee.name).toBe('myFunc') | ||||
|     expect(result.arguments[0].type).toBe('Literal') | ||||
|     expect((result.arguments[0] as any).value).toBe(5) | ||||
|     expect((result.arguments[0] as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
| describe('Testing createObjectExpression', () => { | ||||
| @ -61,7 +68,7 @@ describe('Testing createObjectExpression', () => { | ||||
|     expect(result.properties[0].type).toBe('ObjectProperty') | ||||
|     expect(result.properties[0].key.name).toBe('myProp') | ||||
|     expect(result.properties[0].value.type).toBe('Literal') | ||||
|     expect((result.properties[0].value as any).value).toBe(5) | ||||
|     expect((result.properties[0].value as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
| describe('Testing createArrayExpression', () => { | ||||
| @ -69,7 +76,7 @@ describe('Testing createArrayExpression', () => { | ||||
|     const result = createArrayExpression([createLiteral(5)]) | ||||
|     expect(result.type).toBe('ArrayExpression') | ||||
|     expect(result.elements[0].type).toBe('Literal') | ||||
|     expect((result.elements[0] as any).value).toBe(5) | ||||
|     expect((result.elements[0] as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
| describe('Testing createPipeSubstitution', () => { | ||||
| @ -86,7 +93,7 @@ describe('Testing createVariableDeclaration', () => { | ||||
|     expect(result.declaration.id.type).toBe('Identifier') | ||||
|     expect(result.declaration.id.name).toBe('myVar') | ||||
|     expect(result.declaration.init.type).toBe('Literal') | ||||
|     expect((result.declaration.init as any).value).toBe(5) | ||||
|     expect((result.declaration.init as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
| describe('Testing createPipeExpression', () => { | ||||
| @ -94,7 +101,7 @@ describe('Testing createPipeExpression', () => { | ||||
|     const result = createPipeExpression([createLiteral(5)]) | ||||
|     expect(result.type).toBe('PipeExpression') | ||||
|     expect(result.body[0].type).toBe('Literal') | ||||
|     expect((result.body[0] as any).value).toBe(5) | ||||
|     expect((result.body[0] as any).value.value).toBe(5) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -148,11 +155,7 @@ function giveSketchFnCallTagTestHelper( | ||||
|   // making it more of an integration test, but easier to read the test intention is the goal | ||||
|   const ast = assertParse(code) | ||||
|   const start = code.indexOf(searchStr) | ||||
|   const range: [number, number, boolean] = [ | ||||
|     start, | ||||
|     start + searchStr.length, | ||||
|     true, | ||||
|   ] | ||||
|   const range = topLevelRange(start, start + searchStr.length) | ||||
|   const sketchRes = giveSketchFnCallTag(ast, range) | ||||
|   if (err(sketchRes)) throw sketchRes | ||||
|   const { modifiedAst, tag, isTagExisting } = sketchRes | ||||
| @ -230,7 +233,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const { modifiedAst } = moveValueIntoNewVariable( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [startIndex, startIndex, true], | ||||
|       topLevelRange(startIndex, startIndex), | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
| @ -244,7 +247,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const { modifiedAst } = moveValueIntoNewVariable( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [startIndex, startIndex, true], | ||||
|       topLevelRange(startIndex, startIndex), | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
| @ -258,7 +261,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const { modifiedAst } = moveValueIntoNewVariable( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [startIndex, startIndex, true], | ||||
|       topLevelRange(startIndex, startIndex), | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
| @ -272,7 +275,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const { modifiedAst } = moveValueIntoNewVariable( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [startIndex, startIndex, true], | ||||
|       topLevelRange(startIndex, startIndex), | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
| @ -286,7 +289,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const { modifiedAst } = moveValueIntoNewVariable( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [startIndex, startIndex, true], | ||||
|       topLevelRange(startIndex, startIndex), | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
| @ -306,18 +309,16 @@ describe('testing sketchOnExtrudedFace', () => { | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const segmentSnippet = `line([9.7, 9.19], %)` | ||||
|     const segmentRange: [number, number, boolean] = [ | ||||
|     const segmentRange = topLevelRange( | ||||
|       code.indexOf(segmentSnippet), | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length | ||||
|     ) | ||||
|     const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) | ||||
|     const extrudeSnippet = `extrude(5 + 7, %)` | ||||
|     const extrudeRange: [number, number, boolean] = [ | ||||
|     const extrudeRange = topLevelRange( | ||||
|       code.indexOf(extrudeSnippet), | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length | ||||
|     ) | ||||
|     const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) | ||||
|  | ||||
|     const extruded = sketchOnExtrudedFace( | ||||
| @ -346,18 +347,16 @@ sketch001 = startSketchOn(part001, seg01)`) | ||||
|   |> extrude(5 + 7, %)` | ||||
|     const ast = assertParse(code) | ||||
|     const segmentSnippet = `close(%)` | ||||
|     const segmentRange: [number, number, boolean] = [ | ||||
|     const segmentRange = topLevelRange( | ||||
|       code.indexOf(segmentSnippet), | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length | ||||
|     ) | ||||
|     const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) | ||||
|     const extrudeSnippet = `extrude(5 + 7, %)` | ||||
|     const extrudeRange: [number, number, boolean] = [ | ||||
|     const extrudeRange = topLevelRange( | ||||
|       code.indexOf(extrudeSnippet), | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length | ||||
|     ) | ||||
|     const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) | ||||
|  | ||||
|     const extruded = sketchOnExtrudedFace( | ||||
| @ -386,18 +385,16 @@ sketch001 = startSketchOn(part001, seg01)`) | ||||
|   |> extrude(5 + 7, %)` | ||||
|     const ast = assertParse(code) | ||||
|     const sketchSnippet = `startProfileAt([3.58, 2.06], %)` | ||||
|     const sketchRange: [number, number, boolean] = [ | ||||
|     const sketchRange = topLevelRange( | ||||
|       code.indexOf(sketchSnippet), | ||||
|       code.indexOf(sketchSnippet) + sketchSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(sketchSnippet) + sketchSnippet.length | ||||
|     ) | ||||
|     const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange) | ||||
|     const extrudeSnippet = `extrude(5 + 7, %)` | ||||
|     const extrudeRange: [number, number, boolean] = [ | ||||
|     const extrudeRange = topLevelRange( | ||||
|       code.indexOf(extrudeSnippet), | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length | ||||
|     ) | ||||
|     const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) | ||||
|  | ||||
|     const extruded = sketchOnExtrudedFace( | ||||
| @ -435,18 +432,16 @@ sketch001 = startSketchOn(part001, 'END')`) | ||||
|     part001 = extrude(5 + 7, sketch001)` | ||||
|     const ast = assertParse(code) | ||||
|     const segmentSnippet = `line([4.99, -0.46], %)` | ||||
|     const segmentRange: [number, number, boolean] = [ | ||||
|     const segmentRange = topLevelRange( | ||||
|       code.indexOf(segmentSnippet), | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(segmentSnippet) + segmentSnippet.length | ||||
|     ) | ||||
|     const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) | ||||
|     const extrudeSnippet = `extrude(5 + 7, sketch001)` | ||||
|     const extrudeRange: [number, number, boolean] = [ | ||||
|     const extrudeRange = topLevelRange( | ||||
|       code.indexOf(extrudeSnippet), | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(extrudeSnippet) + extrudeSnippet.length | ||||
|     ) | ||||
|     const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) | ||||
|  | ||||
|     const updatedAst = sketchOnExtrudedFace( | ||||
| @ -471,11 +466,10 @@ describe('Testing deleteSegmentFromPipeExpression', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const execState = await enginelessExecutor(ast) | ||||
|     const lineOfInterest = 'line([306.21, 198.85], %, $a)' | ||||
|     const range: [number, number, boolean] = [ | ||||
|     const range = topLevelRange( | ||||
|       code.indexOf(lineOfInterest), | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     ) | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|     const modifiedAst = deleteSegmentFromPipeExpression( | ||||
|       [], | ||||
| @ -549,11 +543,10 @@ ${!replace1 ? `  |> ${line}\n` : ''}  |> angledLine([-65, ${ | ||||
|       const ast = assertParse(code) | ||||
|       const execState = await enginelessExecutor(ast) | ||||
|       const lineOfInterest = line | ||||
|       const range: [number, number, boolean] = [ | ||||
|       const range = topLevelRange( | ||||
|         code.indexOf(lineOfInterest), | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|       ) | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|       const dependentSegments = findUsesOfTagInPipe(ast, pathToNode) | ||||
|       const modifiedAst = deleteSegmentFromPipeExpression( | ||||
| @ -638,11 +631,10 @@ describe('Testing removeSingleConstraintInfo', () => { | ||||
|  | ||||
|       const execState = await enginelessExecutor(ast) | ||||
|       const lineOfInterest = expectedFinish.split('(')[0] + '(' | ||||
|       const range: [number, number, boolean] = [ | ||||
|       const range = topLevelRange( | ||||
|         code.indexOf(lineOfInterest) + 1, | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|       ) | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|       let argPosition: SimplifiedArgDetails | ||||
|       if (key === 'arrayIndex' && typeof value === 'number') { | ||||
| @ -692,11 +684,10 @@ describe('Testing removeSingleConstraintInfo', () => { | ||||
|  | ||||
|       const execState = await enginelessExecutor(ast) | ||||
|       const lineOfInterest = expectedFinish.split('(')[0] + '(' | ||||
|       const range: [number, number, boolean] = [ | ||||
|       const range = topLevelRange( | ||||
|         code.indexOf(lineOfInterest) + 1, | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|       ) | ||||
|       let argPosition: SimplifiedArgDetails | ||||
|       if (key === 'arrayIndex' && typeof value === 'number') { | ||||
|         argPosition = { | ||||
| @ -889,11 +880,10 @@ sketch002 = startSketchOn({ | ||||
|       const execState = await enginelessExecutor(ast) | ||||
|  | ||||
|       // deleteFromSelection | ||||
|       const range: [number, number, boolean] = [ | ||||
|       const range = topLevelRange( | ||||
|         codeBefore.indexOf(lineOfInterest), | ||||
|         codeBefore.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|         true, | ||||
|       ] | ||||
|         codeBefore.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|       ) | ||||
|       const artifact = { type } as Artifact | ||||
|       const newAst = await deleteFromSelection( | ||||
|         ast, | ||||
|  | ||||
| @ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): { | ||||
|   return splitPathAtPipeExpression(pathToNode.slice(0, -1)) | ||||
| } | ||||
|  | ||||
| export function createLiteral(value: LiteralValue): Node<Literal> { | ||||
| export function createLiteral(value: LiteralValue | number): Node<Literal> { | ||||
|   const raw = `${value}` | ||||
|   if (typeof value === 'number') { | ||||
|     value = { value, suffix: 'None' } | ||||
|   } | ||||
|   return { | ||||
|     type: 'Literal', | ||||
|     start: 0, | ||||
|     end: 0, | ||||
|     moduleId: 0, | ||||
|     value, | ||||
|     raw: `${value}`, | ||||
|     raw, | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -8,6 +8,8 @@ import { | ||||
|   makeDefaultPlanes, | ||||
|   PipeExpression, | ||||
|   VariableDeclarator, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from '../wasm' | ||||
| import { | ||||
|   EdgeTreatmentType, | ||||
| @ -77,11 +79,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async ( | ||||
|     code: string, | ||||
|     expectedExtrudeSnippet: string | ||||
|   ): CallExpression | PipeExpression | Error { | ||||
|     const extrudeRange: [number, number, boolean] = [ | ||||
|     const extrudeRange = topLevelRange( | ||||
|       code.indexOf(expectedExtrudeSnippet), | ||||
|       code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length | ||||
|     ) | ||||
|     const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange) | ||||
|     const expectedExtrudeNodeResult = getNodeFromPath< | ||||
|       VariableDeclarator | CallExpression | ||||
| @ -112,11 +113,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async ( | ||||
|   const ast = assertParse(code) | ||||
|  | ||||
|   // selection | ||||
|   const segmentRange: [number, number, boolean] = [ | ||||
|   const segmentRange = topLevelRange( | ||||
|     code.indexOf(selectedSegmentSnippet), | ||||
|     code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, | ||||
|     true, | ||||
|   ] | ||||
|     code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length | ||||
|   ) | ||||
|   const selection: Selection = { | ||||
|     codeRef: codeRefFromRange(segmentRange, ast), | ||||
|   } | ||||
| @ -260,12 +260,12 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async ( | ||||
|   const ast = assertParse(code) | ||||
|  | ||||
|   // selection | ||||
|   const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map( | ||||
|     (selectionSnippet) => [ | ||||
|       code.indexOf(selectionSnippet), | ||||
|       code.indexOf(selectionSnippet) + selectionSnippet.length, | ||||
|       true, | ||||
|     ] | ||||
|   const segmentRanges: Array<SourceRange> = selectionSnippets.map( | ||||
|     (selectionSnippet) => | ||||
|       topLevelRange( | ||||
|         code.indexOf(selectionSnippet), | ||||
|         code.indexOf(selectionSnippet) + selectionSnippet.length | ||||
|       ) | ||||
|   ) | ||||
|  | ||||
|   // executeAst | ||||
| @ -596,11 +596,10 @@ extrude001 = extrude(-5, sketch001) | ||||
|   it('should correctly identify getOppositeEdge and baseEdge edges', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const lineOfInterest = `line([7.11, 3.48], %, $seg01)` | ||||
|     const range: [number, number, boolean] = [ | ||||
|     const range = topLevelRange( | ||||
|       code.indexOf(lineOfInterest), | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     ) | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|     if (err(pathToNode)) return | ||||
|     const callExp = getNodeFromPath<CallExpression>( | ||||
| @ -615,11 +614,10 @@ extrude001 = extrude(-5, sketch001) | ||||
|   it('should correctly identify getPreviousAdjacentEdge edges', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const lineOfInterest = `line([-6.37, 3.88], %, $seg02)` | ||||
|     const range: [number, number, boolean] = [ | ||||
|     const range = topLevelRange( | ||||
|       code.indexOf(lineOfInterest), | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     ) | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|     if (err(pathToNode)) return | ||||
|     const callExp = getNodeFromPath<CallExpression>( | ||||
| @ -634,11 +632,10 @@ extrude001 = extrude(-5, sketch001) | ||||
|   it('should correctly identify no edges', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const lineOfInterest = `line([-3.29, -13.85], %)` | ||||
|     const range: [number, number, boolean] = [ | ||||
|     const range = topLevelRange( | ||||
|       code.indexOf(lineOfInterest), | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length, | ||||
|       true, | ||||
|     ] | ||||
|       code.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     ) | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|     if (err(pathToNode)) return | ||||
|     const callExp = getNodeFromPath<CallExpression>( | ||||
| @ -660,13 +657,12 @@ describe('Testing button states', () => { | ||||
|   ) => { | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const range: [number, number, boolean] = segmentSnippet | ||||
|       ? [ | ||||
|     const range = segmentSnippet | ||||
|       ? topLevelRange( | ||||
|           code.indexOf(segmentSnippet), | ||||
|           code.indexOf(segmentSnippet) + segmentSnippet.length, | ||||
|           true, | ||||
|         ] | ||||
|       : [ast.end, ast.end, true] // empty line in the end of the code | ||||
|           code.indexOf(segmentSnippet) + segmentSnippet.length | ||||
|         ) | ||||
|       : topLevelRange(ast.end, ast.end) // empty line in the end of the code | ||||
|  | ||||
|     const selectionRanges: Selections = { | ||||
|       graphSelections: [ | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { | ||||
|   ArtifactGraph, | ||||
|   CallExpression, | ||||
|   Expr, | ||||
|   Identifier, | ||||
| @ -31,11 +32,7 @@ import { | ||||
| import { err, trap } from 'lib/trap' | ||||
| import { Selection, Selections } from 'lib/selections' | ||||
| import { KclCommandValue } from 'lib/commandTypes' | ||||
| import { | ||||
|   Artifact, | ||||
|   ArtifactGraph, | ||||
|   getSweepFromSuspectedPath, | ||||
| } from 'lang/std/artifactGraph' | ||||
| import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph' | ||||
| import { | ||||
|   kclManager, | ||||
|   engineCommandManager, | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { ArtifactGraph } from 'lang/std/artifactGraph' | ||||
| import { Selections } from 'lib/selections' | ||||
| import { Expr } from 'wasm-lib/kcl/bindings/Expr' | ||||
| import { Program } from 'wasm-lib/kcl/bindings/Program' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { PathToNode, VariableDeclarator } from 'lang/wasm' | ||||
| import { ArtifactGraph, PathToNode, VariableDeclarator } from 'lang/wasm' | ||||
| import { | ||||
|   getPathToExtrudeForSegmentSelection, | ||||
|   mutateAstWithTagForSketchSegment, | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { | ||||
|   initPromise, | ||||
|   PathToNode, | ||||
|   Identifier, | ||||
|   topLevelRange, | ||||
| } from './wasm' | ||||
| import { | ||||
|   findAllPreviousVariables, | ||||
| @ -57,7 +58,7 @@ variableBelowShouldNotBeIncluded = 3 | ||||
|     const { variables, bodyPath, insertIndex } = findAllPreviousVariables( | ||||
|       ast, | ||||
|       execState.memory, | ||||
|       [rangeStart, rangeStart, true] | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     expect(variables).toEqual([ | ||||
|       { key: 'baseThick', value: 1 }, | ||||
| @ -87,7 +88,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find a safe binaryExpression', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('100 + 100') + 2 | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(true) | ||||
|     expect(result.value?.type).toBe('BinaryExpression') | ||||
| @ -100,7 +104,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find a safe Identifier', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('abc') | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(true) | ||||
|     expect(result.value?.type).toBe('Identifier') | ||||
| @ -109,7 +116,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find a safe CallExpression', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('def') | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(true) | ||||
|     expect(result.value?.type).toBe('CallExpression') | ||||
| @ -122,7 +132,7 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('ghi') | ||||
|     const range: [number, number, boolean] = [rangeStart, rangeStart, true] | ||||
|     const range = topLevelRange(rangeStart, rangeStart) | ||||
|     const result = isNodeSafeToReplace(ast, range) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(false) | ||||
| @ -132,7 +142,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find an UNsafe Identifier, as it is a callee', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('ine([2.8,') | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(false) | ||||
|     expect(result.value?.type).toBe('CallExpression') | ||||
| @ -143,7 +156,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it("find a safe BinaryExpression that's assigned to a variable", () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('5 + 6') + 1 | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(true) | ||||
|     expect(result.value?.type).toBe('BinaryExpression') | ||||
| @ -156,7 +172,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|   it('find a safe BinaryExpression that has a CallExpression within', () => { | ||||
|     const ast = assertParse(code) | ||||
|     const rangeStart = code.indexOf('jkl') + 1 | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|     expect(result.isSafe).toBe(true) | ||||
|     expect(result.value?.type).toBe('BinaryExpression') | ||||
| @ -173,7 +192,10 @@ yo2 = hmm([identifierGuy + 5])` | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const rangeStart = code.indexOf('identifierGuy') + 1 | ||||
|     const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) | ||||
|     const result = isNodeSafeToReplace( | ||||
|       ast, | ||||
|       topLevelRange(rangeStart, rangeStart) | ||||
|     ) | ||||
|     if (err(result)) throw result | ||||
|  | ||||
|     expect(result.isSafe).toBe(true) | ||||
| @ -222,11 +244,10 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     const sourceIndex = code.indexOf(searchLn) + searchLn.length | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const result = getNodePathFromSourceRange(ast, [ | ||||
|       sourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const result = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(sourceIndex, sourceIndex) | ||||
|     ) | ||||
|     expect(result).toEqual([ | ||||
|       ['body', ''], | ||||
|       [0, 'index'], | ||||
| @ -241,11 +262,10 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     const sourceIndex = code.indexOf(searchLn) + searchLn.length | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const result = getNodePathFromSourceRange(ast, [ | ||||
|       sourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const result = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(sourceIndex, sourceIndex) | ||||
|     ) | ||||
|     const expected = [ | ||||
|       ['body', ''], | ||||
|       [0, 'index'], | ||||
| @ -257,18 +277,16 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     expect(result).toEqual(expected) | ||||
|     // expect similar result for start of line | ||||
|     const startSourceIndex = code.indexOf(searchLn) | ||||
|     const startResult = getNodePathFromSourceRange(ast, [ | ||||
|       startSourceIndex, | ||||
|       startSourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const startResult = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(startSourceIndex, startSourceIndex) | ||||
|     ) | ||||
|     expect(startResult).toEqual([...expected, ['callee', 'CallExpression']]) | ||||
|     // expect similar result when whole line is selected | ||||
|     const selectWholeThing = getNodePathFromSourceRange(ast, [ | ||||
|       startSourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const selectWholeThing = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(startSourceIndex, sourceIndex) | ||||
|     ) | ||||
|     expect(selectWholeThing).toEqual(expected) | ||||
|   }) | ||||
|  | ||||
| @ -283,11 +301,10 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     const sourceIndex = code.indexOf(searchLn) | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const result = getNodePathFromSourceRange(ast, [ | ||||
|       sourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const result = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(sourceIndex, sourceIndex) | ||||
|     ) | ||||
|     expect(result).toEqual([ | ||||
|       ['body', ''], | ||||
|       [1, 'index'], | ||||
| @ -313,11 +330,10 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     const sourceIndex = code.indexOf(searchLn) | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const result = getNodePathFromSourceRange(ast, [ | ||||
|       sourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const result = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(sourceIndex, sourceIndex) | ||||
|     ) | ||||
|     expect(result).toEqual([ | ||||
|       ['body', ''], | ||||
|       [1, 'index'], | ||||
| @ -341,11 +357,10 @@ describe('testing getNodePathFromSourceRange', () => { | ||||
|     const sourceIndex = code.indexOf(searchLn) | ||||
|     const ast = assertParse(code) | ||||
|  | ||||
|     const result = getNodePathFromSourceRange(ast, [ | ||||
|       sourceIndex, | ||||
|       sourceIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const result = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(sourceIndex, sourceIndex) | ||||
|     ) | ||||
|     expect(result).toEqual([ | ||||
|       ['body', ''], | ||||
|       [0, 'index'], | ||||
| @ -375,7 +390,7 @@ part001 = startSketchAt([-1.41, 3.46]) | ||||
|     const result = hasExtrudeSketch({ | ||||
|       ast, | ||||
|       selection: { | ||||
|         codeRef: codeRefFromRange([100, 101, true], ast), | ||||
|         codeRef: codeRefFromRange(topLevelRange(100, 101), ast), | ||||
|       }, | ||||
|       programMemory: execState.memory, | ||||
|     }) | ||||
| @ -395,7 +410,7 @@ part001 = startSketchAt([-1.41, 3.46]) | ||||
|     const result = hasExtrudeSketch({ | ||||
|       ast, | ||||
|       selection: { | ||||
|         codeRef: codeRefFromRange([100, 101, true], ast), | ||||
|         codeRef: codeRefFromRange(topLevelRange(100, 101), ast), | ||||
|       }, | ||||
|       programMemory: execState.memory, | ||||
|     }) | ||||
| @ -409,7 +424,7 @@ part001 = startSketchAt([-1.41, 3.46]) | ||||
|     const result = hasExtrudeSketch({ | ||||
|       ast, | ||||
|       selection: { | ||||
|         codeRef: codeRefFromRange([10, 11, true], ast), | ||||
|         codeRef: codeRefFromRange(topLevelRange(10, 11), ast), | ||||
|       }, | ||||
|       programMemory: execState.memory, | ||||
|     }) | ||||
| @ -431,11 +446,10 @@ describe('Testing findUsesOfTagInPipe', () => { | ||||
|     const lineOfInterest = `198.85], %, $seg01` | ||||
|     const characterIndex = | ||||
|       exampleCode.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, [ | ||||
|       characterIndex, | ||||
|       characterIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const pathToNode = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(characterIndex, characterIndex) | ||||
|     ) | ||||
|     const result = findUsesOfTagInPipe(ast, pathToNode) | ||||
|     expect(result).toHaveLength(2) | ||||
|     result.forEach((range) => { | ||||
| @ -448,11 +462,10 @@ describe('Testing findUsesOfTagInPipe', () => { | ||||
|     const lineOfInterest = `line([306.21, 198.82], %)` | ||||
|     const characterIndex = | ||||
|       exampleCode.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, [ | ||||
|       characterIndex, | ||||
|       characterIndex, | ||||
|       true, | ||||
|     ]) | ||||
|     const pathToNode = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(characterIndex, characterIndex) | ||||
|     ) | ||||
|     const result = findUsesOfTagInPipe(ast, pathToNode) | ||||
|     expect(result).toHaveLength(0) | ||||
|   }) | ||||
| @ -498,7 +511,10 @@ sketch003 = startSketchOn(extrude001, 'END') | ||||
|       exampleCode.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     const extruded = hasSketchPipeBeenExtruded( | ||||
|       { | ||||
|         codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), | ||||
|         codeRef: codeRefFromRange( | ||||
|           topLevelRange(characterIndex, characterIndex), | ||||
|           ast | ||||
|         ), | ||||
|       }, | ||||
|       ast | ||||
|     ) | ||||
| @ -511,7 +527,10 @@ sketch003 = startSketchOn(extrude001, 'END') | ||||
|       exampleCode.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     const extruded = hasSketchPipeBeenExtruded( | ||||
|       { | ||||
|         codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), | ||||
|         codeRef: codeRefFromRange( | ||||
|           topLevelRange(characterIndex, characterIndex), | ||||
|           ast | ||||
|         ), | ||||
|       }, | ||||
|       ast | ||||
|     ) | ||||
| @ -524,7 +543,10 @@ sketch003 = startSketchOn(extrude001, 'END') | ||||
|       exampleCode.indexOf(lineOfInterest) + lineOfInterest.length | ||||
|     const extruded = hasSketchPipeBeenExtruded( | ||||
|       { | ||||
|         codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), | ||||
|         codeRef: codeRefFromRange( | ||||
|           topLevelRange(characterIndex, characterIndex), | ||||
|           ast | ||||
|         ), | ||||
|       }, | ||||
|       ast | ||||
|     ) | ||||
| @ -638,7 +660,7 @@ myNestedVar = [ | ||||
|       enter: (node, path) => { | ||||
|         if ( | ||||
|           node.type === 'Literal' && | ||||
|           String(node.value) === literalOfInterest | ||||
|           String((node as any).value.value) === literalOfInterest | ||||
|         ) { | ||||
|           pathToNode = path | ||||
|         } else if ( | ||||
| @ -651,11 +673,10 @@ myNestedVar = [ | ||||
|     }) | ||||
|  | ||||
|     const literalIndex = code.indexOf(literalOfInterest) | ||||
|     const pathToNode2 = getNodePathFromSourceRange(ast, [ | ||||
|       literalIndex + 2, | ||||
|       literalIndex + 2, | ||||
|       true, | ||||
|     ]) | ||||
|     const pathToNode2 = getNodePathFromSourceRange( | ||||
|       ast, | ||||
|       topLevelRange(literalIndex + 2, literalIndex + 2) | ||||
|     ) | ||||
|     expect(pathToNode).toEqual(pathToNode2) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { ToolTip } from 'lang/langHelpers' | ||||
| import { Selection, Selections } from 'lib/selections' | ||||
| import { | ||||
|   ArrayExpression, | ||||
|   ArtifactGraph, | ||||
|   BinaryExpression, | ||||
|   CallExpression, | ||||
|   Expr, | ||||
| @ -16,8 +17,8 @@ import { | ||||
|   sketchFromKclValue, | ||||
|   sketchFromKclValueOptional, | ||||
|   SourceRange, | ||||
|   sourceRangeFromRust, | ||||
|   SyntaxType, | ||||
|   topLevelRange, | ||||
|   VariableDeclaration, | ||||
|   VariableDeclarator, | ||||
| } from './wasm' | ||||
| @ -32,7 +33,7 @@ import { | ||||
| import { err, Reason } from 'lib/trap' | ||||
| import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' | ||||
| import { codeRefFromRange } from './std/artifactGraph' | ||||
|  | ||||
| /** | ||||
|  * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. | ||||
| @ -716,16 +717,6 @@ function isTypeInArrayExp( | ||||
|   return node.elements.some((el) => isTypeInValue(el, syntaxType)) | ||||
| } | ||||
|  | ||||
| export function isValueZero(val?: Expr): boolean { | ||||
|   return ( | ||||
|     (val?.type === 'Literal' && Number(val.value) === 0) || | ||||
|     (val?.type === 'UnaryExpression' && | ||||
|       val.operator === '-' && | ||||
|       val.argument.type === 'Literal' && | ||||
|       Number(val.argument.value) === 0) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export function isLinesParallelAndConstrained( | ||||
|   ast: Program, | ||||
|   artifactGraph: ArtifactGraph, | ||||
| @ -819,7 +810,7 @@ export function isLinesParallelAndConstrained( | ||||
|     return { | ||||
|       isParallelAndConstrained, | ||||
|       selection: { | ||||
|         codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast), | ||||
|         codeRef: codeRefFromRange(prevSourceRange, ast), | ||||
|         artifact: artifactGraph.get(prevSegment.__geoMeta.id), | ||||
|       }, | ||||
|     } | ||||
| @ -937,7 +928,7 @@ export function findUsesOfTagInPipe( | ||||
|       const tagArgValue = | ||||
|         tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name | ||||
|       if (tagArgValue === tag) | ||||
|         dependentRanges.push([node.start, node.end, true]) | ||||
|         dependentRanges.push(topLevelRange(node.start, node.end)) | ||||
|     }, | ||||
|   }) | ||||
|   return dependentRanges | ||||
|  | ||||
| @ -1,559 +0,0 @@ | ||||
| // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||||
|  | ||||
| exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = ` | ||||
| Map { | ||||
|   "UUID-0" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         12, | ||||
|         31, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "id": "UUID", | ||||
|     "pathIds": [ | ||||
|       "UUID", | ||||
|     ], | ||||
|     "type": "plane", | ||||
|   }, | ||||
|   "UUID-1" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         37, | ||||
|         64, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "id": "UUID", | ||||
|     "planeId": "UUID", | ||||
|     "segIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "solid2dId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "path", | ||||
|   }, | ||||
|   "UUID-2" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         70, | ||||
|         86, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-3" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         92, | ||||
|         119, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeCutId": "UUID", | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-4" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         125, | ||||
|         150, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-5" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         156, | ||||
|         203, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-6" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         209, | ||||
|         217, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-7" => { | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "type": "solid2D", | ||||
|   }, | ||||
|   "UUID-8" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         231, | ||||
|         254, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "subType": "extrusion", | ||||
|     "surfaceIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "type": "sweep", | ||||
|   }, | ||||
|   "UUID-9" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-10" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [ | ||||
|       "UUID", | ||||
|     ], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-11" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-12" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-13" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "subType": "start", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "cap", | ||||
|   }, | ||||
|   "UUID-14" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "subType": "end", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "cap", | ||||
|   }, | ||||
|   "UUID-15" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-16" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-17" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-18" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-19" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-20" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-21" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-22" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-23" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         260, | ||||
|         299, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "consumedEdgeId": "UUID", | ||||
|     "edgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "subType": "fillet", | ||||
|     "type": "edgeCut", | ||||
|   }, | ||||
|   "UUID-24" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         350, | ||||
|         377, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "id": "UUID", | ||||
|     "planeId": "UUID", | ||||
|     "segIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "solid2dId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "path", | ||||
|   }, | ||||
|   "UUID-25" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         383, | ||||
|         398, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-26" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         404, | ||||
|         420, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-27" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         426, | ||||
|         473, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "surfaceId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-28" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         479, | ||||
|         487, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "type": "segment", | ||||
|   }, | ||||
|   "UUID-29" => { | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "type": "solid2D", | ||||
|   }, | ||||
|   "UUID-30" => { | ||||
|     "codeRef": { | ||||
|       "pathToNode": [ | ||||
|         [ | ||||
|           "body", | ||||
|           "", | ||||
|         ], | ||||
|       ], | ||||
|       "range": [ | ||||
|         501, | ||||
|         522, | ||||
|         true, | ||||
|       ], | ||||
|     }, | ||||
|     "edgeIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "id": "UUID", | ||||
|     "pathId": "UUID", | ||||
|     "subType": "extrusion", | ||||
|     "surfaceIds": [ | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|       "UUID", | ||||
|     ], | ||||
|     "type": "sweep", | ||||
|   }, | ||||
|   "UUID-31" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-32" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-33" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "segId": "UUID", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "wall", | ||||
|   }, | ||||
|   "UUID-34" => { | ||||
|     "edgeCutEdgeIds": [], | ||||
|     "id": "UUID", | ||||
|     "pathIds": [], | ||||
|     "subType": "end", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "cap", | ||||
|   }, | ||||
|   "UUID-35" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-36" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-37" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-38" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-39" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "opposite", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
|   "UUID-40" => { | ||||
|     "id": "UUID", | ||||
|     "segId": "UUID", | ||||
|     "subType": "adjacent", | ||||
|     "sweepId": "UUID", | ||||
|     "type": "sweepEdge", | ||||
|   }, | ||||
| } | ||||
| `; | ||||
| @ -1,960 +0,0 @@ | ||||
| import { | ||||
|   makeDefaultPlanes, | ||||
|   assertParse, | ||||
|   initPromise, | ||||
|   Program, | ||||
|   ArtifactCommand, | ||||
|   ExecState, | ||||
| } from 'lang/wasm' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { | ||||
|   ResponseMap, | ||||
|   createArtifactGraph, | ||||
|   filterArtifacts, | ||||
|   expandPlane, | ||||
|   expandPath, | ||||
|   expandSweep, | ||||
|   ArtifactGraph, | ||||
|   expandSegment, | ||||
|   getArtifactsToUpdate, | ||||
| } from './artifactGraph' | ||||
| import { err } from 'lib/trap' | ||||
| import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
| import { VITE_KC_DEV_TOKEN } from 'env' | ||||
| import fsp from 'fs/promises' | ||||
| import fs from 'fs' | ||||
| import { chromium } from 'playwright' | ||||
| import * as d3 from 'd3-force' | ||||
| import path from 'path' | ||||
| import pixelmatch from 'pixelmatch' | ||||
| import { PNG } from 'pngjs' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
|  | ||||
| /* | ||||
| Note this is an integration test, these tests connect to our real dev server and make websocket commands. | ||||
| It's needed for testing the artifactGraph, as it is tied to the websocket commands. | ||||
| */ | ||||
|  | ||||
| const pathStart = 'src/lang/std/artifactMapCache' | ||||
| const fullPath = `${pathStart}/artifactMapCache.json` | ||||
|  | ||||
| const exampleCode1 = `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-5, -5], %) | ||||
|   |> line([0, 10], %) | ||||
|   |> line([10.55, 0], %, $seg01) | ||||
|   |> line([0, -10], %, $seg02) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(-10, sketch001) | ||||
|   |> fillet({ radius: 5, tags: [seg01] }, %) | ||||
| sketch002 = startSketchOn(extrude001, seg02) | ||||
|   |> startProfileAt([-2, -6], %) | ||||
|   |> line([2, 3], %) | ||||
|   |> line([2, -3], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude002 = extrude(5, sketch002) | ||||
| ` | ||||
|  | ||||
| const exampleCodeNo3D = `sketch003 = startSketchOn('YZ') | ||||
|   |> startProfileAt([5.82, 0], %) | ||||
|   |> angledLine([180, 11.54], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        8.21 | ||||
|      ], %, $rectangleSegmentB001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %, $rectangleSegmentC001) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| sketch004 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([0, 14.36], %) | ||||
|   |> line([15.49, 0.05], %) | ||||
|   |> tangentialArcTo([0, 0], %) | ||||
|   |> tangentialArcTo([-6.8, 8.17], %) | ||||
| ` | ||||
|  | ||||
| const sketchOnFaceOnFaceEtc = `sketch001 = startSketchOn('XZ') | ||||
| |> startProfileAt([0, 0], %) | ||||
| |> line([4, 8], %) | ||||
| |> line([5, -8], %, $seg01) | ||||
| |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
| |> close(%) | ||||
| extrude001 = extrude(6, sketch001) | ||||
| sketch002 = startSketchOn(extrude001, seg01) | ||||
| |> startProfileAt([-0.5, 0.5], %) | ||||
| |> line([2, 5], %) | ||||
| |> line([2, -5], %) | ||||
| |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
| |> close(%) | ||||
| extrude002 = extrude(5, sketch002) | ||||
| sketch003 = startSketchOn(extrude002, 'END') | ||||
| |> startProfileAt([1, 1.5], %) | ||||
| |> line([0.5, 2], %, $seg02) | ||||
| |> line([1, -2], %) | ||||
| |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
| |> close(%) | ||||
| extrude003 = extrude(4, sketch003) | ||||
| sketch004 = startSketchOn(extrude003, seg02) | ||||
| |> startProfileAt([-3, 14], %) | ||||
| |> line([0.5, 1], %) | ||||
| |> line([0.5, -2], %) | ||||
| |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
| |> close(%) | ||||
| extrude004 = extrude(3, sketch004) | ||||
| ` | ||||
| const exampleCodeOffsetPlanes = ` | ||||
| offsetPlane001 = offsetPlane("XY", 20) | ||||
| offsetPlane002 = offsetPlane("XZ", -50) | ||||
| offsetPlane003 = offsetPlane("YZ", 10) | ||||
|  | ||||
| sketch002 = startSketchOn(offsetPlane001) | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([6.78, 15.01], %) | ||||
| ` | ||||
|  | ||||
| // add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests | ||||
| const codeToWriteCacheFor = { | ||||
|   exampleCode1, | ||||
|   sketchOnFaceOnFaceEtc, | ||||
|   exampleCodeNo3D, | ||||
|   exampleCodeOffsetPlanes, | ||||
| } as const | ||||
|  | ||||
| type CodeKey = keyof typeof codeToWriteCacheFor | ||||
|  | ||||
| type CacheShape = { | ||||
|   [key in CodeKey]: { | ||||
|     artifactCommands: ArtifactCommand[] | ||||
|     responseMap: ResponseMap | ||||
|     execStateArtifacts: ExecState['artifacts'] | ||||
|   } | ||||
| } | ||||
|  | ||||
| beforeAll(async () => { | ||||
|   await initPromise | ||||
|  | ||||
|   // THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local | ||||
|   await new Promise((resolve) => { | ||||
|     engineCommandManager.start({ | ||||
|       // disableWebRTC: true, | ||||
|       token: VITE_KC_DEV_TOKEN, | ||||
|       // there does seem to be a minimum resolution, not sure what it is but 256 works ok. | ||||
|       width: 256, | ||||
|       height: 256, | ||||
|       makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), | ||||
|       setMediaStream: () => {}, | ||||
|       setIsStreamReady: () => {}, | ||||
|       // eslint-disable-next-line @typescript-eslint/no-misused-promises | ||||
|       callbackOnEngineLiteConnect: async () => { | ||||
|         const cacheEntries = Object.entries(codeToWriteCacheFor) as [ | ||||
|           CodeKey, | ||||
|           string | ||||
|         ][] | ||||
|         const cacheToWriteToFileTemp: Partial<CacheShape> = {} | ||||
|         for (const [codeKey, code] of cacheEntries) { | ||||
|           const ast = assertParse(code) | ||||
|           await kclManager.executeAst({ ast }) | ||||
|  | ||||
|           cacheToWriteToFileTemp[codeKey] = { | ||||
|             artifactCommands: kclManager.execState.artifactCommands, | ||||
|             responseMap: engineCommandManager.responseMap, | ||||
|             execStateArtifacts: kclManager.execState.artifacts, | ||||
|           } | ||||
|         } | ||||
|         const cache = JSON.stringify(cacheToWriteToFileTemp) | ||||
|  | ||||
|         await fsp.mkdir(pathStart, { recursive: true }) | ||||
|         await fsp.writeFile(fullPath, cache) | ||||
|         resolve(true) | ||||
|       }, | ||||
|     }) | ||||
|   }) | ||||
| }, 20_000) | ||||
|  | ||||
| afterAll(() => { | ||||
|   engineCommandManager.tearDown() | ||||
| }) | ||||
|  | ||||
| describe('testing createArtifactGraph', () => { | ||||
|   describe('code with offset planes and a sketch:', () => { | ||||
|     let ast: Node<Program> | ||||
|     let theMap: ReturnType<typeof createArtifactGraph> | ||||
|  | ||||
|     it('setup', () => { | ||||
|       // putting this logic in here because describe blocks runs before beforeAll has finished | ||||
|       const { | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast: _ast, | ||||
|         execStateArtifacts, | ||||
|       } = getCommands('exampleCodeOffsetPlanes') | ||||
|       ast = _ast | ||||
|       theMap = createArtifactGraph({ | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast, | ||||
|         execStateArtifacts, | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it(`there should be one sketch`, () => { | ||||
|       const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map( | ||||
|         (path) => expandPath(path[1], theMap) | ||||
|       ) | ||||
|       expect(sketches).toHaveLength(1) | ||||
|       sketches.forEach((path) => { | ||||
|         if (err(path)) throw path | ||||
|         expect(path.type).toBe('path') | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it(`there should be three offsetPlanes`, () => { | ||||
|       const offsetPlanes = [ | ||||
|         ...filterArtifacts({ types: ['plane'] }, theMap), | ||||
|       ].map((plane) => expandPlane(plane[1], theMap)) | ||||
|       expect(offsetPlanes).toHaveLength(3) | ||||
|       offsetPlanes.forEach((path) => { | ||||
|         expect(path.type).toBe('plane') | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it(`Only one offset plane should have a path`, () => { | ||||
|       const offsetPlanes = [ | ||||
|         ...filterArtifacts({ types: ['plane'] }, theMap), | ||||
|       ].map((plane) => expandPlane(plane[1], theMap)) | ||||
|       const offsetPlaneWithPaths = offsetPlanes.filter( | ||||
|         (plane) => plane.paths.length | ||||
|       ) | ||||
|       expect(offsetPlaneWithPaths).toHaveLength(1) | ||||
|     }) | ||||
|   }) | ||||
|   describe('code with an extrusion, fillet and sketch of face:', () => { | ||||
|     let ast: Node<Program> | ||||
|     let theMap: ReturnType<typeof createArtifactGraph> | ||||
|     it('setup', () => { | ||||
|       // putting this logic in here because describe blocks runs before beforeAll has finished | ||||
|       const { | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast: _ast, | ||||
|         execStateArtifacts, | ||||
|       } = getCommands('exampleCode1') | ||||
|       ast = _ast | ||||
|       theMap = createArtifactGraph({ | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast, | ||||
|         execStateArtifacts, | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it('there should be two planes for the extrusion and the sketch on face', () => { | ||||
|       const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map( | ||||
|         (plane) => expandPlane(plane[1], theMap) | ||||
|       ) | ||||
|       expect(planes).toHaveLength(1) | ||||
|       planes.forEach((path) => { | ||||
|         expect(path.type).toBe('plane') | ||||
|       }) | ||||
|     }) | ||||
|     it('there should be two paths for the extrusion and the sketch on face', () => { | ||||
|       const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map( | ||||
|         (path) => expandPath(path[1], theMap) | ||||
|       ) | ||||
|       expect(paths).toHaveLength(2) | ||||
|       paths.forEach((path) => { | ||||
|         if (err(path)) throw path | ||||
|         expect(path.type).toBe('path') | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => { | ||||
|       const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map( | ||||
|         (extrusion) => expandSweep(extrusion[1], theMap) | ||||
|       ) | ||||
|       expect(extrusions).toHaveLength(2) | ||||
|       extrusions.forEach((extrusion, index) => { | ||||
|         if (err(extrusion)) throw extrusion | ||||
|         expect(extrusion.type).toBe('sweep') | ||||
|         const firstExtrusionIsACubeIE6Sides = 6 | ||||
|         // Each face of the triangular prism (5), but without the bottom cap. | ||||
|         // The engine doesn't generate that. | ||||
|         const secondExtrusionIsATriangularPrism = 4 | ||||
|         expect(extrusion.surfaces.length).toBe( | ||||
|           !index | ||||
|             ? firstExtrusionIsACubeIE6Sides | ||||
|             : secondExtrusionIsATriangularPrism | ||||
|         ) | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it('there should be 5 + 4 segments,  4 (+close) from the first extrusion and 3 (+close) from the second', () => { | ||||
|       const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map( | ||||
|         (segment) => expandSegment(segment[1], theMap) | ||||
|       ) | ||||
|       expect(segments).toHaveLength(9) | ||||
|     }) | ||||
|  | ||||
|     it('snapshot of the artifactGraph', () => { | ||||
|       const stableMap = new Map( | ||||
|         [...theMap].map(([, artifact], index): [string, any] => { | ||||
|           const stableValue: any = {} | ||||
|           Object.entries(artifact).forEach(([propName, value]) => { | ||||
|             if ( | ||||
|               propName === 'type' || | ||||
|               propName === 'codeRef' || | ||||
|               propName === 'subType' | ||||
|             ) { | ||||
|               stableValue[propName] = value | ||||
|               return | ||||
|             } | ||||
|             if (Array.isArray(value)) | ||||
|               stableValue[propName] = value.map(() => 'UUID') | ||||
|             if (typeof value === 'string' && value) | ||||
|               stableValue[propName] = 'UUID' | ||||
|           }) | ||||
|           return [`UUID-${index}`, stableValue] | ||||
|         }) | ||||
|       ) | ||||
|       expect(stableMap).toMatchSnapshot() | ||||
|     }) | ||||
|  | ||||
|     it('screenshot graph', async () => { | ||||
|       // Ostensibly this takes a screen shot of the graph of the artifactGraph | ||||
|       // but it's it also tests that all of the id links are correct because if one | ||||
|       // of the edges refers to a non-existent node, the graph will throw. | ||||
|       // further more we can check that each edge is bi-directional, if it's not | ||||
|       // by checking the arrow heads going both ways, on the graph. | ||||
|       await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png') | ||||
|     }, 20000) | ||||
|   }) | ||||
|  | ||||
|   describe(`code with sketches but no extrusions or other 3D elements`, () => { | ||||
|     let ast: Node<Program> | ||||
|     let theMap: ReturnType<typeof createArtifactGraph> | ||||
|     it(`setup`, () => { | ||||
|       // putting this logic in here because describe blocks runs before beforeAll has finished | ||||
|       const { | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast: _ast, | ||||
|         execStateArtifacts, | ||||
|       } = getCommands('exampleCodeNo3D') | ||||
|       ast = _ast | ||||
|       theMap = createArtifactGraph({ | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast, | ||||
|         execStateArtifacts, | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it('there should be two planes, one for each sketch path', () => { | ||||
|       const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map( | ||||
|         (plane) => expandPlane(plane[1], theMap) | ||||
|       ) | ||||
|       expect(planes).toHaveLength(2) | ||||
|       planes.forEach((path) => { | ||||
|         expect(path.type).toBe('plane') | ||||
|       }) | ||||
|     }) | ||||
|     it('there should be two paths, one on each plane', () => { | ||||
|       const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map( | ||||
|         (path) => expandPath(path[1], theMap) | ||||
|       ) | ||||
|       expect(paths).toHaveLength(2) | ||||
|       paths.forEach((path) => { | ||||
|         if (err(path)) throw path | ||||
|         expect(path.type).toBe('path') | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     it(`there should be 1 solid2D, just for the first closed path`, () => { | ||||
|       const solid2Ds = [...filterArtifacts({ types: ['solid2D'] }, theMap)] | ||||
|       expect(solid2Ds).toHaveLength(1) | ||||
|     }) | ||||
|  | ||||
|     it('there should be no extrusions', () => { | ||||
|       const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map( | ||||
|         (extrusion) => expandSweep(extrusion[1], theMap) | ||||
|       ) | ||||
|       expect(extrusions).toHaveLength(0) | ||||
|     }) | ||||
|  | ||||
|     it('there should be 8 segments, 4 + 1 (close) from the first sketch and 3 from the second', () => { | ||||
|       const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map( | ||||
|         (segment) => expandSegment(segment[1], theMap) | ||||
|       ) | ||||
|       expect(segments).toHaveLength(8) | ||||
|     }) | ||||
|  | ||||
|     it('screenshot graph', async () => { | ||||
|       // Ostensibly this takes a screen shot of the graph of the artifactGraph | ||||
|       // but it's it also tests that all of the id links are correct because if one | ||||
|       // of the edges refers to a non-existent node, the graph will throw. | ||||
|       // further more we can check that each edge is bi-directional, if it's not | ||||
|       // by checking the arrow heads going both ways, on the graph. | ||||
|       await GraphTheGraph(theMap, 2000, 2000, 'exampleCodeNo3D.png') | ||||
|     }, 20000) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| describe('capture graph of sketchOnFaceOnFace...', () => { | ||||
|   describe('code with an extrusion, fillet and sketch of face:', () => { | ||||
|     let ast: Node<Program> | ||||
|     let theMap: ReturnType<typeof createArtifactGraph> | ||||
|     it('setup', async () => { | ||||
|       // putting this logic in here because describe blocks runs before beforeAll has finished | ||||
|       const { | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast: _ast, | ||||
|         execStateArtifacts, | ||||
|       } = getCommands('sketchOnFaceOnFaceEtc') | ||||
|       ast = _ast | ||||
|       theMap = createArtifactGraph({ | ||||
|         artifactCommands, | ||||
|         responseMap, | ||||
|         ast, | ||||
|         execStateArtifacts, | ||||
|       }) | ||||
|  | ||||
|       // Ostensibly this takes a screen shot of the graph of the artifactGraph | ||||
|       // but it's it also tests that all of the id links are correct because if one | ||||
|       // of the edges refers to a non-existent node, the graph will throw. | ||||
|       // further more we can check that each edge is bi-directional, if it's not | ||||
|       // by checking the arrow heads going both ways, on the graph. | ||||
|       await GraphTheGraph(theMap, 3000, 3000, 'sketchOnFaceOnFaceEtc.png') | ||||
|     }, 20000) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| function getCommands( | ||||
|   codeKey: CodeKey | ||||
| ): CacheShape[CodeKey] & { ast: Node<Program> } { | ||||
|   const ast = assertParse(codeKey) | ||||
|   const file = fs.readFileSync(fullPath, 'utf-8') | ||||
|   const parsed: CacheShape = JSON.parse(file) | ||||
|   // these either already exist from the last run, or were created in | ||||
|   const artifactCommands = parsed[codeKey].artifactCommands | ||||
|   const responseMap = parsed[codeKey].responseMap | ||||
|   const execStateArtifacts = parsed[codeKey].execStateArtifacts | ||||
|   return { | ||||
|     artifactCommands, | ||||
|     responseMap, | ||||
|     ast, | ||||
|     execStateArtifacts, | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function GraphTheGraph( | ||||
|   theMap: ArtifactGraph, | ||||
|   sizeX: number, | ||||
|   sizeY: number, | ||||
|   imageName: string | ||||
| ) { | ||||
|   const nodes: Array<{ id: string; label: string }> = [] | ||||
|   const edges: Array<{ source: string; target: string; label: string }> = [] | ||||
|   let index = 0 | ||||
|   for (const [commandId, artifact] of theMap) { | ||||
|     nodes.push({ | ||||
|       id: commandId, | ||||
|       label: `${artifact.type}-${index++}`, | ||||
|     }) | ||||
|     Object.entries(artifact).forEach(([propName, value]) => { | ||||
|       if ( | ||||
|         propName === 'type' || | ||||
|         propName === 'codeRef' || | ||||
|         propName === 'subType' || | ||||
|         propName === 'id' | ||||
|       ) | ||||
|         return | ||||
|       if (Array.isArray(value)) | ||||
|         value.forEach((v) => { | ||||
|           v && edges.push({ source: commandId, target: v, label: propName }) | ||||
|         }) | ||||
|       if (typeof value === 'string' && value) | ||||
|         edges.push({ source: commandId, target: value, label: propName }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   // Create a force simulation to calculate node positions | ||||
|   const simulation = d3 | ||||
|     .forceSimulation(nodes as any) | ||||
|     .force( | ||||
|       'link', | ||||
|       d3 | ||||
|         .forceLink(edges) | ||||
|         .id((d: any) => d.id) | ||||
|         .distance(100) | ||||
|     ) | ||||
|     .force('charge', d3.forceManyBody().strength(-300)) | ||||
|     .force('center', d3.forceCenter(300, 200)) | ||||
|     .stop() | ||||
|  | ||||
|   // Run the simulation | ||||
|   for (let i = 0; i < 300; ++i) simulation.tick() | ||||
|  | ||||
|   // Create traces for Plotly | ||||
|   const nodeTrace = { | ||||
|     x: nodes.map((node: any) => node.x), | ||||
|     y: nodes.map((node: any) => node.y), | ||||
|     text: nodes.map((node) => node.label), // Use the custom label | ||||
|     mode: 'markers+text', | ||||
|     type: 'scatter', | ||||
|     marker: { size: 20, color: 'gray' }, // Nodes in gray | ||||
|     textfont: { size: 14, color: 'black' }, // Labels in black | ||||
|     textposition: 'top center', // Position text on top | ||||
|   } | ||||
|  | ||||
|   const edgeTrace = { | ||||
|     x: [], | ||||
|     y: [], | ||||
|     mode: 'lines', | ||||
|     type: 'scatter', | ||||
|     line: { width: 2, color: 'lightgray' }, // Edges in light gray | ||||
|   } | ||||
|  | ||||
|   const annotations: any[] = [] | ||||
|  | ||||
|   edges.forEach((edge) => { | ||||
|     const sourceNode = nodes.find( | ||||
|       (node: any) => node.id === (edge as any).source.id | ||||
|     ) | ||||
|     const targetNode = nodes.find( | ||||
|       (node: any) => node.id === (edge as any).target.id | ||||
|     ) | ||||
|  | ||||
|     // Check if nodes are found | ||||
|     if (!sourceNode || !targetNode) { | ||||
|       throw new Error( | ||||
|         // @ts-ignore | ||||
|         `Node not found: ${!sourceNode ? edge.source.id : edge.target.id}` | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     // @ts-ignore | ||||
|     edgeTrace.x.push(sourceNode.x, targetNode.x, null) | ||||
|     // @ts-ignore | ||||
|     edgeTrace.y.push(sourceNode.y, targetNode.y, null) | ||||
|  | ||||
|     // Calculate offset for arrowhead | ||||
|     const offsetFactor = 0.9 // Adjust this factor to control the offset distance | ||||
|     // @ts-ignore | ||||
|     const offsetX = (targetNode.x - sourceNode.x) * offsetFactor | ||||
|     // @ts-ignore | ||||
|     const offsetY = (targetNode.y - sourceNode.y) * offsetFactor | ||||
|  | ||||
|     // Add arrowhead annotation with offset | ||||
|     annotations.push({ | ||||
|       // @ts-ignore | ||||
|       ax: sourceNode.x, | ||||
|       // @ts-ignore | ||||
|       ay: sourceNode.y, | ||||
|       // @ts-ignore | ||||
|       x: targetNode.x - offsetX, | ||||
|       // @ts-ignore | ||||
|       y: targetNode.y - offsetY, | ||||
|       xref: 'x', | ||||
|       yref: 'y', | ||||
|       axref: 'x', | ||||
|       ayref: 'y', | ||||
|       showarrow: true, | ||||
|       arrowhead: 2, | ||||
|       arrowsize: 1, | ||||
|       arrowwidth: 2, | ||||
|       arrowcolor: 'darkgray', // Arrowheads in dark gray | ||||
|     }) | ||||
|  | ||||
|     // Add edge label annotation closer to the edge tail (25% of the length) | ||||
|     // @ts-ignore | ||||
|     const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25 | ||||
|     // @ts-ignore | ||||
|     const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25 | ||||
|     annotations.push({ | ||||
|       x: labelX, | ||||
|       y: labelY, | ||||
|       xref: 'x', | ||||
|       yref: 'y', | ||||
|       text: edge.label, | ||||
|       showarrow: false, | ||||
|       font: { size: 12, color: 'black' }, // Edge labels in black | ||||
|       align: 'center', | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   const data = [edgeTrace, nodeTrace] | ||||
|  | ||||
|   const layout = { | ||||
|     // title: 'Force-Directed Graph with Nodes and Edges', | ||||
|     xaxis: { showgrid: false, zeroline: false, showticklabels: false }, | ||||
|     yaxis: { showgrid: false, zeroline: false, showticklabels: false }, | ||||
|     showlegend: false, | ||||
|     annotations: annotations, | ||||
|   } | ||||
|  | ||||
|   // Export to PNG using Playwright | ||||
|   const browser = await chromium.launch() | ||||
|   const page = await browser.newPage() | ||||
|   await page.setContent(` | ||||
|     <html> | ||||
|       <head> | ||||
|         <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> | ||||
|       </head> | ||||
|       <body> | ||||
|         <div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div> | ||||
|         <script> | ||||
|           Plotly.newPlot('plotly-graph', ${JSON.stringify( | ||||
|             data | ||||
|           )}, ${JSON.stringify(layout)}) | ||||
|         </script> | ||||
|       </body> | ||||
|     </html> | ||||
|   `) | ||||
|   await page.waitForSelector('#plotly-graph') | ||||
|   const element = await page.$('#plotly-graph') | ||||
|  | ||||
|   // @ts-ignore | ||||
|   await element.screenshot({ | ||||
|     path: `./e2e/playwright/temp3.png`, | ||||
|   }) | ||||
|  | ||||
|   await browser.close() | ||||
|  | ||||
|   const originalImgPath = path.resolve( | ||||
|     `./src/lang/std/artifactMapGraphs/${imageName}` | ||||
|   ) | ||||
|   // chop the top 30 pixels off the image | ||||
|   const originalImgExists = fs.existsSync(originalImgPath) | ||||
|   const originalImg = originalImgExists | ||||
|     ? PNG.sync.read(fs.readFileSync(originalImgPath)) | ||||
|     : null | ||||
|   // const img1Data = new Uint8Array(img1.data) | ||||
|   // const img1DataChopped = img1Data.slice(30 * img1.width * 4) | ||||
|   // img1.data = Buffer.from(img1DataChopped) | ||||
|  | ||||
|   const newImagePath = path.resolve('./e2e/playwright/temp3.png') | ||||
|   const newImage = PNG.sync.read(fs.readFileSync(newImagePath)) | ||||
|   const newImageData = new Uint8Array(newImage.data) | ||||
|   const newImageDataChopped = newImageData.slice(30 * newImage.width * 4) | ||||
|   newImage.data = Buffer.from(newImageDataChopped) | ||||
|  | ||||
|   const { width, height } = originalImg ?? newImage | ||||
|   const diff = new PNG({ width, height }) | ||||
|  | ||||
|   const imageSizeDifferent = originalImg?.data.length !== newImage.data.length | ||||
|   let numDiffPixels = 0 | ||||
|   if (!imageSizeDifferent) { | ||||
|     numDiffPixels = pixelmatch( | ||||
|       originalImg.data, | ||||
|       newImage.data, | ||||
|       diff.data, | ||||
|       width, | ||||
|       height, | ||||
|       { | ||||
|         threshold: 0.1, | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   if (numDiffPixels > 10 || imageSizeDifferent) { | ||||
|     console.warn('numDiffPixels', numDiffPixels) | ||||
|     // write file out to final place | ||||
|     fs.writeFileSync( | ||||
|       `src/lang/std/artifactMapGraphs/${imageName}`, | ||||
|       PNG.sync.write(newImage) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| describe('testing getArtifactsToUpdate', () => { | ||||
|   it('should return an array of artifacts to update', () => { | ||||
|     const { artifactCommands, responseMap, ast, execStateArtifacts } = | ||||
|       getCommands('exampleCode1') | ||||
|     const map = createArtifactGraph({ | ||||
|       artifactCommands, | ||||
|       responseMap, | ||||
|       ast, | ||||
|       execStateArtifacts, | ||||
|     }) | ||||
|     const getArtifact = (id: string) => map.get(id) | ||||
|     const currentPlaneId = 'UUID-1' | ||||
|     const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => { | ||||
|       const artifactCommand = artifactCommands.find( | ||||
|         (a) => a.command.type === type | ||||
|       ) | ||||
|       if (!artifactCommand) { | ||||
|         throw new Error(`No artifactCommand found for ${type}`) | ||||
|       } | ||||
|       const artifactsToUpdate = getArtifactsToUpdate({ | ||||
|         artifactCommand, | ||||
|         responseMap, | ||||
|         getArtifact, | ||||
|         currentPlaneId, | ||||
|         ast, | ||||
|         execStateArtifacts, | ||||
|       }) | ||||
|       return artifactsToUpdate.map(({ artifact }) => artifact) | ||||
|     } | ||||
|     expect(getUpdateObjects('start_path')).toEqual([ | ||||
|       { | ||||
|         type: 'path', | ||||
|         segIds: [], | ||||
|         id: expect.any(String), | ||||
|         planeId: 'UUID-1', | ||||
|         sweepId: undefined, | ||||
|         codeRef: { | ||||
|           pathToNode: [['body', '']], | ||||
|           range: [37, 64, true], | ||||
|         }, | ||||
|       }, | ||||
|     ]) | ||||
|     expect(getUpdateObjects('extrude')).toEqual([ | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         pathId: expect.any(String), | ||||
|         id: expect.any(String), | ||||
|         surfaceIds: [], | ||||
|         edgeIds: [], | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'path', | ||||
|         id: expect.any(String), | ||||
|         segIds: expect.any(Array), | ||||
|         planeId: expect.any(String), | ||||
|         sweepId: expect.any(String), | ||||
|         codeRef: { | ||||
|           range: [37, 64, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|         solid2dId: expect.any(String), | ||||
|       }, | ||||
|     ]) | ||||
|     expect(getUpdateObjects('extend_path')).toEqual([ | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: undefined, | ||||
|         edgeIds: [], | ||||
|         codeRef: { | ||||
|           range: [70, 86, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'path', | ||||
|         id: expect.any(String), | ||||
|         segIds: expect.any(Array), | ||||
|         planeId: expect.any(String), | ||||
|         sweepId: expect.any(String), | ||||
|         codeRef: { | ||||
|           range: [37, 64, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|         solid2dId: expect.any(String), | ||||
|       }, | ||||
|     ]) | ||||
|     expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([ | ||||
|       { | ||||
|         type: 'edgeCut', | ||||
|         subType: 'fillet', | ||||
|         id: expect.any(String), | ||||
|         consumedEdgeId: expect.any(String), | ||||
|         edgeIds: [], | ||||
|         surfaceId: undefined, | ||||
|         codeRef: { | ||||
|           range: [260, 299, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: expect.any(String), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [92, 119, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|         edgeCutId: expect.any(String), | ||||
|       }, | ||||
|     ]) | ||||
|     expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([ | ||||
|       { | ||||
|         type: 'wall', | ||||
|         id: expect.any(String), | ||||
|         segId: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: expect.any(String), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [156, 203, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'wall', | ||||
|         id: expect.any(String), | ||||
|         segId: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: expect.any(String), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [125, 150, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'wall', | ||||
|         id: expect.any(String), | ||||
|         segId: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: expect.any(String), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [92, 119, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|         edgeCutId: expect.any(String), | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'wall', | ||||
|         id: expect.any(String), | ||||
|         segId: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'segment', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceId: expect.any(String), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [70, 86, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'cap', | ||||
|         subType: 'start', | ||||
|         id: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: 'cap', | ||||
|         subType: 'end', | ||||
|         id: expect.any(String), | ||||
|         edgeCutEdgeIds: [], | ||||
|         sweepId: expect.any(String), | ||||
|         pathIds: [], | ||||
|       }, | ||||
|       { | ||||
|         type: 'sweep', | ||||
|         subType: 'extrusion', | ||||
|         id: expect.any(String), | ||||
|         pathId: expect.any(String), | ||||
|         surfaceIds: expect.any(Array), | ||||
|         edgeIds: expect.any(Array), | ||||
|         codeRef: { | ||||
|           range: [231, 254, true], | ||||
|           pathToNode: [['body', '']], | ||||
|         }, | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
| }) | ||||
| @ -1,17 +1,25 @@ | ||||
| import { | ||||
|   ArtifactCommand, | ||||
|   ExecState, | ||||
|   Artifact, | ||||
|   ArtifactGraph, | ||||
|   ArtifactId, | ||||
|   PathToNode, | ||||
|   Program, | ||||
|   SourceRange, | ||||
|   sourceRangeFromRust, | ||||
|   PathArtifact, | ||||
|   PlaneArtifact, | ||||
|   WallArtifact, | ||||
|   SegmentArtifact, | ||||
|   Solid2dArtifact as Solid2D, | ||||
|   SweepArtifact, | ||||
|   SweepEdge, | ||||
|   CapArtifact, | ||||
|   EdgeCut, | ||||
| } from 'lang/wasm' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { getNodePathFromSourceRange } from 'lang/queryAst' | ||||
| import { err } from 'lib/trap' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
|  | ||||
| export type ArtifactId = string | ||||
| export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' | ||||
|  | ||||
| interface BaseArtifact { | ||||
|   id: ArtifactId | ||||
| @ -22,30 +30,12 @@ export interface CodeRef { | ||||
|   pathToNode: PathToNode | ||||
| } | ||||
|  | ||||
| export interface PlaneArtifact extends BaseArtifact { | ||||
|   type: 'plane' | ||||
|   pathIds: Array<ArtifactId> | ||||
|   codeRef: CodeRef | ||||
| } | ||||
| export interface PlaneArtifactRich extends BaseArtifact { | ||||
|   type: 'plane' | ||||
|   paths: Array<PathArtifact> | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| export interface PathArtifact extends BaseArtifact { | ||||
|   type: 'path' | ||||
|   planeId: ArtifactId | ||||
|   segIds: Array<ArtifactId> | ||||
|   sweepId?: ArtifactId | ||||
|   solid2dId?: ArtifactId | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| interface solid2D extends BaseArtifact { | ||||
|   type: 'solid2D' | ||||
|   pathId: ArtifactId | ||||
| } | ||||
| export interface PathArtifactRich extends BaseArtifact { | ||||
|   type: 'path' | ||||
|   /** A path must always lie on a plane */ | ||||
| @ -53,18 +43,10 @@ export interface PathArtifactRich extends BaseArtifact { | ||||
|   /** A path must always contain 0 or more segments */ | ||||
|   segments: Array<SegmentArtifact> | ||||
|   /** A path may not result in a sweep artifact */ | ||||
|   sweep?: SweepArtifact | ||||
|   sweep: SweepArtifact | null | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| export interface SegmentArtifact extends BaseArtifact { | ||||
|   type: 'segment' | ||||
|   pathId: ArtifactId | ||||
|   surfaceId?: ArtifactId | ||||
|   edgeIds: Array<ArtifactId> | ||||
|   edgeCutId?: ArtifactId | ||||
|   codeRef: CodeRef | ||||
| } | ||||
| interface SegmentArtifactRich extends BaseArtifact { | ||||
|   type: 'segment' | ||||
|   path: PathArtifact | ||||
| @ -74,15 +56,6 @@ interface SegmentArtifactRich extends BaseArtifact { | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/ | ||||
| interface SweepArtifact extends BaseArtifact { | ||||
|   type: 'sweep' | ||||
|   subType: 'extrusion' | 'revolve' | 'loft' | 'sweep' | ||||
|   pathId: string | ||||
|   surfaceIds: Array<string> | ||||
|   edgeIds: Array<string> | ||||
|   codeRef: CodeRef | ||||
| } | ||||
| interface SweepArtifactRich extends BaseArtifact { | ||||
|   type: 'sweep' | ||||
|   subType: 'extrusion' | 'revolve' | 'loft' | 'sweep' | ||||
| @ -92,58 +65,6 @@ interface SweepArtifactRich extends BaseArtifact { | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| interface WallArtifact extends BaseArtifact { | ||||
|   type: 'wall' | ||||
|   segId: ArtifactId | ||||
|   edgeCutEdgeIds: Array<ArtifactId> | ||||
|   sweepId: ArtifactId | ||||
|   pathIds: Array<ArtifactId> | ||||
| } | ||||
| interface CapArtifact extends BaseArtifact { | ||||
|   type: 'cap' | ||||
|   subType: 'start' | 'end' | ||||
|   edgeCutEdgeIds: Array<ArtifactId> | ||||
|   sweepId: ArtifactId | ||||
|   pathIds: Array<ArtifactId> | ||||
| } | ||||
|  | ||||
| interface SweepEdge extends BaseArtifact { | ||||
|   type: 'sweepEdge' | ||||
|   segId: ArtifactId | ||||
|   sweepId: ArtifactId | ||||
|   subType: 'opposite' | 'adjacent' | ||||
| } | ||||
|  | ||||
| /** A edgeCut is a more generic term for both fillet or chamfer */ | ||||
| interface EdgeCut extends BaseArtifact { | ||||
|   type: 'edgeCut' | ||||
|   subType: 'fillet' | 'chamfer' | ||||
|   consumedEdgeId: ArtifactId | ||||
|   edgeIds: Array<ArtifactId> | ||||
|   surfaceId?: ArtifactId | ||||
|   codeRef: CodeRef | ||||
| } | ||||
|  | ||||
| interface EdgeCutEdge extends BaseArtifact { | ||||
|   type: 'edgeCutEdge' | ||||
|   edgeCutId: ArtifactId | ||||
|   surfaceId: ArtifactId | ||||
| } | ||||
|  | ||||
| export type Artifact = | ||||
|   | PlaneArtifact | ||||
|   | PathArtifact | ||||
|   | SegmentArtifact | ||||
|   | SweepArtifact | ||||
|   | WallArtifact | ||||
|   | CapArtifact | ||||
|   | SweepEdge | ||||
|   | EdgeCut | ||||
|   | EdgeCutEdge | ||||
|   | solid2D | ||||
|  | ||||
| export type ArtifactGraph = Map<ArtifactId, Artifact> | ||||
|  | ||||
| export type EngineCommand = Models['WebSocketRequest_type'] | ||||
|  | ||||
| type OkWebSocketResponseData = Models['OkWebSocketResponseData_type'] | ||||
| @ -152,437 +73,6 @@ export interface ResponseMap { | ||||
|   [commandId: string]: OkWebSocketResponseData | ||||
| } | ||||
|  | ||||
| /** Creates a graph of artifacts from a list of ordered commands and their responses | ||||
|  * muting the Map should happen entirely this function, other functions called within | ||||
|  * should return data on how to update the map, and not do so directly. | ||||
|  */ | ||||
| export function createArtifactGraph({ | ||||
|   artifactCommands, | ||||
|   responseMap, | ||||
|   ast, | ||||
|   execStateArtifacts, | ||||
| }: { | ||||
|   artifactCommands: Array<ArtifactCommand> | ||||
|   responseMap: ResponseMap | ||||
|   ast: Node<Program> | ||||
|   execStateArtifacts: ExecState['artifacts'] | ||||
| }) { | ||||
|   const myMap = new Map<ArtifactId, Artifact>() | ||||
|  | ||||
|   /** see docstring for {@link getArtifactsToUpdate} as to why this is needed */ | ||||
|   let currentPlaneId = '' | ||||
|  | ||||
|   for (const artifactCommand of artifactCommands) { | ||||
|     if (artifactCommand.command.type === 'enable_sketch_mode') { | ||||
|       currentPlaneId = artifactCommand.command.entity_id | ||||
|     } | ||||
|     if (artifactCommand.command.type === 'sketch_mode_disable') { | ||||
|       currentPlaneId = '' | ||||
|     } | ||||
|     const artifactsToUpdate = getArtifactsToUpdate({ | ||||
|       artifactCommand, | ||||
|       responseMap, | ||||
|       getArtifact: (id: ArtifactId) => myMap.get(id), | ||||
|       currentPlaneId, | ||||
|       ast, | ||||
|       execStateArtifacts, | ||||
|     }) | ||||
|     artifactsToUpdate.forEach(({ id, artifact }) => { | ||||
|       const mergedArtifact = mergeArtifacts(myMap.get(id), artifact) | ||||
|       myMap.set(id, mergedArtifact) | ||||
|     }) | ||||
|   } | ||||
|   return myMap | ||||
| } | ||||
|  | ||||
| /** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that | ||||
|  * but maybe types can be improved here. | ||||
|  */ | ||||
| function mergeArtifacts( | ||||
|   oldArtifact: Artifact | undefined, | ||||
|   newArtifact: Artifact | ||||
| ): Artifact { | ||||
|   // only has string and array of strings | ||||
|   interface GenericArtifact { | ||||
|     [key: string]: string | Array<string> | ||||
|   } | ||||
|   if (!oldArtifact) return newArtifact | ||||
|   // merging artifacts of different types should never happen, but if it does, just return the new artifact | ||||
|   if (oldArtifact.type !== newArtifact.type) return newArtifact | ||||
|   const _oldArtifact = oldArtifact as any as GenericArtifact | ||||
|   const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact | ||||
|   Object.entries(newArtifact as any as GenericArtifact).forEach( | ||||
|     ([propName, value]) => { | ||||
|       const otherValue = _oldArtifact[propName] | ||||
|       if (Array.isArray(value) && Array.isArray(otherValue)) { | ||||
|         mergedArtifact[propName] = [...new Set([...otherValue, ...value])] | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
|   return mergedArtifact as any as Artifact | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Processes a single command and it's response in order to populate the artifact map | ||||
|  * It does not mutate the map directly, but returns an array of artifacts to update | ||||
|  * | ||||
|  * @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId | ||||
|  * instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of | ||||
|  * outside of this function. It would be good to update the `start_path` command to include the planeId so we | ||||
|  * can remove this. | ||||
|  */ | ||||
| export function getArtifactsToUpdate({ | ||||
|   artifactCommand, | ||||
|   getArtifact, | ||||
|   responseMap, | ||||
|   currentPlaneId, | ||||
|   ast, | ||||
|   execStateArtifacts, | ||||
| }: { | ||||
|   artifactCommand: ArtifactCommand | ||||
|   responseMap: ResponseMap | ||||
|   /** Passing in a getter because we don't wan this function to update the map directly */ | ||||
|   getArtifact: (id: ArtifactId) => Artifact | undefined | ||||
|   currentPlaneId: ArtifactId | ||||
|   ast: Node<Program> | ||||
|   execStateArtifacts: ExecState['artifacts'] | ||||
| }): Array<{ | ||||
|   id: ArtifactId | ||||
|   artifact: Artifact | ||||
| }> { | ||||
|   const range = sourceRangeFromRust(artifactCommand.range) | ||||
|   const pathToNode = getNodePathFromSourceRange(ast, range) | ||||
|  | ||||
|   const id = artifactCommand.cmdId | ||||
|   const response = responseMap[id] | ||||
|   const cmd = artifactCommand.command | ||||
|   const returnArr: ReturnType<typeof getArtifactsToUpdate> = [] | ||||
|   if (!response) return returnArr | ||||
|   if (cmd.type === 'make_plane' && range[1] !== 0) { | ||||
|     // If we're calling `make_plane` and the code range doesn't end at `0` | ||||
|     // it's not a default plane, but a custom one from the offsetPlane standard library function | ||||
|     return [ | ||||
|       { | ||||
|         id, | ||||
|         artifact: { | ||||
|           type: 'plane', | ||||
|           id, | ||||
|           pathIds: [], | ||||
|           codeRef: { range, pathToNode }, | ||||
|         }, | ||||
|       }, | ||||
|     ] | ||||
|   } else if (cmd.type === 'enable_sketch_mode') { | ||||
|     const plane = getArtifact(currentPlaneId) | ||||
|     const pathIds = plane?.type === 'plane' ? plane?.pathIds : [] | ||||
|     const codeRef = | ||||
|       plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode } | ||||
|     const existingPlane = getArtifact(currentPlaneId) | ||||
|     if (existingPlane?.type === 'wall') { | ||||
|       return [ | ||||
|         { | ||||
|           id: currentPlaneId, | ||||
|           artifact: { | ||||
|             type: 'wall', | ||||
|             id: currentPlaneId, | ||||
|             segId: existingPlane.segId, | ||||
|             edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, | ||||
|             sweepId: existingPlane.sweepId, | ||||
|             pathIds: existingPlane.pathIds, | ||||
|           }, | ||||
|         }, | ||||
|       ] | ||||
|     } else { | ||||
|       return [ | ||||
|         { | ||||
|           id: currentPlaneId, | ||||
|           artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef }, | ||||
|         }, | ||||
|       ] | ||||
|     } | ||||
|   } else if (cmd.type === 'start_path') { | ||||
|     returnArr.push({ | ||||
|       id, | ||||
|       artifact: { | ||||
|         type: 'path', | ||||
|         id, | ||||
|         segIds: [], | ||||
|         planeId: currentPlaneId, | ||||
|         sweepId: undefined, | ||||
|         codeRef: { range, pathToNode }, | ||||
|       }, | ||||
|     }) | ||||
|     const plane = getArtifact(currentPlaneId) | ||||
|     const codeRef = | ||||
|       plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode } | ||||
|     if (plane?.type === 'plane') { | ||||
|       returnArr.push({ | ||||
|         id: currentPlaneId, | ||||
|         artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef }, | ||||
|       }) | ||||
|     } | ||||
|     if (plane?.type === 'wall') { | ||||
|       returnArr.push({ | ||||
|         id: currentPlaneId, | ||||
|         artifact: { | ||||
|           type: 'wall', | ||||
|           id: currentPlaneId, | ||||
|           segId: plane.segId, | ||||
|           edgeCutEdgeIds: plane.edgeCutEdgeIds, | ||||
|           sweepId: plane.sweepId, | ||||
|           pathIds: [id], | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|     return returnArr | ||||
|   } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') { | ||||
|     const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id | ||||
|     returnArr.push({ | ||||
|       id, | ||||
|       artifact: { | ||||
|         type: 'segment', | ||||
|         id, | ||||
|         pathId, | ||||
|         surfaceId: undefined, | ||||
|         edgeIds: [], | ||||
|         codeRef: { range, pathToNode }, | ||||
|       }, | ||||
|     }) | ||||
|     const path = getArtifact(pathId) | ||||
|     if (path?.type === 'path') | ||||
|       returnArr.push({ | ||||
|         id: pathId, | ||||
|         artifact: { ...path, segIds: [id] }, | ||||
|       }) | ||||
|     if ( | ||||
|       response?.type === 'modeling' && | ||||
|       response.data.modeling_response.type === 'close_path' | ||||
|     ) { | ||||
|       returnArr.push({ | ||||
|         id: response.data.modeling_response.data.face_id, | ||||
|         artifact: { | ||||
|           type: 'solid2D', | ||||
|           id: response.data.modeling_response.data.face_id, | ||||
|           pathId, | ||||
|         }, | ||||
|       }) | ||||
|       const path = getArtifact(pathId) | ||||
|       if (path?.type === 'path') | ||||
|         returnArr.push({ | ||||
|           id: pathId, | ||||
|           artifact: { | ||||
|             ...path, | ||||
|             solid2dId: response.data.modeling_response.data.face_id, | ||||
|           }, | ||||
|         }) | ||||
|     } | ||||
|     return returnArr | ||||
|   } else if ( | ||||
|     cmd.type === 'extrude' || | ||||
|     cmd.type === 'revolve' || | ||||
|     cmd.type === 'sweep' | ||||
|   ) { | ||||
|     const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type | ||||
|     returnArr.push({ | ||||
|       id, | ||||
|       artifact: { | ||||
|         type: 'sweep', | ||||
|         subType: subType, | ||||
|         id, | ||||
|         pathId: cmd.target, | ||||
|         surfaceIds: [], | ||||
|         edgeIds: [], | ||||
|         codeRef: { range, pathToNode }, | ||||
|       }, | ||||
|     }) | ||||
|     const path = getArtifact(cmd.target) | ||||
|     if (path?.type === 'path') | ||||
|       returnArr.push({ | ||||
|         id: cmd.target, | ||||
|         artifact: { ...path, sweepId: id }, | ||||
|       }) | ||||
|     return returnArr | ||||
|   } else if ( | ||||
|     cmd.type === 'loft' && | ||||
|     response.type === 'modeling' && | ||||
|     response.data.modeling_response.type === 'loft' | ||||
|   ) { | ||||
|     returnArr.push({ | ||||
|       id, | ||||
|       artifact: { | ||||
|         type: 'sweep', | ||||
|         subType: 'loft', | ||||
|         id, | ||||
|         // TODO: make sure to revisit this choice, don't think it matters for now | ||||
|         pathId: cmd.section_ids[0], | ||||
|         surfaceIds: [], | ||||
|         edgeIds: [], | ||||
|         codeRef: { range, pathToNode }, | ||||
|       }, | ||||
|     }) | ||||
|     for (const sectionId of cmd.section_ids) { | ||||
|       const path = getArtifact(sectionId) | ||||
|       if (path?.type === 'path') | ||||
|         returnArr.push({ | ||||
|           id: sectionId, | ||||
|           artifact: { ...path, sweepId: id }, | ||||
|         }) | ||||
|     } | ||||
|     return returnArr | ||||
|   } else if ( | ||||
|     cmd.type === 'solid3d_get_extrusion_face_info' && | ||||
|     response?.type === 'modeling' && | ||||
|     response.data.modeling_response.type === 'solid3d_get_extrusion_face_info' | ||||
|   ) { | ||||
|     let lastPath: PathArtifact | ||||
|     response.data.modeling_response.data.faces.forEach( | ||||
|       ({ curve_id, cap, face_id }) => { | ||||
|         if (cap === 'none' && curve_id && face_id) { | ||||
|           const seg = getArtifact(curve_id) | ||||
|           if (seg?.type !== 'segment') return | ||||
|           const path = getArtifact(seg.pathId) | ||||
|           if (path?.type === 'path' && seg?.type === 'segment') { | ||||
|             lastPath = path | ||||
|             returnArr.push({ | ||||
|               id: face_id, | ||||
|               artifact: { | ||||
|                 type: 'wall', | ||||
|                 id: face_id, | ||||
|                 segId: curve_id, | ||||
|                 edgeCutEdgeIds: [], | ||||
|                 // TODO: Add explicit check for sweepId.  Should never use '' | ||||
|                 sweepId: path.sweepId ?? '', | ||||
|                 pathIds: [], | ||||
|               }, | ||||
|             }) | ||||
|             returnArr.push({ | ||||
|               id: curve_id, | ||||
|               artifact: { ...seg, surfaceId: face_id }, | ||||
|             }) | ||||
|             if (path.sweepId) { | ||||
|               const sweep = getArtifact(path.sweepId) | ||||
|               if (sweep?.type === 'sweep') { | ||||
|                 returnArr.push({ | ||||
|                   id: path.sweepId, | ||||
|                   artifact: { | ||||
|                     ...sweep, | ||||
|                     surfaceIds: [face_id], | ||||
|                   }, | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ) | ||||
|     response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => { | ||||
|       if ((cap === 'top' || cap === 'bottom') && face_id) { | ||||
|         const path = lastPath | ||||
|         if (path?.type === 'path') { | ||||
|           returnArr.push({ | ||||
|             id: face_id, | ||||
|             artifact: { | ||||
|               type: 'cap', | ||||
|               id: face_id, | ||||
|               subType: cap === 'bottom' ? 'start' : 'end', | ||||
|               edgeCutEdgeIds: [], | ||||
|               // TODO: Add explicit check for sweepId.  Should never use '' | ||||
|               sweepId: path.sweepId ?? '', | ||||
|               pathIds: [], | ||||
|             }, | ||||
|           }) | ||||
|           if (path.sweepId) { | ||||
|             const sweep = getArtifact(path.sweepId) | ||||
|             if (sweep?.type !== 'sweep') return | ||||
|             returnArr.push({ | ||||
|               id: path.sweepId, | ||||
|               artifact: { | ||||
|                 ...sweep, | ||||
|                 surfaceIds: [face_id], | ||||
|               }, | ||||
|             }) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return returnArr | ||||
|   } else if ( | ||||
|     // is opposite edge | ||||
|     (cmd.type === 'solid3d_get_opposite_edge' && | ||||
|       response.type === 'modeling' && | ||||
|       response.data.modeling_response.type === 'solid3d_get_opposite_edge' && | ||||
|       response.data.modeling_response.data.edge) || | ||||
|     // or is adjacent edge | ||||
|     (cmd.type === 'solid3d_get_next_adjacent_edge' && | ||||
|       response.type === 'modeling' && | ||||
|       response.data.modeling_response.type === | ||||
|         'solid3d_get_next_adjacent_edge' && | ||||
|       response.data.modeling_response.data.edge) | ||||
|   ) { | ||||
|     const wall = getArtifact(cmd.face_id) | ||||
|     if (wall?.type !== 'wall') return returnArr | ||||
|     const sweep = getArtifact(wall.sweepId) | ||||
|     if (sweep?.type !== 'sweep') return returnArr | ||||
|     const path = getArtifact(sweep.pathId) | ||||
|     if (path?.type !== 'path') return returnArr | ||||
|     const segment = getArtifact(cmd.edge_id) | ||||
|     if (segment?.type !== 'segment') return returnArr | ||||
|  | ||||
|     return [ | ||||
|       { | ||||
|         id: response.data.modeling_response.data.edge, | ||||
|         artifact: { | ||||
|           type: 'sweepEdge', | ||||
|           id: response.data.modeling_response.data.edge, | ||||
|           subType: | ||||
|             cmd.type === 'solid3d_get_next_adjacent_edge' | ||||
|               ? 'adjacent' | ||||
|               : 'opposite', | ||||
|           segId: cmd.edge_id, | ||||
|           // TODO: Add explicit check for sweepId.  Should never use '' | ||||
|           sweepId: path.sweepId ?? '', | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         id: cmd.edge_id, | ||||
|         artifact: { | ||||
|           ...segment, | ||||
|           edgeIds: [response.data.modeling_response.data.edge], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         id: sweep.id, | ||||
|         artifact: { | ||||
|           ...sweep, | ||||
|           edgeIds: [response.data.modeling_response.data.edge], | ||||
|         }, | ||||
|       }, | ||||
|     ] | ||||
|   } else if (cmd.type === 'solid3d_fillet_edge') { | ||||
|     returnArr.push({ | ||||
|       id, | ||||
|       artifact: { | ||||
|         type: 'edgeCut', | ||||
|         id, | ||||
|         subType: cmd.cut_type, | ||||
|         consumedEdgeId: cmd.edge_id, | ||||
|         edgeIds: [], | ||||
|         surfaceId: undefined, | ||||
|         codeRef: { range, pathToNode }, | ||||
|       }, | ||||
|     }) | ||||
|     const consumedEdge = getArtifact(cmd.edge_id) | ||||
|     if (consumedEdge?.type === 'segment') { | ||||
|       returnArr.push({ | ||||
|         id: cmd.edge_id, | ||||
|         artifact: { ...consumedEdge, edgeCutId: id }, | ||||
|       }) | ||||
|     } | ||||
|     return returnArr | ||||
|   } | ||||
|   return [] | ||||
| } | ||||
|  | ||||
| /** filter map items of a specific type */ | ||||
| export function filterArtifacts<T extends Artifact['type'][]>( | ||||
|   { | ||||
| @ -676,7 +166,7 @@ export function expandPath( | ||||
|         }, | ||||
|         artifactGraph | ||||
|       ) | ||||
|     : undefined | ||||
|     : null | ||||
|   const plane = getArtifactOfTypes( | ||||
|     { key: path.planeId, types: ['plane', 'wall'] }, | ||||
|     artifactGraph | ||||
| @ -778,11 +268,11 @@ export function getCapCodeRef( | ||||
| } | ||||
|  | ||||
| export function getSolid2dCodeRef( | ||||
|   solid2D: solid2D, | ||||
|   solid2d: Solid2D, | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): CodeRef | Error { | ||||
|   const path = getArtifactOfTypes( | ||||
|     { key: solid2D.pathId, types: ['path'] }, | ||||
|     { key: solid2d.pathId, types: ['path'] }, | ||||
|     artifactGraph | ||||
|   ) | ||||
|   if (err(path)) return path | ||||
| @ -881,7 +371,7 @@ export function getCodeRefsByArtifactId( | ||||
|   artifactGraph: ArtifactGraph | ||||
| ): Array<CodeRef> | null { | ||||
|   const artifact = artifactGraph.get(id) | ||||
|   if (artifact?.type === 'solid2D') { | ||||
|   if (artifact?.type === 'solid2d') { | ||||
|     const codeRef = getSolid2dCodeRef(artifact, artifactGraph) | ||||
|     if (err(codeRef)) return null | ||||
|     return [codeRef] | ||||
|  | ||||
| @ -1,9 +1,7 @@ | ||||
| import { | ||||
|   ArtifactCommand, | ||||
|   defaultRustSourceRange, | ||||
|   ArtifactGraph, | ||||
|   defaultSourceRange, | ||||
|   ExecState, | ||||
|   Program, | ||||
|   RustSourceRange, | ||||
|   SourceRange, | ||||
| } from 'lang/wasm' | ||||
| import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' | ||||
| @ -17,12 +15,7 @@ import { | ||||
|   darkModeMatcher, | ||||
| } from 'lib/theme' | ||||
| import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' | ||||
| import { | ||||
|   ArtifactGraph, | ||||
|   EngineCommand, | ||||
|   ResponseMap, | ||||
|   createArtifactGraph, | ||||
| } from 'lang/std/artifactGraph' | ||||
| import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { exportMake } from 'lib/exportMake' | ||||
| import toast from 'react-hot-toast' | ||||
| @ -36,7 +29,6 @@ import { KclManager } from 'lang/KclSingleton' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { markOnce } from 'lib/performance' | ||||
| import { MachineManager } from 'components/MachineManagerProvider' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
|  | ||||
| // TODO(paultag): This ought to be tweakable. | ||||
| const pingIntervalMs = 5_000 | ||||
| @ -1022,6 +1014,11 @@ class EngineConnection extends EventTarget { | ||||
|               this.pingPongSpan.pong = new Date() | ||||
|               break | ||||
|  | ||||
|             case 'modeling_session_data': | ||||
|               let api_call_id = resp.data?.session?.api_call_id | ||||
|               console.log(`API Call ID: ${api_call_id}`) | ||||
|               break | ||||
|  | ||||
|             // Only fires on successful authentication. | ||||
|             case 'ice_server_info': | ||||
|               let ice_servers = resp.data?.ice_servers | ||||
| @ -1309,8 +1306,8 @@ export enum EngineCommandManagerEvents { | ||||
|  | ||||
| interface PendingMessage { | ||||
|   command: EngineCommand | ||||
|   range: RustSourceRange | ||||
|   idToRangeMap: { [key: string]: RustSourceRange } | ||||
|   range: SourceRange | ||||
|   idToRangeMap: { [key: string]: SourceRange } | ||||
|   resolve: (data: [Models['WebSocketResponse_type']]) => void | ||||
|   reject: (reason: string) => void | ||||
|   promise: Promise<[Models['WebSocketResponse_type']]> | ||||
| @ -1994,7 +1991,7 @@ export class EngineCommandManager extends EventTarget { | ||||
|       { | ||||
|         command, | ||||
|         idToRangeMap: {}, | ||||
|         range: defaultRustSourceRange(), | ||||
|         range: defaultSourceRange(), | ||||
|       }, | ||||
|       true // isSceneCommand | ||||
|     ) | ||||
| @ -2025,9 +2022,9 @@ export class EngineCommandManager extends EventTarget { | ||||
|       return Promise.reject(new Error('rangeStr is undefined')) | ||||
|     if (commandStr === undefined) | ||||
|       return Promise.reject(new Error('commandStr is undefined')) | ||||
|     const range: RustSourceRange = JSON.parse(rangeStr) | ||||
|     const range: SourceRange = JSON.parse(rangeStr) | ||||
|     const command: EngineCommand = JSON.parse(commandStr) | ||||
|     const idToRangeMap: { [key: string]: RustSourceRange } = | ||||
|     const idToRangeMap: { [key: string]: SourceRange } = | ||||
|       JSON.parse(idToRangeStr) | ||||
|  | ||||
|     // Current executeAst is stale, going to interrupt, a new executeAst will trigger | ||||
| @ -2087,17 +2084,8 @@ export class EngineCommandManager extends EventTarget { | ||||
|       Object.values(this.pendingCommands).map((a) => a.promise) | ||||
|     ) | ||||
|   } | ||||
|   updateArtifactGraph( | ||||
|     ast: Node<Program>, | ||||
|     artifactCommands: ArtifactCommand[], | ||||
|     execStateArtifacts: ExecState['artifacts'] | ||||
|   ) { | ||||
|     this.artifactGraph = createArtifactGraph({ | ||||
|       artifactCommands, | ||||
|       responseMap: this.responseMap, | ||||
|       ast, | ||||
|       execStateArtifacts, | ||||
|     }) | ||||
|   updateArtifactGraph(execStateArtifactGraph: ExecState['artifactGraph']) { | ||||
|     this.artifactGraph = execStateArtifactGraph | ||||
|     // TODO check if these still need to be deferred once e2e tests are working again. | ||||
|     if (this.artifactGraph.size) { | ||||
|       this.deferredArtifactEmptied(null) | ||||
|  | ||||
| @ -11,8 +11,8 @@ import { | ||||
|   assertParse, | ||||
|   recast, | ||||
|   initPromise, | ||||
|   SourceRange, | ||||
|   CallExpression, | ||||
|   topLevelRange, | ||||
| } from '../wasm' | ||||
| import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' | ||||
| import { enginelessExecutor } from '../../lib/testHelpers' | ||||
| @ -124,7 +124,10 @@ describe('testing changeSketchArguments', () => { | ||||
|       execState.memory, | ||||
|       { | ||||
|         type: 'sourceRange', | ||||
|         sourceRange: [sourceStart, sourceStart + lineToChange.length, true], | ||||
|         sourceRange: topLevelRange( | ||||
|           sourceStart, | ||||
|           sourceStart + lineToChange.length | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         type: 'straight-segment', | ||||
| @ -219,11 +222,10 @@ describe('testing addTagForSketchOnFace', () => { | ||||
|     const ast = assertParse(code) | ||||
|     await enginelessExecutor(ast) | ||||
|     const sourceStart = code.indexOf(originalLine) | ||||
|     const sourceRange: [number, number, boolean] = [ | ||||
|     const sourceRange = topLevelRange( | ||||
|       sourceStart, | ||||
|       sourceStart + originalLine.length, | ||||
|       true, | ||||
|     ] | ||||
|       sourceStart + originalLine.length | ||||
|     ) | ||||
|     if (err(ast)) return ast | ||||
|     const pathToNode = getNodePathFromSourceRange(ast, sourceRange) | ||||
|     const sketchOnFaceRetVal = addTagForSketchOnFace( | ||||
| @ -292,11 +294,10 @@ ${insertCode} | ||||
|       await enginelessExecutor(ast) | ||||
|       const sourceStart = code.indexOf(originalChamfer) | ||||
|       const extraChars = originalChamfer.indexOf('chamfer') | ||||
|       const sourceRange: [number, number, boolean] = [ | ||||
|       const sourceRange = topLevelRange( | ||||
|         sourceStart + extraChars, | ||||
|         sourceStart + originalChamfer.length - extraChars, | ||||
|         true, | ||||
|       ] | ||||
|         sourceStart + originalChamfer.length - extraChars | ||||
|       ) | ||||
|  | ||||
|       if (err(ast)) throw ast | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, sourceRange) | ||||
| @ -357,7 +358,6 @@ describe('testing getConstraintInfo', () => { | ||||
|     offset = 0 | ||||
|   }, %) | ||||
|   |> tangentialArcTo([3.14, 13.14], %)` | ||||
|     const ast = assertParse(code) | ||||
|     test.each([ | ||||
|       [ | ||||
|         'line', | ||||
| @ -366,7 +366,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3', | ||||
|             sourceRange: [78, 79, true], | ||||
|             sourceRange: topLevelRange(78, 79), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'line', | ||||
| @ -375,7 +375,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: false, | ||||
|             value: '4', | ||||
|             sourceRange: [81, 82, true], | ||||
|             sourceRange: topLevelRange(81, 82), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'line', | ||||
| @ -389,7 +389,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [118, 122, true], | ||||
|             sourceRange: topLevelRange(118, 122), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -398,7 +398,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'length', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [137, 141, true], | ||||
|             sourceRange: topLevelRange(137, 141), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -412,7 +412,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '6.14', | ||||
|             sourceRange: [164, 168, true], | ||||
|             sourceRange: topLevelRange(164, 168), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'lineTo', | ||||
| @ -421,7 +421,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [170, 174, true], | ||||
|             sourceRange: topLevelRange(170, 174), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'lineTo', | ||||
| @ -435,7 +435,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'horizontal', | ||||
|             isConstrained: true, | ||||
|             value: 'xLineTo', | ||||
|             sourceRange: [185, 192, true], | ||||
|             sourceRange: topLevelRange(185, 192), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLineTo', | ||||
| @ -444,7 +444,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '8', | ||||
|             sourceRange: [193, 194, true], | ||||
|             sourceRange: topLevelRange(193, 194), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLineTo', | ||||
| @ -458,7 +458,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'vertical', | ||||
|             isConstrained: true, | ||||
|             value: 'yLineTo', | ||||
|             sourceRange: [204, 211, true], | ||||
|             sourceRange: topLevelRange(204, 211), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLineTo', | ||||
| @ -467,7 +467,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '5', | ||||
|             sourceRange: [212, 213, true], | ||||
|             sourceRange: topLevelRange(212, 213), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLineTo', | ||||
| @ -481,7 +481,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'vertical', | ||||
|             isConstrained: true, | ||||
|             value: 'yLine', | ||||
|             sourceRange: [223, 228, true], | ||||
|             sourceRange: topLevelRange(223, 228), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLine', | ||||
| @ -490,7 +490,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [229, 233, true], | ||||
|             sourceRange: topLevelRange(229, 233), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLine', | ||||
| @ -504,7 +504,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'horizontal', | ||||
|             isConstrained: true, | ||||
|             value: 'xLine', | ||||
|             sourceRange: [247, 252, true], | ||||
|             sourceRange: topLevelRange(247, 252), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLine', | ||||
| @ -513,7 +513,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [253, 257, true], | ||||
|             sourceRange: topLevelRange(253, 257), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLine', | ||||
| @ -527,7 +527,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [301, 305, true], | ||||
|             sourceRange: topLevelRange(301, 305), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -536,7 +536,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [320, 324, true], | ||||
|             sourceRange: topLevelRange(320, 324), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -550,7 +550,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '30', | ||||
|             sourceRange: [373, 375, true], | ||||
|             sourceRange: topLevelRange(373, 375), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -559,7 +559,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3', | ||||
|             sourceRange: [390, 391, true], | ||||
|             sourceRange: topLevelRange(390, 391), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -573,7 +573,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '12.14', | ||||
|             sourceRange: [434, 439, true], | ||||
|             sourceRange: topLevelRange(434, 439), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -582,7 +582,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '12', | ||||
|             sourceRange: [450, 452, true], | ||||
|             sourceRange: topLevelRange(450, 452), | ||||
|             argPosition: { type: 'objectProperty', key: 'to' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -596,7 +596,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '30', | ||||
|             sourceRange: [495, 497, true], | ||||
|             sourceRange: topLevelRange(495, 497), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -605,7 +605,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '10.14', | ||||
|             sourceRange: [508, 513, true], | ||||
|             sourceRange: topLevelRange(508, 513), | ||||
|             argPosition: { type: 'objectProperty', key: 'to' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -619,7 +619,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [567, 571, true], | ||||
|             sourceRange: topLevelRange(567, 571), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineThatIntersects', | ||||
| @ -628,7 +628,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'intersectionOffset', | ||||
|             isConstrained: false, | ||||
|             value: '0', | ||||
|             sourceRange: [608, 609, true], | ||||
|             sourceRange: topLevelRange(608, 609), | ||||
|             argPosition: { type: 'objectProperty', key: 'offset' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineThatIntersects', | ||||
| @ -637,7 +637,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'intersectionTag', | ||||
|             isConstrained: false, | ||||
|             value: 'a', | ||||
|             sourceRange: [592, 593, true], | ||||
|             sourceRange: topLevelRange(592, 593), | ||||
|             argPosition: { | ||||
|               key: 'intersectTag', | ||||
|               type: 'objectProperty', | ||||
| @ -654,7 +654,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'tangentialWithPrevious', | ||||
|             isConstrained: true, | ||||
|             value: 'tangentialArcTo', | ||||
|             sourceRange: [623, 638, true], | ||||
|             sourceRange: topLevelRange(623, 638), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -663,7 +663,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [640, 644, true], | ||||
|             sourceRange: topLevelRange(640, 644), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -672,7 +672,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '13.14', | ||||
|             sourceRange: [646, 651, true], | ||||
|             sourceRange: topLevelRange(646, 651), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -680,11 +680,11 @@ describe('testing getConstraintInfo', () => { | ||||
|         ], | ||||
|       ], | ||||
|     ])('testing %s when inputs are unconstrained', (functionName, expected) => { | ||||
|       const sourceRange: SourceRange = [ | ||||
|       const ast = assertParse(code) | ||||
|       const sourceRange = topLevelRange( | ||||
|         code.indexOf(functionName), | ||||
|         code.indexOf(functionName) + functionName.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(functionName) + functionName.length | ||||
|       ) | ||||
|       if (err(ast)) return ast | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, sourceRange) | ||||
|       const callExp = getNodeFromPath<Node<CallExpression>>( | ||||
| @ -717,7 +717,6 @@ describe('testing getConstraintInfo', () => { | ||||
|          offset = 0 | ||||
|        }, %) | ||||
|     |> tangentialArcTo([3.14, 13.14], %)` | ||||
|     const ast = assertParse(code) | ||||
|     test.each([ | ||||
|       [ | ||||
|         `angledLine(`, | ||||
| @ -726,7 +725,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [112, 116, true], | ||||
|             sourceRange: topLevelRange(112, 116), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -735,7 +734,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'length', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [118, 122, true], | ||||
|             sourceRange: topLevelRange(118, 122), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -749,7 +748,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [277, 281, true], | ||||
|             sourceRange: topLevelRange(277, 281), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -758,7 +757,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3.14', | ||||
|             sourceRange: [283, 287, true], | ||||
|             sourceRange: topLevelRange(283, 287), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -772,7 +771,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '30', | ||||
|             sourceRange: [321, 323, true], | ||||
|             sourceRange: topLevelRange(321, 323), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -781,7 +780,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: false, | ||||
|             value: '3', | ||||
|             sourceRange: [325, 326, true], | ||||
|             sourceRange: topLevelRange(325, 326), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -795,7 +794,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '12', | ||||
|             sourceRange: [354, 356, true], | ||||
|             sourceRange: topLevelRange(354, 356), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -804,7 +803,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '12', | ||||
|             sourceRange: [358, 360, true], | ||||
|             sourceRange: topLevelRange(358, 360), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -818,7 +817,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: false, | ||||
|             value: '30', | ||||
|             sourceRange: [388, 390, true], | ||||
|             sourceRange: topLevelRange(388, 390), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -827,7 +826,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: false, | ||||
|             value: '10', | ||||
|             sourceRange: [392, 394, true], | ||||
|             sourceRange: topLevelRange(392, 394), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -835,11 +834,11 @@ describe('testing getConstraintInfo', () => { | ||||
|         ], | ||||
|       ], | ||||
|     ])('testing %s when inputs are unconstrained', (functionName, expected) => { | ||||
|       const sourceRange: SourceRange = [ | ||||
|       const ast = assertParse(code) | ||||
|       const sourceRange = topLevelRange( | ||||
|         code.indexOf(functionName), | ||||
|         code.indexOf(functionName) + functionName.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(functionName) + functionName.length | ||||
|       ) | ||||
|       if (err(ast)) return ast | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, sourceRange) | ||||
|       const callExp = getNodeFromPath<Node<CallExpression>>( | ||||
| @ -872,7 +871,6 @@ describe('testing getConstraintInfo', () => { | ||||
|          offset = 0 + 0 | ||||
|        }, %) | ||||
|     |> tangentialArcTo([3.14 + 0, 13.14 + 0], %)` | ||||
|     const ast = assertParse(code) | ||||
|     test.each([ | ||||
|       [ | ||||
|         'line', | ||||
| @ -881,7 +879,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: true, | ||||
|             value: '3 + 0', | ||||
|             sourceRange: [83, 88, true], | ||||
|             sourceRange: topLevelRange(83, 88), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'line', | ||||
| @ -890,7 +888,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: true, | ||||
|             value: '4 + 0', | ||||
|             sourceRange: [90, 95, true], | ||||
|             sourceRange: topLevelRange(90, 95), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'line', | ||||
| @ -904,7 +902,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [129, 137, true], | ||||
|             sourceRange: topLevelRange(129, 137), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -913,7 +911,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'length', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [148, 156, true], | ||||
|             sourceRange: topLevelRange(148, 156), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLine', | ||||
| @ -927,7 +925,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '6.14 + 0', | ||||
|             sourceRange: [178, 186, true], | ||||
|             sourceRange: topLevelRange(178, 186), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'lineTo', | ||||
| @ -936,7 +934,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [188, 196, true], | ||||
|             sourceRange: topLevelRange(188, 196), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'lineTo', | ||||
| @ -950,7 +948,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'horizontal', | ||||
|             isConstrained: true, | ||||
|             value: 'xLineTo', | ||||
|             sourceRange: [209, 216, true], | ||||
|             sourceRange: topLevelRange(209, 216), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLineTo', | ||||
| @ -959,7 +957,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '8 + 0', | ||||
|             sourceRange: [217, 222, true], | ||||
|             sourceRange: topLevelRange(217, 222), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLineTo', | ||||
| @ -973,7 +971,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'vertical', | ||||
|             isConstrained: true, | ||||
|             value: 'yLineTo', | ||||
|             sourceRange: [234, 241, true], | ||||
|             sourceRange: topLevelRange(234, 241), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLineTo', | ||||
| @ -982,7 +980,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '5 + 0', | ||||
|             sourceRange: [242, 247, true], | ||||
|             sourceRange: topLevelRange(242, 247), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLineTo', | ||||
| @ -996,7 +994,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'vertical', | ||||
|             isConstrained: true, | ||||
|             value: 'yLine', | ||||
|             sourceRange: [259, 264, true], | ||||
|             sourceRange: topLevelRange(259, 264), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLine', | ||||
| @ -1005,7 +1003,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [265, 273, true], | ||||
|             sourceRange: topLevelRange(265, 273), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'yLine', | ||||
| @ -1019,7 +1017,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'horizontal', | ||||
|             isConstrained: true, | ||||
|             value: 'xLine', | ||||
|             sourceRange: [289, 294, true], | ||||
|             sourceRange: topLevelRange(289, 294), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLine', | ||||
| @ -1028,7 +1026,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [295, 303, true], | ||||
|             sourceRange: topLevelRange(295, 303), | ||||
|             argPosition: { type: 'singleValue' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'xLine', | ||||
| @ -1042,7 +1040,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [345, 353, true], | ||||
|             sourceRange: topLevelRange(345, 353), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -1051,7 +1049,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xRelative', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [364, 372, true], | ||||
|             sourceRange: topLevelRange(364, 372), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfXLength', | ||||
| @ -1065,7 +1063,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '30 + 0', | ||||
|             sourceRange: [416, 422, true], | ||||
|             sourceRange: topLevelRange(416, 422), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -1074,7 +1072,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yRelative', | ||||
|             isConstrained: true, | ||||
|             value: '3 + 0', | ||||
|             sourceRange: [433, 438, true], | ||||
|             sourceRange: topLevelRange(433, 438), | ||||
|             argPosition: { type: 'objectProperty', key: 'length' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineOfYLength', | ||||
| @ -1088,7 +1086,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '12.14 + 0', | ||||
|             sourceRange: [476, 485, true], | ||||
|             sourceRange: topLevelRange(476, 485), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -1097,7 +1095,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '12 + 0', | ||||
|             sourceRange: [492, 498, true], | ||||
|             sourceRange: topLevelRange(492, 498), | ||||
|             argPosition: { type: 'objectProperty', key: 'to' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToX', | ||||
| @ -1111,7 +1109,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '30 + 0', | ||||
|             sourceRange: [536, 542, true], | ||||
|             sourceRange: topLevelRange(536, 542), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -1120,7 +1118,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '10.14 + 0', | ||||
|             sourceRange: [549, 558, true], | ||||
|             sourceRange: topLevelRange(549, 558), | ||||
|             argPosition: { type: 'objectProperty', key: 'to' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineToY', | ||||
| @ -1134,7 +1132,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'angle', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [616, 624, true], | ||||
|             sourceRange: topLevelRange(616, 624), | ||||
|             argPosition: { type: 'objectProperty', key: 'angle' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineThatIntersects', | ||||
| @ -1143,7 +1141,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'intersectionOffset', | ||||
|             isConstrained: true, | ||||
|             value: '0 + 0', | ||||
|             sourceRange: [671, 676, true], | ||||
|             sourceRange: topLevelRange(671, 676), | ||||
|             argPosition: { type: 'objectProperty', key: 'offset' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineThatIntersects', | ||||
| @ -1152,7 +1150,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'intersectionTag', | ||||
|             isConstrained: false, | ||||
|             value: 'a', | ||||
|             sourceRange: [650, 651, true], | ||||
|             sourceRange: topLevelRange(650, 651), | ||||
|             argPosition: { key: 'intersectTag', type: 'objectProperty' }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'angledLineThatIntersects', | ||||
| @ -1166,7 +1164,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'tangentialWithPrevious', | ||||
|             isConstrained: true, | ||||
|             value: 'tangentialArcTo', | ||||
|             sourceRange: [697, 712, true], | ||||
|             sourceRange: topLevelRange(697, 712), | ||||
|             argPosition: undefined, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -1175,7 +1173,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'xAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '3.14 + 0', | ||||
|             sourceRange: [714, 722, true], | ||||
|             sourceRange: topLevelRange(714, 722), | ||||
|             argPosition: { type: 'arrayItem', index: 0 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -1184,7 +1182,7 @@ describe('testing getConstraintInfo', () => { | ||||
|             type: 'yAbsolute', | ||||
|             isConstrained: true, | ||||
|             value: '13.14 + 0', | ||||
|             sourceRange: [724, 733, true], | ||||
|             sourceRange: topLevelRange(724, 733), | ||||
|             argPosition: { type: 'arrayItem', index: 1 }, | ||||
|             pathToNode: expect.any(Array), | ||||
|             stdLibFnName: 'tangentialArcTo', | ||||
| @ -1192,11 +1190,11 @@ describe('testing getConstraintInfo', () => { | ||||
|         ], | ||||
|       ], | ||||
|     ])('testing %s when inputs are unconstrained', (functionName, expected) => { | ||||
|       const sourceRange: SourceRange = [ | ||||
|       const ast = assertParse(code) | ||||
|       const sourceRange = topLevelRange( | ||||
|         code.indexOf(functionName), | ||||
|         code.indexOf(functionName) + functionName.length, | ||||
|         true, | ||||
|       ] | ||||
|         code.indexOf(functionName) + functionName.length | ||||
|       ) | ||||
|       if (err(ast)) return ast | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, sourceRange) | ||||
|       const callExp = getNodeFromPath<Node<CallExpression>>( | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { | ||||
|   VariableDeclaration, | ||||
|   Identifier, | ||||
|   sketchFromKclValue, | ||||
|   topLevelRange, | ||||
| } from 'lang/wasm' | ||||
| import { | ||||
|   getNodeFromPath, | ||||
| @ -222,7 +223,7 @@ const commonConstraintInfoHelper = ( | ||||
|         code.slice(input1.start, input1.end), | ||||
|         stdLibFnName, | ||||
|         isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput, | ||||
|         [input1.start, input1.end, true], | ||||
|         topLevelRange(input1.start, input1.end), | ||||
|         pathToFirstArg | ||||
|       ) | ||||
|     ) | ||||
| @ -234,7 +235,7 @@ const commonConstraintInfoHelper = ( | ||||
|         code.slice(input2.start, input2.end), | ||||
|         stdLibFnName, | ||||
|         isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput, | ||||
|         [input2.start, input2.end, true], | ||||
|         topLevelRange(input2.start, input2.end), | ||||
|         pathToSecondArg | ||||
|       ) | ||||
|     ) | ||||
| @ -266,7 +267,7 @@ const horzVertConstraintInfoHelper = ( | ||||
|       callee.name, | ||||
|       stdLibFnName, | ||||
|       undefined, | ||||
|       [callee.start, callee.end, true], | ||||
|       topLevelRange(callee.start, callee.end), | ||||
|       pathToCallee | ||||
|     ), | ||||
|     constrainInfo( | ||||
| @ -275,7 +276,7 @@ const horzVertConstraintInfoHelper = ( | ||||
|       code.slice(firstArg.start, firstArg.end), | ||||
|       stdLibFnName, | ||||
|       abbreviatedInput, | ||||
|       [firstArg.start, firstArg.end, true], | ||||
|       topLevelRange(firstArg.start, firstArg.end), | ||||
|       pathToFirstArg | ||||
|     ), | ||||
|   ] | ||||
| @ -905,7 +906,7 @@ export const tangentialArcTo: SketchLineHelper = { | ||||
|         callee.name, | ||||
|         'tangentialArcTo', | ||||
|         undefined, | ||||
|         [callee.start, callee.end, true], | ||||
|         topLevelRange(callee.start, callee.end), | ||||
|         pathToCallee | ||||
|       ), | ||||
|       constrainInfo( | ||||
| @ -914,7 +915,7 @@ export const tangentialArcTo: SketchLineHelper = { | ||||
|         code.slice(firstArg.elements[0].start, firstArg.elements[0].end), | ||||
|         'tangentialArcTo', | ||||
|         0, | ||||
|         [firstArg.elements[0].start, firstArg.elements[0].end, true], | ||||
|         topLevelRange(firstArg.elements[0].start, firstArg.elements[0].end), | ||||
|         pathToFirstArg | ||||
|       ), | ||||
|       constrainInfo( | ||||
| @ -923,7 +924,7 @@ export const tangentialArcTo: SketchLineHelper = { | ||||
|         code.slice(firstArg.elements[1].start, firstArg.elements[1].end), | ||||
|         'tangentialArcTo', | ||||
|         1, | ||||
|         [firstArg.elements[1].start, firstArg.elements[1].end, true], | ||||
|         topLevelRange(firstArg.elements[1].start, firstArg.elements[1].end), | ||||
|         pathToSecondArg | ||||
|       ), | ||||
|     ] | ||||
| @ -1052,7 +1053,7 @@ export const circle: SketchLineHelper = { | ||||
|         code.slice(radiusDetails.expr.start, radiusDetails.expr.end), | ||||
|         'circle', | ||||
|         'radius', | ||||
|         [radiusDetails.expr.start, radiusDetails.expr.end, true], | ||||
|         topLevelRange(radiusDetails.expr.start, radiusDetails.expr.end), | ||||
|         pathToRadiusLiteral | ||||
|       ), | ||||
|       { | ||||
| @ -1061,11 +1062,10 @@ export const circle: SketchLineHelper = { | ||||
|         isConstrained: isNotLiteralArrayOrStatic( | ||||
|           centerDetails.expr.elements[0] | ||||
|         ), | ||||
|         sourceRange: [ | ||||
|         sourceRange: topLevelRange( | ||||
|           centerDetails.expr.elements[0].start, | ||||
|           centerDetails.expr.elements[0].end, | ||||
|           true, | ||||
|         ], | ||||
|           centerDetails.expr.elements[0].end | ||||
|         ), | ||||
|         pathToNode: pathToXArg, | ||||
|         value: code.slice( | ||||
|           centerDetails.expr.elements[0].start, | ||||
| @ -1083,11 +1083,10 @@ export const circle: SketchLineHelper = { | ||||
|         isConstrained: isNotLiteralArrayOrStatic( | ||||
|           centerDetails.expr.elements[1] | ||||
|         ), | ||||
|         sourceRange: [ | ||||
|         sourceRange: topLevelRange( | ||||
|           centerDetails.expr.elements[1].start, | ||||
|           centerDetails.expr.elements[1].end, | ||||
|           true, | ||||
|         ], | ||||
|           centerDetails.expr.elements[1].end | ||||
|         ), | ||||
|         pathToNode: pathToYArg, | ||||
|         value: code.slice( | ||||
|           centerDetails.expr.elements[1].start, | ||||
| @ -1763,7 +1762,7 @@ export const angledLineThatIntersects: SketchLineHelper = { | ||||
|           code.slice(angle.start, angle.end), | ||||
|           'angledLineThatIntersects', | ||||
|           'angle', | ||||
|           [angle.start, angle.end, true], | ||||
|           topLevelRange(angle.start, angle.end), | ||||
|           pathToAngleProp | ||||
|         ) | ||||
|       ) | ||||
| @ -1782,7 +1781,7 @@ export const angledLineThatIntersects: SketchLineHelper = { | ||||
|           code.slice(offset.start, offset.end), | ||||
|           'angledLineThatIntersects', | ||||
|           'offset', | ||||
|           [offset.start, offset.end, true], | ||||
|           topLevelRange(offset.start, offset.end), | ||||
|           pathToOffsetProp | ||||
|         ) | ||||
|       ) | ||||
| @ -1801,7 +1800,7 @@ export const angledLineThatIntersects: SketchLineHelper = { | ||||
|         code.slice(tag.start, tag.end), | ||||
|         'angledLineThatIntersects', | ||||
|         'intersectTag', | ||||
|         [tag.start, tag.end, true], | ||||
|         topLevelRange(tag.start, tag.end), | ||||
|         pathToTagProp | ||||
|       ) | ||||
|       returnVal.push(info) | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { | ||||
|   initPromise, | ||||
|   sketchFromKclValue, | ||||
|   SourceRange, | ||||
|   topLevelRange, | ||||
| } from '../wasm' | ||||
| import { | ||||
|   ConstraintType, | ||||
| @ -31,10 +32,10 @@ async function testingSwapSketchFnCall({ | ||||
|   constraintType: ConstraintType | ||||
| }): Promise<{ | ||||
|   newCode: string | ||||
|   originalRange: [number, number, boolean] | ||||
|   originalRange: SourceRange | ||||
| }> { | ||||
|   const startIndex = inputCode.indexOf(callToSwap) | ||||
|   const range: SourceRange = [startIndex, startIndex + callToSwap.length, true] | ||||
|   const range = topLevelRange(startIndex, startIndex + callToSwap.length) | ||||
|   const ast = assertParse(inputCode) | ||||
|  | ||||
|   const execState = await enginelessExecutor(ast) | ||||
| @ -375,7 +376,10 @@ part001 = startSketchOn('XY') | ||||
|       execState.memory.get('part001'), | ||||
|       'part001' | ||||
|     ) as Sketch | ||||
|     const _segment = getSketchSegmentFromSourceRange(sg, [index, index, true]) | ||||
|     const _segment = getSketchSegmentFromSourceRange( | ||||
|       sg, | ||||
|       topLevelRange(index, index) | ||||
|     ) | ||||
|     if (err(_segment)) throw _segment | ||||
|     const { __geoMeta, ...segment } = _segment.segment | ||||
|     expect(segment).toEqual({ | ||||
| @ -390,7 +394,7 @@ part001 = startSketchOn('XY') | ||||
|     const index = code.indexOf('// segment-in-start') - 7 | ||||
|     const _segment = getSketchSegmentFromSourceRange( | ||||
|       sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch, | ||||
|       [index, index, true] | ||||
|       topLevelRange(index, index) | ||||
|     ) | ||||
|     if (err(_segment)) throw _segment | ||||
|     const { __geoMeta, ...segment } = _segment.segment | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { | ||||
|   Path, | ||||
|   PathToNode, | ||||
|   Expr, | ||||
|   topLevelRange, | ||||
| } from '../wasm' | ||||
| import { err } from 'lib/trap' | ||||
|  | ||||
| @ -31,7 +32,7 @@ export function getSketchSegmentFromPathToNode( | ||||
|   const node = nodeMeta.node | ||||
|   if (!node || typeof node.start !== 'number' || !node.end) | ||||
|     return new Error('no node found') | ||||
|   const sourceRange: SourceRange = [node.start, node.end, true] | ||||
|   const sourceRange = topLevelRange(node.start, node.end) | ||||
|   return getSketchSegmentFromSourceRange(sketch, sourceRange) | ||||
| } | ||||
| export function getSketchSegmentFromSourceRange( | ||||
|  | ||||
| @ -1,4 +1,11 @@ | ||||
| import { assertParse, Expr, recast, initPromise, Program } from '../wasm' | ||||
| import { | ||||
|   assertParse, | ||||
|   Expr, | ||||
|   recast, | ||||
|   initPromise, | ||||
|   Program, | ||||
|   topLevelRange, | ||||
| } from '../wasm' | ||||
| import { | ||||
|   getConstraintType, | ||||
|   getTransformInfos, | ||||
| @ -125,7 +132,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () => | ||||
|         ) | ||||
|       } | ||||
|       const start = codeBeforeLine + line.indexOf('|> ' + 5) | ||||
|       const range: [number, number, boolean] = [start, start, true] | ||||
|       const range = topLevelRange(start, start) | ||||
|       return { | ||||
|         codeRef: codeRefFromRange(range, ast), | ||||
|       } | ||||
| @ -297,7 +304,7 @@ part001 = startSketchOn('XY') | ||||
|         const comment = ln.split('//')[1] | ||||
|         const start = inputScript.indexOf('//' + comment) - 7 | ||||
|         return { | ||||
|           codeRef: codeRefFromRange([start, start, true], ast), | ||||
|           codeRef: codeRefFromRange(topLevelRange(start, start), ast), | ||||
|         } | ||||
|       }) | ||||
|  | ||||
| @ -386,7 +393,7 @@ part001 = startSketchOn('XY') | ||||
|         const comment = ln.split('//')[1] | ||||
|         const start = inputScript.indexOf('//' + comment) - 7 | ||||
|         return { | ||||
|           codeRef: codeRefFromRange([start, start, true], ast), | ||||
|           codeRef: codeRefFromRange(topLevelRange(start, start), ast), | ||||
|         } | ||||
|       }) | ||||
|  | ||||
| @ -446,7 +453,7 @@ part001 = startSketchOn('XY') | ||||
|         const comment = ln.split('//')[1] | ||||
|         const start = inputScript.indexOf('//' + comment) - 7 | ||||
|         return { | ||||
|           codeRef: codeRefFromRange([start, start, true], ast), | ||||
|           codeRef: codeRefFromRange(topLevelRange(start, start), ast), | ||||
|         } | ||||
|       }) | ||||
|  | ||||
| @ -541,7 +548,7 @@ async function helperThing( | ||||
|       const comment = ln.split('//')[1] | ||||
|       const start = inputScript.indexOf('//' + comment) - 7 | ||||
|       return { | ||||
|         codeRef: codeRefFromRange([start, start, true], ast), | ||||
|         codeRef: codeRefFromRange(topLevelRange(start, start), ast), | ||||
|       } | ||||
|     }) | ||||
|  | ||||
| @ -610,7 +617,7 @@ part001 = startSketchOn('XY') | ||||
|         } | ||||
|         const offsetIndex = index - 7 | ||||
|         const expectedConstraintLevel = getConstraintLevelFromSourceRange( | ||||
|           [offsetIndex, offsetIndex, true], | ||||
|           topLevelRange(offsetIndex, offsetIndex), | ||||
|           ast | ||||
|         ) | ||||
|         if (err(expectedConstraintLevel)) { | ||||
|  | ||||
| @ -20,12 +20,12 @@ import { | ||||
|   sketchFromKclValue, | ||||
|   Literal, | ||||
|   SourceRange, | ||||
|   LiteralValue, | ||||
| } from '../wasm' | ||||
| import { | ||||
|   getNodeFromPath, | ||||
|   getNodeFromPathCurry, | ||||
|   getNodePathFromSourceRange, | ||||
|   isValueZero, | ||||
| } from '../queryAst' | ||||
| import { | ||||
|   createArrayExpression, | ||||
| @ -79,11 +79,32 @@ export type ConstraintType = | ||||
|   | 'setAngleBetween' | ||||
|  | ||||
| const REF_NUM_ERR = new Error('Referenced segment does not have a to value') | ||||
|  | ||||
| function asNum(val: LiteralValue): number | Error { | ||||
|   if (typeof val === 'object') return val.value | ||||
|   return REF_NUM_ERR | ||||
| } | ||||
|  | ||||
| function forceNum(arg: Literal): number { | ||||
|   if (typeof arg.value === 'boolean' || typeof arg.value === 'string') { | ||||
|     return Number(arg.value) | ||||
|   } else { | ||||
|     return arg.value.value | ||||
|   } | ||||
| } | ||||
|  | ||||
| function isUndef(val: any): val is undefined { | ||||
|   return typeof val === 'undefined' | ||||
| } | ||||
| function isNum(val: any): val is number { | ||||
|   return typeof val === 'number' | ||||
|  | ||||
| function isValueZero(val?: Expr): boolean { | ||||
|   return ( | ||||
|     (val?.type === 'Literal' && forceNum(val) === 0) || | ||||
|     (val?.type === 'UnaryExpression' && | ||||
|       val.operator === '-' && | ||||
|       val.argument.type === 'Literal' && | ||||
|       Number(val.argument.value) === 0) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| function createCallWrapper( | ||||
| @ -190,7 +211,7 @@ const xyLineSetLength = | ||||
|       : referenceSeg | ||||
|       ? segRef | ||||
|       : args[0].expr | ||||
|     const literalARg = getArgLiteralVal(args[0].expr) | ||||
|     const literalARg = asNum(args[0].expr.value) | ||||
|     if (err(literalARg)) return literalARg | ||||
|     return createCallWrapper(xOrY, lineVal, tag, literalARg) | ||||
|   } | ||||
| @ -211,13 +232,14 @@ const basicAngledLineCreateNode = | ||||
|     referencedSegment: path, | ||||
|   }) => { | ||||
|     const refAng = path ? getAngle(path?.from, path?.to) : 0 | ||||
|     if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|     const argValue = asNum(args[0].expr.value) | ||||
|     if (err(argValue)) return argValue | ||||
|     const nonForcedAng = | ||||
|       varValToUse === 'ang' | ||||
|         ? inputs[0].expr | ||||
|         : referenceSeg === 'ang' | ||||
|         ? getClosesAngleDirection( | ||||
|             args[0].expr.value, | ||||
|             argValue, | ||||
|             refAng, | ||||
|             createSegAngle(referenceSegName) | ||||
|           ) | ||||
| @ -230,8 +252,8 @@ const basicAngledLineCreateNode = | ||||
|         : args[1].expr | ||||
|     const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform | ||||
|     const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform | ||||
|     const literalArg = getArgLiteralVal( | ||||
|       valToForce === 'ang' ? args[0].expr : args[1].expr | ||||
|     const literalArg = asNum( | ||||
|       valToForce === 'ang' ? args[0].expr.value : args[1].expr.value | ||||
|     ) | ||||
|     if (err(literalArg)) return literalArg | ||||
|     return createCallWrapper( | ||||
| @ -283,7 +305,7 @@ const getMinAndSegAngVals = ( | ||||
| } | ||||
|  | ||||
| const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) => | ||||
|   Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal | ||||
|   forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal | ||||
|  | ||||
| const getLegAng = (ang: number, legAngleVal: BinaryPart) => { | ||||
|   const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360 | ||||
| @ -322,8 +344,7 @@ const setHorzVertDistanceCreateNode = | ||||
|     referencedSegment, | ||||
|   }) => { | ||||
|     const refNum = referencedSegment?.to?.[index] | ||||
|     const literalArg = getArgLiteralVal(args?.[index].expr) | ||||
|     if (err(literalArg)) return literalArg | ||||
|     const literalArg = asNum(args?.[index].expr.value) | ||||
|     if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR | ||||
|  | ||||
|     const valueUsedInTransform = roundOff(literalArg - refNum, 2) | ||||
| @ -352,7 +373,7 @@ const setHorzVertDistanceForAngleLineCreateNode = | ||||
|     referencedSegment, | ||||
|   }) => { | ||||
|     const refNum = referencedSegment?.to?.[index] | ||||
|     const literalArg = getArgLiteralVal(args?.[1].expr) | ||||
|     const literalArg = asNum(args?.[1].expr.value) | ||||
|     if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR | ||||
|     const valueUsedInTransform = roundOff(literalArg - refNum, 2) | ||||
|     const binExp = createBinaryExpressionWithUnary([ | ||||
| @ -374,8 +395,8 @@ const setAbsDistanceCreateNode = | ||||
|     index = xOrY === 'x' ? 0 : 1 | ||||
|   ): CreateStdLibSketchCallExpr => | ||||
|   ({ tag, forceValueUsedInTransform, rawArgs: args }) => { | ||||
|     const literalArg = getArgLiteralVal(args?.[index].expr) | ||||
|     if (err(literalArg)) return REF_NUM_ERR | ||||
|     const literalArg = asNum(args?.[index].expr.value) | ||||
|     if (err(literalArg)) return literalArg | ||||
|     const valueUsedInTransform = roundOff(literalArg, 2) | ||||
|     const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) | ||||
|     if (isXOrYLine) { | ||||
| @ -396,8 +417,8 @@ const setAbsDistanceCreateNode = | ||||
| const setAbsDistanceForAngleLineCreateNode = | ||||
|   (xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr => | ||||
|   ({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => { | ||||
|     const literalArg = getArgLiteralVal(args?.[1].expr) | ||||
|     if (err(literalArg)) return REF_NUM_ERR | ||||
|     const literalArg = asNum(args?.[1].expr.value) | ||||
|     if (err(literalArg)) return literalArg | ||||
|     const valueUsedInTransform = roundOff(literalArg, 2) | ||||
|     const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform) | ||||
|     return createCallWrapper( | ||||
| @ -419,7 +440,7 @@ const setHorVertDistanceForXYLines = | ||||
|   }) => { | ||||
|     const index = xOrY === 'x' ? 0 : 1 | ||||
|     const refNum = referencedSegment?.to?.[index] | ||||
|     const literalArg = getArgLiteralVal(args?.[index].expr) | ||||
|     const literalArg = asNum(args?.[index].expr.value) | ||||
|     if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR | ||||
|     const valueUsedInTransform = roundOff(literalArg - refNum, 2) | ||||
|     const makeBinExp = createBinaryExpressionWithUnary([ | ||||
| @ -445,9 +466,9 @@ const setHorzVertDistanceConstraintLineCreateNode = | ||||
|     ]) | ||||
|  | ||||
|     const makeBinExp = (index: 0 | 1) => { | ||||
|       const arg = getArgLiteralVal(args?.[index].expr) | ||||
|       const arg = asNum(args?.[index].expr.value) | ||||
|       const refNum = referencedSegment?.to?.[index] | ||||
|       if (err(arg) || !isNum(refNum)) return REF_NUM_ERR | ||||
|       if (err(arg) || isUndef(refNum)) return REF_NUM_ERR | ||||
|       return createBinaryExpressionWithUnary([ | ||||
|         createSegEnd(referenceSegName, isX), | ||||
|         createLiteral(roundOff(arg - refNum, 2)), | ||||
| @ -468,9 +489,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({ | ||||
|   forceValueUsedInTransform, | ||||
|   rawArgs: args, | ||||
| }) => { | ||||
|   const val = args[1].expr.value, | ||||
|     angle = args[0].expr.value | ||||
|   if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR | ||||
|   const val = asNum(args[1].expr.value), | ||||
|     angle = asNum(args[0].expr.value) | ||||
|   if (err(val) || err(angle)) return REF_NUM_ERR | ||||
|   const valueUsedInTransform = roundOff(val, 2) | ||||
|   const varNamMap: { [key: number]: string } = { | ||||
|     0: 'ZERO', | ||||
| @ -498,8 +519,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({ | ||||
|   inputs, | ||||
|   rawArgs: args, | ||||
| }) => { | ||||
|   const val = args[1].expr.value | ||||
|   if (!isNum(val)) return REF_NUM_ERR | ||||
|   const val = asNum(args[1].expr.value) | ||||
|   if (err(val)) return val | ||||
|   const valueUsedInTransform = roundOff(val, 2) | ||||
|   return intersectCallWrapper({ | ||||
|     fnName: 'angledLineThatIntersects', | ||||
| @ -524,8 +545,8 @@ const setAngleBetweenCreateNode = | ||||
|     const refAngle = referencedSegment | ||||
|       ? getAngle(referencedSegment?.from, referencedSegment?.to) | ||||
|       : 0 | ||||
|     const val = args[0].expr.value | ||||
|     if (!isNum(val)) return REF_NUM_ERR | ||||
|     const val = asNum(args[0].expr.value) | ||||
|     if (err(val)) return val | ||||
|     let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle)) | ||||
|     let firstHalfValue = createSegAngle(referenceSegName) | ||||
|     if (Math.abs(valueUsedInTransform) > 90) { | ||||
| @ -706,13 +727,11 @@ const transformMap: TransformMap = { | ||||
|               createPipeSubstitution(), | ||||
|             ] | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineToX', | ||||
|             [ | ||||
|               getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall), | ||||
|               inputs[0].expr, | ||||
|             ], | ||||
|             [getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -739,13 +758,11 @@ const transformMap: TransformMap = { | ||||
|               createPipeSubstitution(), | ||||
|             ] | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineToY', | ||||
|             [ | ||||
|               getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall), | ||||
|               inputs[1].expr, | ||||
|             ], | ||||
|             [getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -763,7 +780,7 @@ const transformMap: TransformMap = { | ||||
|           forceValueUsedInTransform, | ||||
|           rawArgs: args, | ||||
|         }) => { | ||||
|           const val = getArgLiteralVal(args[0].expr) | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineToY', | ||||
| @ -844,7 +861,7 @@ const transformMap: TransformMap = { | ||||
|         tooltip: 'yLine', | ||||
|         createNode: ({ inputs, tag, rawArgs: args }) => { | ||||
|           const expr = inputs[1].expr | ||||
|           if (Number(args[0].expr.value) >= 0) | ||||
|           if (forceNum(args[0].expr) >= 0) | ||||
|             return createCallWrapper('yLine', expr, tag) | ||||
|           if (isExprBinaryPart(expr)) | ||||
|             return createCallWrapper('yLine', createUnaryExpression(expr), tag) | ||||
| @ -856,7 +873,7 @@ const transformMap: TransformMap = { | ||||
|         tooltip: 'xLine', | ||||
|         createNode: ({ inputs, tag, rawArgs: args }) => { | ||||
|           const expr = inputs[1].expr | ||||
|           if (Number(args[0].expr.value) >= 0) | ||||
|           if (forceNum(args[0].expr) >= 0) | ||||
|             return createCallWrapper('xLine', expr, tag) | ||||
|           if (isExprBinaryPart(expr)) | ||||
|             return createCallWrapper('xLine', createUnaryExpression(expr), tag) | ||||
| @ -900,10 +917,11 @@ const transformMap: TransformMap = { | ||||
|             referenceSegName, | ||||
|             getInputOfType(inputs, 'xRelative').expr | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineOfXLength', | ||||
|             [getLegAng(args[0].expr.value, legAngle), minVal], | ||||
|             [getLegAng(val, legAngle), minVal], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -912,7 +930,7 @@ const transformMap: TransformMap = { | ||||
|         tooltip: 'xLine', | ||||
|         createNode: ({ inputs, tag, rawArgs: args }) => { | ||||
|           const expr = inputs[1].expr | ||||
|           if (Number(args[0].expr.value) >= 0) | ||||
|           if (forceNum(args[0].expr) >= 0) | ||||
|             return createCallWrapper('xLine', expr, tag) | ||||
|           if (isExprBinaryPart(expr)) | ||||
|             return createCallWrapper('xLine', createUnaryExpression(expr), tag) | ||||
| @ -953,10 +971,11 @@ const transformMap: TransformMap = { | ||||
|             inputs[1].expr, | ||||
|             'legAngY' | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineOfXLength', | ||||
|             [getLegAng(args[0].expr.value, legAngle), minVal], | ||||
|             [getLegAng(val, legAngle), minVal], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -965,7 +984,7 @@ const transformMap: TransformMap = { | ||||
|         tooltip: 'yLine', | ||||
|         createNode: ({ inputs, tag, rawArgs: args }) => { | ||||
|           const expr = inputs[1].expr | ||||
|           if (Number(args[0].expr.value) >= 0) | ||||
|           if (forceNum(args[0].expr) >= 0) | ||||
|             return createCallWrapper('yLine', expr, tag) | ||||
|           if (isExprBinaryPart(expr)) | ||||
|             return createCallWrapper('yLine', createUnaryExpression(expr), tag) | ||||
| @ -1005,13 +1024,11 @@ const transformMap: TransformMap = { | ||||
|               createPipeSubstitution(), | ||||
|             ] | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineToX', | ||||
|             [ | ||||
|               getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall), | ||||
|               inputs[1].expr, | ||||
|             ], | ||||
|             [getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -1057,13 +1074,11 @@ const transformMap: TransformMap = { | ||||
|               createPipeSubstitution(), | ||||
|             ] | ||||
|           ) | ||||
|           if (!isNum(args[0].expr.value)) return REF_NUM_ERR | ||||
|           const val = asNum(args[0].expr.value) | ||||
|           if (err(val)) return val | ||||
|           return createCallWrapper( | ||||
|             'angledLineToY', | ||||
|             [ | ||||
|               getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall), | ||||
|               inputs[1].expr, | ||||
|             ], | ||||
|             [getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr], | ||||
|             tag | ||||
|           ) | ||||
|         }, | ||||
| @ -1080,7 +1095,7 @@ const transformMap: TransformMap = { | ||||
|       equalLength: { | ||||
|         tooltip: 'xLine', | ||||
|         createNode: ({ referenceSegName, tag, rawArgs: args }) => { | ||||
|           const argVal = getArgLiteralVal(args[0].expr) | ||||
|           const argVal = asNum(args[0].expr.value) | ||||
|           if (err(argVal)) return argVal | ||||
|           const segLen = createSegLen(referenceSegName) | ||||
|           if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal) | ||||
| @ -1118,7 +1133,7 @@ const transformMap: TransformMap = { | ||||
|       equalLength: { | ||||
|         tooltip: 'yLine', | ||||
|         createNode: ({ referenceSegName, tag, rawArgs: args }) => { | ||||
|           const argVal = getArgLiteralVal(args[0].expr) | ||||
|           const argVal = asNum(args[0].expr.value) | ||||
|           if (err(argVal)) return argVal | ||||
|           let segLen = createSegLen(referenceSegName) | ||||
|           if (argVal < 0) segLen = createUnaryExpression(segLen) | ||||
| @ -1714,7 +1729,7 @@ export function transformAstSketchLines({ | ||||
|     let kclVal = programMemory.get(varName) | ||||
|     let sketch | ||||
|     if (kclVal?.type === 'Solid') { | ||||
|       sketch = kclVal.sketch | ||||
|       sketch = kclVal.value.sketch | ||||
|     } else { | ||||
|       sketch = sketchFromKclValue(kclVal, varName) | ||||
|       if (err(sketch)) { | ||||
| @ -1823,11 +1838,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> { | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| function getArgLiteralVal(arg: Literal): number | Error { | ||||
|   if (!isNum(arg.value)) return REF_NUM_ERR | ||||
|   return arg.value | ||||
| } | ||||
|  | ||||
| export type ConstraintLevel = 'free' | 'partial' | 'full' | ||||
|  | ||||
| export function getConstraintLevelFromSourceRange( | ||||
|  | ||||
| @ -5,8 +5,9 @@ import { | ||||
|   Literal, | ||||
|   ArrayExpression, | ||||
|   BinaryExpression, | ||||
|   ArtifactGraph, | ||||
| } from './wasm' | ||||
| import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' | ||||
| import { filterArtifacts } from 'lang/std/artifactGraph' | ||||
| import { isOverlap } from 'lib/utils' | ||||
|  | ||||
| export function updatePathToNodeFromMap( | ||||
|  | ||||
							
								
								
									
										111
									
								
								src/lang/wasm.ts
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								src/lang/wasm.ts
									
									
									
									
									
								
							| @ -44,17 +44,30 @@ import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef' | ||||
| import { Environment } from '../wasm-lib/kcl/bindings/Environment' | ||||
| import { Node } from 'wasm-lib/kcl/bindings/Node' | ||||
| import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError' | ||||
| import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' | ||||
| import { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange' | ||||
| import { getAllCurrentSettings } from 'lib/settings/settingsUtils' | ||||
| import { Operation } from 'wasm-lib/kcl/bindings/Operation' | ||||
| import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs' | ||||
| import { Artifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId' | ||||
| import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand' | ||||
| import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| import { Artifact } from './std/artifactGraph' | ||||
| import { getNodePathFromSourceRange } from './queryAst' | ||||
|  | ||||
| export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand' | ||||
| export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId' | ||||
| export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Cap as CapArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { CodeRef } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { EdgeCut } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Path as PathArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Plane as PlaneArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Segment as SegmentArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Solid2d as Solid2dArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Sweep as SweepArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { SweepEdge } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Wall as WallArtifact } from 'wasm-lib/kcl/bindings/Artifact' | ||||
| export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration' | ||||
| export type { Program } from '../wasm-lib/kcl/bindings/Program' | ||||
| export type { Expr } from '../wasm-lib/kcl/bindings/Expr' | ||||
| @ -76,7 +89,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart' | ||||
| export type { Literal } from '../wasm-lib/kcl/bindings/Literal' | ||||
| export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue' | ||||
| export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression' | ||||
| export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' | ||||
| export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange' | ||||
|  | ||||
| export type SyntaxType = | ||||
|   | 'Program' | ||||
| @ -105,35 +118,36 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid' | ||||
| export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue' | ||||
| export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' | ||||
|  | ||||
| /** | ||||
|  * The first two items are the start and end points (byte offsets from the start of the file). | ||||
|  * The third item is whether the source range belongs to the 'main' file, i.e., the file currently | ||||
|  * being rendered/displayed in the editor (TODO we need to handle modules better in the frontend). | ||||
|  */ | ||||
| export type SourceRange = [number, number, boolean] | ||||
|  | ||||
| /** | ||||
|  * Convert a SourceRange as used inside the KCL interpreter into the above one for use in the | ||||
|  * frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange | ||||
|  * so as not to expose details of the interpreter's current representation of module ids throughout | ||||
|  * the frontend). | ||||
|  */ | ||||
| export function sourceRangeFromRust(s: RustSourceRange): SourceRange { | ||||
|   return [s[0], s[1], s[2] === 0] | ||||
| export function sourceRangeFromRust(s: SourceRange): SourceRange { | ||||
|   return [s[0], s[1], s[2]] | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a default SourceRange for testing or as a placeholder. | ||||
|  */ | ||||
| export function defaultSourceRange(): SourceRange { | ||||
|   return [0, 0, true] | ||||
|   return [0, 0, 0] | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a default RustSourceRange for testing or as a placeholder. | ||||
|  * Create a SourceRange for the top-level module. | ||||
|  */ | ||||
| export function defaultRustSourceRange(): RustSourceRange { | ||||
|   return [0, 0, 0] | ||||
| export function topLevelRange(start: number, end: number): SourceRange { | ||||
|   return [start, end, 0] | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns true if this source range is from the file being executed.  Returns | ||||
|  * false if it's from a file that was imported. | ||||
|  */ | ||||
| export function isTopLevelModule(range: SourceRange): boolean { | ||||
|   return range[2] === 0 | ||||
| } | ||||
|  | ||||
| export const wasmUrl = () => { | ||||
| @ -234,7 +248,8 @@ export const parse = (code: string | Error): ParseResult | Error => { | ||||
|       parsed.msg, | ||||
|       sourceRangeFromRust(parsed.sourceRanges[0]), | ||||
|       [], | ||||
|       [] | ||||
|       [], | ||||
|       defaultArtifactGraph() | ||||
|     ) | ||||
|   } | ||||
| } | ||||
| @ -258,8 +273,9 @@ export const isPathToNodeNumber = ( | ||||
| export interface ExecState { | ||||
|   memory: ProgramMemory | ||||
|   operations: Operation[] | ||||
|   artifacts: { [key in ArtifactId]?: Artifact } | ||||
|   artifacts: { [key in ArtifactId]?: RustArtifact } | ||||
|   artifactCommands: ArtifactCommand[] | ||||
|   artifactGraph: ArtifactGraph | ||||
| } | ||||
|  | ||||
| /** | ||||
| @ -272,18 +288,53 @@ export function emptyExecState(): ExecState { | ||||
|     operations: [], | ||||
|     artifacts: {}, | ||||
|     artifactCommands: [], | ||||
|     artifactGraph: defaultArtifactGraph(), | ||||
|   } | ||||
| } | ||||
|  | ||||
| function execStateFromRust(execOutcome: RustExecOutcome): ExecState { | ||||
| function execStateFromRust( | ||||
|   execOutcome: RustExecOutcome, | ||||
|   program: Node<Program> | ||||
| ): ExecState { | ||||
|   const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph) | ||||
|   // We haven't ported pathToNode logic to Rust yet, so we need to fill it in. | ||||
|   for (const [id, artifact] of artifactGraph) { | ||||
|     if (!artifact) continue | ||||
|     if (!('codeRef' in artifact)) continue | ||||
|     const pathToNode = getNodePathFromSourceRange( | ||||
|       program, | ||||
|       sourceRangeFromRust(artifact.codeRef.range) | ||||
|     ) | ||||
|     artifact.codeRef.pathToNode = pathToNode | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     memory: ProgramMemory.fromRaw(execOutcome.memory), | ||||
|     operations: execOutcome.operations, | ||||
|     artifacts: execOutcome.artifacts, | ||||
|     artifactCommands: execOutcome.artifactCommands, | ||||
|     artifactGraph, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export type ArtifactGraph = Map<ArtifactId, Artifact> | ||||
|  | ||||
| function rustArtifactGraphToMap( | ||||
|   rustArtifactGraph: RustArtifactGraph | ||||
| ): ArtifactGraph { | ||||
|   const map = new Map<ArtifactId, Artifact>() | ||||
|   for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) { | ||||
|     if (!artifact) continue | ||||
|     map.set(id, artifact) | ||||
|   } | ||||
|  | ||||
|   return map | ||||
| } | ||||
|  | ||||
| export function defaultArtifactGraph(): ArtifactGraph { | ||||
|   return new Map() | ||||
| } | ||||
|  | ||||
| interface Memory { | ||||
|   [key: string]: KclValue | undefined | ||||
| } | ||||
| @ -488,7 +539,8 @@ export function sketchFromKclValueOptional( | ||||
| ): Sketch | Reason { | ||||
|   if (obj?.value?.type === 'Sketch') return obj.value | ||||
|   if (obj?.value?.type === 'Solid') return obj.value.sketch | ||||
|   if (obj?.type === 'Solid') return obj.sketch | ||||
|   if (obj?.type === 'Sketch') return obj.value | ||||
|   if (obj?.type === 'Solid') return obj.value.sketch | ||||
|   if (!varName) { | ||||
|     varName = 'a KCL value' | ||||
|   } | ||||
| @ -543,7 +595,7 @@ export const executor = async ( | ||||
|       engineCommandManager, | ||||
|       fileSystemManager | ||||
|     ) | ||||
|     return execStateFromRust(execOutcome) | ||||
|     return execStateFromRust(execOutcome, node) | ||||
|   } catch (e: any) { | ||||
|     console.log(e) | ||||
|     const parsed: KclErrorWithOutputs = JSON.parse(e.toString()) | ||||
| @ -552,7 +604,8 @@ export const executor = async ( | ||||
|       parsed.error.msg, | ||||
|       sourceRangeFromRust(parsed.error.sourceRanges[0]), | ||||
|       parsed.operations, | ||||
|       parsed.artifactCommands | ||||
|       parsed.artifactCommands, | ||||
|       rustArtifactGraphToMap(parsed.artifactGraph) | ||||
|     ) | ||||
|  | ||||
|     return Promise.reject(kclError) | ||||
| @ -613,7 +666,8 @@ export const modifyAstForSketch = async ( | ||||
|       parsed.msg, | ||||
|       sourceRangeFromRust(parsed.sourceRanges[0]), | ||||
|       [], | ||||
|       [] | ||||
|       [], | ||||
|       defaultArtifactGraph() | ||||
|     ) | ||||
|  | ||||
|     console.log(kclError) | ||||
| @ -683,7 +737,8 @@ export function programMemoryInit(): ProgramMemory | Error { | ||||
|       parsed.msg, | ||||
|       sourceRangeFromRust(parsed.sourceRanges[0]), | ||||
|       [], | ||||
|       [] | ||||
|       [], | ||||
|       defaultArtifactGraph() | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,12 @@ import { Selections } from 'lib/selections' | ||||
| import { kclManager } from 'lib/singletons' | ||||
| import { err } from 'lib/trap' | ||||
| import { modelingMachine, SketchTool } from 'machines/modelingMachine' | ||||
| import { loftValidator, revolveAxisValidator } from './validators' | ||||
| import { | ||||
|   loftValidator, | ||||
|   revolveAxisValidator, | ||||
|   shellValidator, | ||||
|   sweepValidator, | ||||
| } from './validators' | ||||
|  | ||||
| type OutputFormat = Models['OutputFormat_type'] | ||||
| type OutputTypeKey = OutputFormat['type'] | ||||
| @ -38,8 +43,8 @@ export type ModelingCommandSchema = { | ||||
|     distance: KclCommandValue | ||||
|   } | ||||
|   Sweep: { | ||||
|     path: Selections | ||||
|     profile: Selections | ||||
|     target: Selections | ||||
|     trajectory: Selections | ||||
|   } | ||||
|   Loft: { | ||||
|     selection: Selections | ||||
| @ -276,7 +281,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|     args: { | ||||
|       selection: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['solid2D', 'segment'], | ||||
|         selectionTypes: ['solid2d', 'segment'], | ||||
|         multiple: false, // TODO: multiple selection | ||||
|         required: true, | ||||
|         skip: true, | ||||
| @ -304,25 +309,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|       'Create a 3D body by moving a sketch region along an arbitrary path.', | ||||
|     icon: 'sweep', | ||||
|     status: 'development', | ||||
|     needsReview: true, | ||||
|     needsReview: false, | ||||
|     args: { | ||||
|       profile: { | ||||
|       target: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['solid2D'], | ||||
|         selectionTypes: ['solid2d'], | ||||
|         required: true, | ||||
|         skip: true, | ||||
|         multiple: false, | ||||
|         // TODO: add dry-run validation | ||||
|         warningMessage: | ||||
|           'The sweep workflow is new and under tested. Please break it and report issues.', | ||||
|       }, | ||||
|       path: { | ||||
|       trajectory: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['segment', 'path'], | ||||
|         required: true, | ||||
|         skip: true, | ||||
|         skip: false, | ||||
|         multiple: false, | ||||
|         // TODO: add dry-run validation | ||||
|         validation: sweepValidator, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| @ -333,7 +337,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|     args: { | ||||
|       selection: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['solid2D'], | ||||
|         selectionTypes: ['solid2d'], | ||||
|         multiple: true, | ||||
|         required: true, | ||||
|         skip: false, | ||||
| @ -351,12 +355,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|         selectionTypes: ['cap', 'wall'], | ||||
|         multiple: true, | ||||
|         required: true, | ||||
|         skip: false, | ||||
|         validation: shellValidator, | ||||
|       }, | ||||
|       thickness: { | ||||
|         inputType: 'kcl', | ||||
|         defaultValue: KCL_DEFAULT_LENGTH, | ||||
|         required: true, | ||||
|         // TODO: add dry-run validation on thickness param | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| @ -368,7 +373,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|     args: { | ||||
|       selection: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['solid2D', 'segment'], | ||||
|         selectionTypes: ['solid2d', 'segment'], | ||||
|         multiple: false, // TODO: multiple selection | ||||
|         required: true, | ||||
|         skip: true, | ||||
| @ -573,7 +578,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|       selection: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: [ | ||||
|           'solid2D', | ||||
|           'solid2d', | ||||
|           'segment', | ||||
|           'sweepEdge', | ||||
|           'cap', | ||||
|  | ||||
| @ -116,16 +116,16 @@ export const loftValidator = async ({ | ||||
|   } | ||||
|   const { selection } = data | ||||
|  | ||||
|   if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) { | ||||
|     return 'Unable to loft, some selection are not solid2Ds' | ||||
|   if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) { | ||||
|     return 'Unable to loft, some selection are not solid2ds' | ||||
|   } | ||||
|  | ||||
|   const sectionIds = data.selection.graphSelections.flatMap((s) => | ||||
|     s.artifact?.type === 'solid2D' ? s.artifact.pathId : [] | ||||
|     s.artifact?.type === 'solid2d' ? s.artifact.pathId : [] | ||||
|   ) | ||||
|  | ||||
|   if (sectionIds.length < 2) { | ||||
|     return 'Unable to loft, selection contains less than two solid2Ds' | ||||
|     return 'Unable to loft, selection contains less than two solid2ds' | ||||
|   } | ||||
|  | ||||
|   const loftCommand = async () => { | ||||
| @ -153,3 +153,118 @@ export const loftValidator = async ({ | ||||
|     return 'Unable to loft with selected sketches' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const shellValidator = async ({ | ||||
|   data, | ||||
| }: { | ||||
|   data: { selection: Selections } | ||||
| }): Promise<boolean | string> => { | ||||
|   if (!isSelections(data.selection)) { | ||||
|     return 'Unable to shell, selections are missing' | ||||
|   } | ||||
|  | ||||
|   // No validation on the faces, filtering is done upstream and we have the dry run validation just below | ||||
|   const face_ids = data.selection.graphSelections.flatMap((s) => | ||||
|     s.artifact ? s.artifact.id : [] | ||||
|   ) | ||||
|  | ||||
|   // We don't have the concept of solid3ds in TS yet. | ||||
|   // So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell: | ||||
|   // https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238 | ||||
|   // TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids | ||||
|   const object_id = engineCommandManager.artifactGraph | ||||
|     .values() | ||||
|     .find((v) => v.type === 'sweep')?.pathId | ||||
|  | ||||
|   if (!object_id) { | ||||
|     return "Unable to shell, couldn't find the solid" | ||||
|   } | ||||
|  | ||||
|   const shellCommand = async () => { | ||||
|     // TODO: figure out something better than an arbitrarily small value | ||||
|     const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9 | ||||
|     const DEFAULT_HOLLOW = false | ||||
|     const cmdArgs = { | ||||
|       face_ids, | ||||
|       object_id, | ||||
|       hollow: DEFAULT_HOLLOW, | ||||
|       shell_thickness: DEFAULT_THICKNESS, | ||||
|     } | ||||
|     return await engineCommandManager.sendSceneCommand({ | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd_id: uuidv4(), | ||||
|       cmd: { | ||||
|         type: 'solid3d_shell_face', | ||||
|         ...cmdArgs, | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   const attemptShell = await dryRunWrapper(shellCommand) | ||||
|   if (attemptShell?.success) { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   return 'Unable to shell with the provided selection' | ||||
| } | ||||
|  | ||||
| export const sweepValidator = async ({ | ||||
|   context, | ||||
|   data, | ||||
| }: { | ||||
|   context: CommandBarContext | ||||
|   data: { trajectory: Selections } | ||||
| }): Promise<boolean | string> => { | ||||
|   if (!isSelections(data.trajectory)) { | ||||
|     console.log('Unable to sweep, selections are missing') | ||||
|     return 'Unable to sweep, selections are missing' | ||||
|   } | ||||
|  | ||||
|   // Retrieve the parent path from the segment selection directly | ||||
|   const trajectoryArtifact = data.trajectory.graphSelections[0].artifact | ||||
|   if (!trajectoryArtifact) { | ||||
|     return "Unable to sweep, couldn't find the trajectory artifact" | ||||
|   } | ||||
|   if (trajectoryArtifact.type !== 'segment') { | ||||
|     return "Unable to sweep, couldn't find the target from a non-segment selection" | ||||
|   } | ||||
|   const trajectory = trajectoryArtifact.pathId | ||||
|  | ||||
|   // Get the former arg in the command bar flow, and retrieve the path from the solid2d directly | ||||
|   const targetArg = context.argumentsToSubmit['target'] as Selections | ||||
|   const targetArtifact = targetArg.graphSelections[0].artifact | ||||
|   if (!targetArtifact) { | ||||
|     return "Unable to sweep, couldn't find the profile artifact" | ||||
|   } | ||||
|   if (targetArtifact.type !== 'solid2d') { | ||||
|     return "Unable to sweep, couldn't find the target from a non-solid2d selection" | ||||
|   } | ||||
|   const target = targetArtifact.pathId | ||||
|  | ||||
|   const sweepCommand = async () => { | ||||
|     // TODO: second look on defaults here | ||||
|     const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7 | ||||
|     const DEFAULT_SECTIONAL = false | ||||
|     const cmdArgs = { | ||||
|       target, | ||||
|       trajectory, | ||||
|       sectional: DEFAULT_SECTIONAL, | ||||
|       tolerance: DEFAULT_TOLERANCE, | ||||
|     } | ||||
|     return await engineCommandManager.sendSceneCommand({ | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd_id: uuidv4(), | ||||
|       cmd: { | ||||
|         type: 'sweep', | ||||
|         ...cmdArgs, | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   const attemptSweep = await dryRunWrapper(sweepCommand) | ||||
|   if (attemptSweep?.success) { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   return 'Unable to sweep with the provided selection' | ||||
| } | ||||
|  | ||||
							
								
								
									
										58
									
								
								src/lib/desktopFS.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/lib/desktopFS.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| import { getUniqueProjectName } from './desktopFS' | ||||
| import { FileEntry } from './project' | ||||
|  | ||||
| /** Create a dummy project */ | ||||
| function project(name: string, children?: FileEntry[]): FileEntry { | ||||
|   return { | ||||
|     name, | ||||
|     children: children || [ | ||||
|       { name: 'main.kcl', children: null, path: 'main.kcl' }, | ||||
|     ], | ||||
|     path: `/projects/${name}`, | ||||
|   } | ||||
| } | ||||
|  | ||||
| describe(`Getting unique project names`, () => { | ||||
|   it(`should return the same name if no conflicts`, () => { | ||||
|     const projectName = 'new-project' | ||||
|     const projects = [project('existing-project'), project('another-project')] | ||||
|     const result = getUniqueProjectName(projectName, projects) | ||||
|     expect(result).toBe(projectName) | ||||
|   }) | ||||
|   it(`should return a unique name if there is a conflict`, () => { | ||||
|     const projectName = 'existing-project' | ||||
|     const projects = [project('existing-project'), project('another-project')] | ||||
|     const result = getUniqueProjectName(projectName, projects) | ||||
|     expect(result).toBe('existing-project-1') | ||||
|   }) | ||||
|   it(`should increment an ending index until a unique one is found`, () => { | ||||
|     const projectName = 'existing-project-1' | ||||
|     const projects = [ | ||||
|       project('existing-project'), | ||||
|       project('existing-project-1'), | ||||
|       project('existing-project-2'), | ||||
|     ] | ||||
|     const result = getUniqueProjectName(projectName, projects) | ||||
|     expect(result).toBe('existing-project-3') | ||||
|   }) | ||||
|   it(`should prefer the formatting of the index identifier if present`, () => { | ||||
|     const projectName = 'existing-project-$nn' | ||||
|     const projects = [ | ||||
|       project('existing-project'), | ||||
|       project('existing-project-1'), | ||||
|       project('existing-project-2'), | ||||
|     ] | ||||
|     const result = getUniqueProjectName(projectName, projects) | ||||
|     expect(result).toBe('existing-project-03') | ||||
|   }) | ||||
|   it(`be able to get an incrementing index regardless of padding zeroes`, () => { | ||||
|     const projectName = 'existing-project-$nn' | ||||
|     const projects = [ | ||||
|       project('existing-project'), | ||||
|       project('existing-project-01'), | ||||
|       project('existing-project-2'), | ||||
|     ] | ||||
|     const result = getUniqueProjectName(projectName, projects) | ||||
|     expect(result).toBe('existing-project-03') | ||||
|   }) | ||||
| }) | ||||
| @ -54,8 +54,10 @@ export function getNextProjectIndex( | ||||
|   const matches = projects.map((project) => project.name?.match(regex)) | ||||
|   const indices = matches | ||||
|     .filter(Boolean) | ||||
|     .map((match) => match![1]) | ||||
|     .map(Number) | ||||
|     .map((match) => (match !== null ? match[1] : '-1')) | ||||
|     .map((maybeMatchIndex) => { | ||||
|       return parseInt(maybeMatchIndex || '0', 10) | ||||
|     }) | ||||
|   const maxIndex = Math.max(...indices, -1) | ||||
|   return maxIndex + 1 | ||||
| } | ||||
| @ -83,6 +85,33 @@ export function doesProjectNameNeedInterpolated(projectName: string) { | ||||
|   return projectName.includes(INDEX_IDENTIFIER) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Given a target name, which may include our magic index interpolation string, | ||||
|  * and a list of projects, return a unique name that doesn't conflict with any | ||||
|  * of the existing projects, incrementing any ending number if necessary. | ||||
|  * @param name | ||||
|  * @param projects | ||||
|  * @returns | ||||
|  */ | ||||
| export function getUniqueProjectName(name: string, projects: FileEntry[]) { | ||||
|   // The name may have our magic index interpolation string in it | ||||
|   const needsInterpolation = doesProjectNameNeedInterpolated(name) | ||||
|  | ||||
|   if (needsInterpolation) { | ||||
|     const nextIndex = getNextProjectIndex(name, projects) | ||||
|     return interpolateProjectNameWithIndex(name, nextIndex) | ||||
|   } else { | ||||
|     let newName = name | ||||
|     while (projects.some((project) => project.name === newName)) { | ||||
|       const nameEndsWithNumber = newName.match(/\d+$/) | ||||
|       newName = nameEndsWithNumber | ||||
|         ? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`) | ||||
|         : `${name}-1` | ||||
|     } | ||||
|     return newName | ||||
|   } | ||||
| } | ||||
|  | ||||
| function escapeRegExpChars(string: string) { | ||||
|   return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { defaultRustSourceRange } from 'lang/wasm' | ||||
| import { defaultSourceRange } from 'lang/wasm' | ||||
| import { filterOperations } from './operations' | ||||
| import { Operation } from 'wasm-lib/kcl/bindings/Operation' | ||||
|  | ||||
| @ -8,7 +8,7 @@ function stdlib(name: string): Operation { | ||||
|     name, | ||||
|     unlabeledArg: null, | ||||
|     labeledArgs: {}, | ||||
|     sourceRange: defaultRustSourceRange(), | ||||
|     sourceRange: defaultSourceRange(), | ||||
|     isError: false, | ||||
|   } | ||||
| } | ||||
| @ -17,10 +17,10 @@ function userCall(name: string): Operation { | ||||
|   return { | ||||
|     type: 'UserDefinedFunctionCall', | ||||
|     name, | ||||
|     functionSourceRange: defaultRustSourceRange(), | ||||
|     functionSourceRange: defaultSourceRange(), | ||||
|     unlabeledArg: null, | ||||
|     labeledArgs: {}, | ||||
|     sourceRange: defaultRustSourceRange(), | ||||
|     sourceRange: defaultSourceRange(), | ||||
|   } | ||||
| } | ||||
| function userReturn(): Operation { | ||||
|  | ||||
| @ -3,8 +3,8 @@ import { VITE_KC_API_BASE_URL } from 'env' | ||||
| import crossPlatformFetch from './crossPlatformFetch' | ||||
| import { err, reportRejection } from './trap' | ||||
| import { Selections } from './selections' | ||||
| import { ArtifactGraph, getArtifactOfTypes } from 'lang/std/artifactGraph' | ||||
| import { SourceRange } from 'lang/wasm' | ||||
| import { getArtifactOfTypes } from 'lang/std/artifactGraph' | ||||
| import { ArtifactGraph, SourceRange, topLevelRange } from 'lang/wasm' | ||||
| import toast from 'react-hot-toast' | ||||
| import { codeManager, editorManager, kclManager } from './singletons' | ||||
| import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad' | ||||
| @ -334,7 +334,7 @@ const reBuildNewCodeWithRanges = ( | ||||
|     } else if (change.added && !change.removed) { | ||||
|       const start = newCodeWithRanges.length | ||||
|       const end = start + change.value.length | ||||
|       insertRanges.push([start, end, true]) | ||||
|       insertRanges.push(topLevelRange(start, end)) | ||||
|       newCodeWithRanges += change.value | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -10,6 +10,7 @@ import { | ||||
|   SourceRange, | ||||
|   Expr, | ||||
|   defaultSourceRange, | ||||
|   topLevelRange, | ||||
| } from 'lang/wasm' | ||||
| import { ModelingMachineEvent } from 'machines/modelingMachine' | ||||
| import { isNonNullable, uuidv4 } from 'lib/utils' | ||||
| @ -63,7 +64,7 @@ type Selection__old = | ||||
|         | 'line-end' | ||||
|         | 'line-mid' | ||||
|         | 'extrude-wall' | ||||
|         | 'solid2D' | ||||
|         | 'solid2d' | ||||
|         | 'start-cap' | ||||
|         | 'end-cap' | ||||
|         | 'point' | ||||
| @ -103,13 +104,13 @@ function convertSelectionToOld(selection: Selection): Selection__old | null { | ||||
|   // return {} as Selection__old | ||||
|   // TODO implementation | ||||
|   const _artifact = selection.artifact | ||||
|   if (_artifact?.type === 'solid2D') { | ||||
|   if (_artifact?.type === 'solid2d') { | ||||
|     const codeRef = getSolid2dCodeRef( | ||||
|       _artifact, | ||||
|       engineCommandManager.artifactGraph | ||||
|     ) | ||||
|     if (err(codeRef)) return null | ||||
|     return { range: codeRef.range, type: 'solid2D' } | ||||
|     return { range: codeRef.range, type: 'solid2d' } | ||||
|   } | ||||
|   if (_artifact?.type === 'cap') { | ||||
|     const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph) | ||||
| @ -269,7 +270,7 @@ export function getEventForSegmentSelection( | ||||
|         selectionType: 'singleCodeCursor', | ||||
|         selection: { | ||||
|           codeRef: { | ||||
|             range: [node.node.start, node.node.end, true], | ||||
|             range: topLevelRange(node.node.start, node.node.end), | ||||
|             pathToNode: group.userData.pathToNode, | ||||
|           }, | ||||
|         }, | ||||
| @ -381,10 +382,13 @@ export function processCodeMirrorRanges({ | ||||
|   if (!isChange) return null | ||||
|   const codeBasedSelections: Selections['graphSelections'] = | ||||
|     codeMirrorRanges.map(({ from, to }) => { | ||||
|       const pathToNode = getNodePathFromSourceRange(ast, [from, to, true]) | ||||
|       const pathToNode = getNodePathFromSourceRange( | ||||
|         ast, | ||||
|         topLevelRange(from, to) | ||||
|       ) | ||||
|       return { | ||||
|         codeRef: { | ||||
|           range: [from, to, true], | ||||
|           range: topLevelRange(from, to), | ||||
|           pathToNode, | ||||
|         }, | ||||
|       } | ||||
| @ -447,7 +451,10 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) { | ||||
|     if (err(nodeMeta)) return | ||||
|     const node = nodeMeta.node | ||||
|     const groupHasCursor = codeBasedSelections.some((selection) => { | ||||
|       return isOverlap(selection?.codeRef?.range, [node.start, node.end, true]) | ||||
|       return isOverlap( | ||||
|         selection?.codeRef?.range, | ||||
|         topLevelRange(node.start, node.end) | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     const color = groupHasCursor | ||||
| @ -575,7 +582,7 @@ export function getSelectionTypeDisplayText( | ||||
|       ([type, count]) => | ||||
|         `${count} ${type | ||||
|           .replace('wall', 'face') | ||||
|           .replace('solid2D', 'face') | ||||
|           .replace('solid2d', 'face') | ||||
|           .replace('segment', 'face')}${count > 1 ? 's' : ''}` | ||||
|     ) | ||||
|     .toArray() | ||||
| @ -650,7 +657,7 @@ export function codeToIdSelections( | ||||
|           const artifact = engineCommandManager.artifactGraph.get( | ||||
|             entry.artifact.solid2dId || '' | ||||
|           ) | ||||
|           if (artifact?.type !== 'solid2D') { | ||||
|           if (artifact?.type !== 'solid2d') { | ||||
|             bestCandidate = { | ||||
|               artifact: entry.artifact, | ||||
|               selection, | ||||
| @ -873,7 +880,7 @@ export function updateSelections( | ||||
|       return { | ||||
|         artifact: artifact, | ||||
|         codeRef: { | ||||
|           range: [node.start, node.end, true], | ||||
|           range: topLevelRange(node.start, node.end), | ||||
|           pathToNode: pathToNode, | ||||
|         }, | ||||
|       } | ||||
| @ -887,7 +894,7 @@ export function updateSelections( | ||||
|     if (err(node)) return node | ||||
|     pathToNodeBasedSelections.push({ | ||||
|       codeRef: { | ||||
|         range: [node.node.start, node.node.end, true], | ||||
|         range: topLevelRange(node.node.start, node.node.end), | ||||
|         pathToNode: pathToNode, | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
| @ -68,10 +68,6 @@ interface TextToKclProps { | ||||
|     data?: unknown | ||||
|   ) => unknown | ||||
|   navigate: NavigateFunction | ||||
|   commandBarSend: ( | ||||
|     type: EventFrom<typeof commandBarMachine>, | ||||
|     data?: unknown | ||||
|   ) => unknown | ||||
|   context: ContextFrom<typeof fileMachine> | ||||
|   token?: string | ||||
|   settings: { | ||||
| @ -84,7 +80,6 @@ export async function submitAndAwaitTextToKcl({ | ||||
|   trimmedPrompt, | ||||
|   fileMachineSend, | ||||
|   navigate, | ||||
|   commandBarSend, | ||||
|   context, | ||||
|   token, | ||||
|   settings, | ||||
| @ -96,7 +91,6 @@ export async function submitAndAwaitTextToKcl({ | ||||
|         ToastTextToCadError({ | ||||
|           toastId, | ||||
|           message, | ||||
|           commandBarSend, | ||||
|           prompt: trimmedPrompt, | ||||
|         }), | ||||
|       { | ||||
| @ -195,7 +189,7 @@ export async function submitAndAwaitTextToKcl({ | ||||
|         .toLowerCase()}${FILE_EXT}` | ||||
|  | ||||
|       if (isDesktop()) { | ||||
|         // We have to pre-emptively run our unique file name logic, | ||||
|         // We have to preemptively run our unique file name logic, | ||||
|         // so that we can pass the unique file name to the toast, | ||||
|         // and by extension the file-deletion-on-reject logic. | ||||
|         newFileName = getNextFileName({ | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user