Compare commits

..

27 Commits

Author SHA1 Message Date
0c2f63b399 Fix yarn lint to use all files recursively 2025-01-12 15:47:33 -05:00
363ae10658 Upgrade typescript-eslint from 5.62.0 to 8.19.1 and remove eslint-config-react-app (#5006) 2025-01-11 09:59:09 -05:00
ac4a6c84cf Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

* Clean up rust changes, taking the sketch with the most paths

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

* WIP: sweep point-and-click

* Working sweep

* Add test

* Make sweep a development command

* Fix tsc error

* Clean up for review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-11 08:20:49 -05:00
c6fad2e2dc Add new lint to disallow use of confusing isNaN (#4999) 2025-01-11 05:28:12 +00:00
013cb10961 Fix so that all artifact commands are returned regardless of caching (#5005)
* Fix so that all artifact commands are returned regardless of caching

* Add some more docs and fix up old ones
2025-01-10 22:33:05 -05:00
6261083cb1 Make the test executor a bit more patient (#5004) 2025-01-10 20:05:27 -05:00
2b0ba37ed0 Use Chromium instead of Chrome for Playwright Electron (#5001)
* Use Chromium instead of Chrome for Playwright Electron

* Remove channel
2025-01-10 13:37:26 -05:00
96174f3cf6 Increase playwright retries to 5 (#5000) 2025-01-10 13:34:27 -05:00
aed62ff912 Fix flaky playwright test 'Shell point-and-click sketch on face' (#5002)
Fixes #4998
2025-01-10 13:32:31 -05:00
9334d64608 Allow under-development commands in Nightly builds (#4995)
* Allow under-development commands in Nightly builds
Fixes #4994

* Fix warning

* Add back status: development to Revolve
2025-01-10 16:24:07 +00:00
4fa7d2d8c8 Feature: new axis and edge selection workflow for point and click revolve (#4939)
* feat: implemented axis or edge selection workflow in the commandbar

* fix: removing comment

* fix: removing console logs from testing

* fix: fixing lint and tsc errors

* fix: changed copy

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-10 08:52:04 -06:00
3e615dfdbc Update Katie's name reference and link in onboarding (#4967) 2025-01-09 22:07:40 -05:00
c9860af29f Fix Shell point-and-click picking the wrong face with piped extrudes (#4981)
* [BUG] Shell point and click references the wrong feature
Fixes #4961

* Add test for sketch on face based on extrudes in pipe

* Add no extrude in pipe case

* Lint

* Add scene.waitForExecutionDone()

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Update src/lang/modifyAst/addShell.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-10 01:20:07 +00:00
23a42f0195 Bump @kittycad/lib to v2.0.13 (#4988) 2025-01-09 16:02:05 -05:00
a77fa639f3 Point-and-click deletion of lofts, shells, and offset planes (#4898)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

* Clean up rust changes, taking the sketch with the most paths

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-09 15:36:50 -05:00
0a5ad7c95b Show deprecated indicator in CodeMirror autocomplete (#4983) 2025-01-09 09:15:00 -05:00
4a654523d2 Prevent toSync from clobbering stack traces (#4980)
* Prevent toSync from clobbering stack traces

* Capture error on the outside of the toSync catch

* fmt

* Actually fix it 🤦
2025-01-09 03:40:42 +00:00
73a7e2bfd6 Return modeling commands from KCL execution (#4912)
* Add Rust side artifacts for startSketchOn face or plane

* Add Rust-generated artifacts to ExecOutcome

* Add output of artifact commands

* Add new output files

* Wire the artifact commands to the artifact graph creation

* Fix to use real PartialEq implemented in modeling commands

* Fix modeling commands with zero fields to work

* Fix missing artifactCommands field in errors

* Change artifact graph to be built from artifact commands

* Wire up ExecState artifacts, but not using them yet

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Remove unneeded local var

* Fix test to fail with a helpful error message when command isn't found

* Rename and deprecate orderedCommands

* Update comment about borrowing

* Move ArtifactCommand tracking to the EngineManager trait

* Update artifact commands since tracking in the engine

* Upgrade kittycad-modeling-cmds from 0.2.85 to 0.2.86

* Remove unneeded JsonSchema derive to speed up build

* Fix to not fail on floating point differences in CI

* Update artifact commands output since truncating floating point numbers

* Fix to ensure artifact commands get cleared after a clear scene

* Update artifact commands snapshot after clearing them on clear scene

* Remove all remnants of OrderedCommands

* Update output for new simulation tests

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2025-01-09 01:02:30 +00:00
eb0850fea9 Make codespell smooth when run locally (#4978)
* Skip the out/ directory produced by yarn tron:package

* Skip all dist/ dirs

Produced when building rollup packages like codemirror-lang-kcl.

* Skip typescript build info

* Fix typo instead of excluding file

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
2025-01-08 10:36:37 -06:00
029f76f273 Nadro/4857/wasm panic catching errors (#4901)
* chore: skeleton code to initialize and detect the global WASM panic

* chore: implementing a reimport method to fix the wasm instance being bricked

* fix: cleaning up tsc/lint

* fix: renaming file to be more accurate

* fix: added toast message

* fix: types...

* fix: typed the functions with arg spreads
2025-01-08 15:58:41 +00:00
max
28b5f7080c Refactor Fillet AST Mod to Async Actor (#4803) 2025-01-08 16:05:24 +01:00
5b1dcfecd6 Open updater toast changelog links externally (#4970)
* fix: Hook into markdown-generated anchors to avoid e.g breaking the desktop app

* add comment

* Disable eslint on copied line from ts-stack

---------

Co-authored-by: marc2332 <mespinsanz@gmail.com>
2025-01-08 09:15:18 -05:00
f89d191425 add a test for foreign characters in project name (#4976)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-08 05:34:08 -05:00
2f4e4b62a8 Don't wait for !isExecuting to play the stream (#4971)
* Add failing test for current behavior

* Change stream behavior so that stream is played regardless of `isExecuting`

* Change expected pixel color

* Widen possible pixel color diff because local and CI produce slightly different colors
2025-01-08 04:34:57 -05:00
5ebd5c8dbb Enhance helixes (#4973)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* allow a helix to go into a sweep

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* snapshots

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* em,pty

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-08 03:10:53 +00:00
a9ceaf2678 fix: make error for missing a closing bracket clearer (#4974)
* fix: make error for missing a closing bracket clearer

* Fix test for error message

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2025-01-08 01:55:07 +00:00
c8afd3399b Dead code clean up (smol PR) (#4653)
* general clean up

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-08 01:46:05 +00:00
249 changed files with 115660 additions and 6085 deletions

View File

@ -1,3 +1,3 @@
[codespell] [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
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts,./packages/codemirror-lang-kcl/test/all.test.ts 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

View File

@ -5,16 +5,24 @@
}, },
"plugins": [ "plugins": [
"css-modules", "css-modules",
"jest",
"react",
"suggest-no-throw", "suggest-no-throw",
"@typescript-eslint"
], ],
"extends": [ "extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended" "plugin:css-modules/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-misused-promises": "error",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"semi": [ "semi": [
"error", "error",
"never" "never"

View File

@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
fi fi
retry=1 retry=1
max_retrys=4 max_retrys=5
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do while [[ $retry -le $max_retrys ]]; do

View File

@ -24,3 +24,5 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work. - **Appearance**: Changing the appearance on a loft does not work.
- **Helix**: Currently sweeping a helix does not work.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,7 @@ layout: manual
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix) * [`helix`](kcl/helix)
* [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`import`](kcl/import) * [`import`](kcl/import)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,19 @@
--- ---
title: "AxisOrEdgeReference" title: "Axis2dOrEdgeReference"
excerpt: "Axis or tagged edge." excerpt: "A 2D axis or tagged edge."
layout: manual layout: manual
--- ---
Axis or tagged edge. A 2D axis or tagged edge.
**This schema accepts any of the following:** **This schema accepts any of the following:**
Axis and origin. 2D axis and origin.
[`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin) [`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d)

View File

@ -0,0 +1,42 @@
---
title: "Axis3dOrEdgeReference"
excerpt: "A 3D axis or tagged edge."
layout: manual
---
A 3D axis or tagged edge.
**This schema accepts any of the following:**
3D axis and origin.
[`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d)
----
Tagged edge.
[`EdgeReference`](/docs/kcl/types/EdgeReference)
----

View File

@ -1,10 +1,10 @@
--- ---
title: "AxisAndOrigin" title: "AxisAndOrigin2d"
excerpt: "Axis and origin." excerpt: "A 2D axis and origin."
layout: manual layout: manual
--- ---
Axis and origin. A 2D axis and origin.

View File

@ -0,0 +1,105 @@
---
title: "AxisAndOrigin3d"
excerpt: "A 3D axis and origin."
layout: manual
---
A 3D axis and origin.
**This schema accepts exactly one of the following:**
X-axis.
**enum:** `X`
----
Y-axis.
**enum:** `Y`
----
Z-axis.
**enum:** `Z`
----
Flip the X-axis.
**enum:** `-X`
----
Flip the Y-axis.
**enum:** `-Y`
----
Flip the Z-axis.
**enum:** `-Z`
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `custom` |`object`| | No |
----

25
docs/kcl/types/Helix.md Normal file
View File

@ -0,0 +1,25 @@
---
title: "Helix"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `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 |

View File

@ -1,10 +1,10 @@
--- ---
title: "HelixData" title: "HelixData"
excerpt: "Data for helices." excerpt: "Data for a helix."
layout: manual layout: manual
--- ---
Data for helices. Data for a helix.
**Type:** `object` **Type:** `object`
@ -19,6 +19,8 @@ Data for helices.
| `revolutions` |`number`| Number of revolutions. | No | | `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No | | `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No | | `length` |`number`| Length of the helix. | No |
| `radius` |`number`| Radius of the helix. | No |
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -0,0 +1,24 @@
---
title: "HelixRevolutionsData"
excerpt: "Data for helix revolutions."
layout: manual
---
Data for helix revolutions.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |

View File

@ -0,0 +1,25 @@
---
title: "HelixValue"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `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 |

View File

@ -285,6 +285,27 @@ An solid is a collection of extrude surfaces.
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No | | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
----
A helix.
**Type:** `object`
## Properties
| 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 |
---- ----
Data for an imported geometry. Data for an imported geometry.

View File

@ -16,6 +16,6 @@ Data for a mirror.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No | | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -17,7 +17,7 @@ Data for revolution surfaces.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No |
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No | | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No |
| `tolerance` |`number`| Tolerance for the revolve operation. | No | | `tolerance` |`number`| Tolerance for the revolve operation. | No |

View File

@ -16,7 +16,7 @@ Data for a sweep.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No | | `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No | | `tolerance` |`number`| Tolerance for the sweep operation. | No |

View File

@ -0,0 +1,42 @@
---
title: "SweepPath"
excerpt: "A path to sweep along."
layout: manual
---
A path to sweep along.
**This schema accepts any of the following:**
A path to sweep along.
[`Sketch`](/docs/kcl/types/Sketch)
----
A path to sweep along.
[`Helix`](/docs/kcl/types/Helix)
----

View File

@ -54,26 +54,23 @@ async function doBasicSketch(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator).toContainText( await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` |> startProfileAt(${commonPoints.startAt}, %)`)
)
} }
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(500) await page.waitForTimeout(500)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) |> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
} }
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
} else { } else {
@ -82,10 +79,8 @@ async function doBasicSketch(
await page.waitForTimeout(200) await page.waitForTimeout(200)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`) |> xLine(${commonPoints.num2 * -1}, %)`)
@ -142,10 +137,8 @@ async function doBasicSketch(
// Open the code pane. // Open the code pane.
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %, $seg01) |> xLine(${commonPoints.num1}, %, $seg01)
|> yLine(${commonPoints.num1 + 0.01}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(-segLen(seg01), %)`) |> xLine(-segLen(seg01), %)`)

View File

@ -38,7 +38,8 @@ test.describe('Can create sketches on all planes and their back sides', () => {
}, },
} }
const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` const code = `sketch001 = startSketchOn('${plane}')
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel() await u.openDebugPanel()

View File

@ -9,15 +9,13 @@ import {
sendCustomCmd, sendCustomCmd,
} from '../test-utils' } from '../test-utils'
type MouseParams = { type mouseParams = {
pixelDiff?: number pixelDiff?: number
shouldDbClick?: boolean
delay?: number
} }
type MouseDragToParams = MouseParams & { type mouseDragToParams = mouseParams & {
fromPoint: { x: number; y: number } fromPoint: { x: number; y: number }
} }
type MouseDragFromParams = MouseParams & { type mouseDragFromParams = mouseParams & {
toPoint: { x: number; y: number } toPoint: { x: number; y: number }
} }
@ -28,17 +26,18 @@ type SceneSerialised = {
} }
} }
type ClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean> type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: MouseDragFromParams dragParams: mouseDragFromParams
) => Promise<void | boolean> ) => Promise<void | boolean>
export class SceneFixture { export class SceneFixture {
public page: Page public page: Page
public streamWrapper!: Locator
public loadingIndicator!: Locator
private exeIndicator!: Locator private exeIndicator!: Locator
constructor(page: Page) { constructor(page: Page) {
@ -66,6 +65,8 @@ export class SceneFixture {
this.page = page this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.streamWrapper = page.getByTestId('stream')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
} }
makeMouseHelpers = ( makeMouseHelpers = (
@ -74,26 +75,17 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler, DblClickHandler] => ): [ClickHandler, MoveHandler, DblClickHandler] =>
[ [
(clickParams?: MouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
() => () => this.page.mouse.click(x, y),
clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y, {
delay: clickParams?.delay || 0,
})
: this.page.mouse.click(x, y, {
delay: clickParams?.delay || 0,
}),
clickParams.pixelDiff clickParams.pixelDiff
) )
} }
return clickParams?.shouldDbClick return this.page.mouse.click(x, y)
? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0 })
: this.page.mouse.click(x, y, { delay: clickParams?.delay || 0 })
}, },
(moveParams?: MouseParams) => { (moveParams?: mouseParams) => {
if (moveParams?.pixelDiff) { if (moveParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -103,7 +95,7 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: MouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -120,7 +112,7 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [DragToHandler, DragFromHandler] => ): [DragToHandler, DragFromHandler] =>
[ [
(dragToParams: MouseDragToParams) => { (dragToParams: mouseDragToParams) => {
if (dragToParams?.pixelDiff) { if (dragToParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -137,7 +129,7 @@ export class SceneFixture {
targetPosition: { x, y }, targetPosition: { x, y },
}) })
}, },
(dragFromParams: MouseDragFromParams) => { (dragFromParams: mouseDragFromParams) => {
if (dragFromParams?.pixelDiff) { if (dragFromParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -225,7 +217,7 @@ export class SceneFixture {
} }
expectPixelColor = async ( expectPixelColor = async (
colour: [number, number, number] | [number, number, number][], colour: [number, number, number],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) => { ) => {
@ -247,36 +239,22 @@ export class SceneFixture {
} }
} }
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return Array.isArray(colour[0])
}
export async function expectPixelColor( export async function expectPixelColor(
page: Page, page: Page,
colour: [number, number, number] | [number, number, number][], colour: [number, number, number],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) { ) {
let finalValue = colour let finalValue = colour
await expect await expect
.poll( .poll(async () => {
async () => { const pixel = (await getPixelRGBs(page)(coords, 1))[0]
const pixel = (await getPixelRGBs(page)(coords, 1))[0] if (!pixel) return null
if (!pixel) return null finalValue = pixel
finalValue = pixel return pixel.every(
if (!isColourArray(colour)) { (channel, index) => Math.abs(channel - colour[index]) < diff
return pixel.every( )
(channel, index) => Math.abs(channel - colour[index]) < diff })
)
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
},
{ timeout: 10_000 }
)
.toBeTruthy() .toBeTruthy()
.catch((cause) => { .catch((cause) => {
throw new Error( throw new Error(

View File

@ -14,14 +14,12 @@ export class ToolbarFixture {
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator shellButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator exitSketchBtn!: Locator
editSketchBtn!: Locator editSketchBtn!: Locator
fileTreeBtn!: Locator fileTreeBtn!: Locator
@ -43,14 +41,12 @@ export class ToolbarFixture {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell') this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit') this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch') this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]') this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -109,15 +105,6 @@ export class ToolbarFixture {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
} }
} }
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down Corner rectangle:' })
.click()
await expect(
this.page.getByTestId('dropdown-center-rectangle')
).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click()
}
async closePane(paneId: SidebarType) { async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -437,7 +437,7 @@ test.describe('Onboarding tests', () => {
) )
}) })
test( test.fixme(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ {
appSettings: { appSettings: {

View File

@ -218,13 +218,18 @@ test.describe('verify sketch on chamfer works', () => {
]}, %)`, ]}, %)`,
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
'startProfileAt([205.96, 254.59], sketch002)', afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) |> angledLine([
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) segAng(rectangleSegmentA002) - 90,
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) 105.26
|>lineTo([profileStartX(%),profileStartY(%)],%) ], %, $rectangleSegmentB001)
|>close(%)`, |> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -244,15 +249,19 @@ test.describe('verify sketch on chamfer works', () => {
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
'startProfileAt([-209.64, 255.28], sketch003)', afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003) |> angledLine([
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%) segAng(rectangleSegmentA003) - 90,
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%) 106.84
|>lineTo([profileStartX(%),profileStartY(%)],%) ], %, $rectangleSegmentB002)
|>close(%)`, |> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -264,14 +273,19 @@ test.describe('verify sketch on chamfer works', () => {
getNextAdjacentEdge(seg02) getNextAdjacentEdge(seg02)
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch004 = startSketchOn(extrude001, seg05)', afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
'startProfileAt([82.57, 322.96], sketch004)', afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004) |> angledLine([
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%) segAng(rectangleSegmentA003) - 90,
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%) 106.84
|>lineTo([profileStartX(%),profileStartY(%)],%)| ], %, $rectangleSegmentB002)
>close(%)`, |> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -283,97 +297,104 @@ test.describe('verify sketch on chamfer works', () => {
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
'startProfileAt([-23.43, 19.69], sketch005)', afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%) |> angledLine([
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) segAng(rectangleSegmentA005) - 90,
|>lineTo([profileStartX(%),profileStartY(%)],%) 84.07
|>close(%)`, ], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([ |> angledLine([0, 268.43], %, $rectangleSegmentA001)
segAng(rectangleSegmentA001) - 90, |> angledLine([
217.26 segAng(rectangleSegmentA001) - 90,
], %, $seg01) 217.26
|> angledLine([ ], %, $seg01)
segAng(rectangleSegmentA001), |> angledLine([
-segLen(rectangleSegmentA001) segAng(rectangleSegmentA001),
], %, $yo) -segLen(rectangleSegmentA001)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02) ], %, $yo)
|> close(%) |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
extrude001 = extrude(100, sketch001) |> close(%)
|> chamfer({ extrude001 = extrude(100, sketch001)
length = 30, |> chamfer({
tags = [getOppositeEdge(seg01)] length = 30,
}, %, $seg03) tags = [getOppositeEdge(seg01)]
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04) }, %, $seg03)
|> chamfer({ |> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
length = 30, |> chamfer({
tags = [getNextAdjacentEdge(seg02)] length = 30,
}, %, $seg05) tags = [getNextAdjacentEdge(seg02)]
|> chamfer({ }, %, $seg05)
length = 30, |> chamfer({
tags = [getNextAdjacentEdge(yo)] length = 30,
}, %, $seg06) tags = [getNextAdjacentEdge(yo)]
sketch005 = startSketchOn(extrude001, seg06) }, %, $seg06)
profile004 = startProfileAt([-23.43, 19.69], sketch005) sketch005 = startSketchOn(extrude001, seg06)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> startProfileAt([-23.43,19.69], %)
|> angledLine([ |> angledLine([0, 9.1], %, $rectangleSegmentA005)
segAng(rectangleSegmentA005) - 90, |> angledLine([
84.07 segAng(rectangleSegmentA005) - 90,
], %) 84.07
|> angledLine([ ], %, $rectangleSegmentB004)
segAng(rectangleSegmentA005), |> angledLine([
-segLen(rectangleSegmentA005) segAng(rectangleSegmentA005),
], %) -segLen(rectangleSegmentA005)
|> lineTo([profileStartX(%), profileStartY(%)], %) ], %, $rectangleSegmentC004)
|> close(%) |> lineTo([profileStartX(%), profileStartY(%)], %)
sketch004 = startSketchOn(extrude001, seg05) |> close(%)
profile003 = startProfileAt([82.57, 322.96], sketch004) sketch004 = startSketchOn(extrude001, seg05)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> startProfileAt([82.57,322.96], %)
|> angledLine([ |> angledLine([0, 11.16], %, $rectangleSegmentA004)
segAng(rectangleSegmentA004) - 90, |> angledLine([
103.07 segAng(rectangleSegmentA004) - 90,
], %) 103.07
|> angledLine([ ], %, $rectangleSegmentB003)
segAng(rectangleSegmentA004), |> angledLine([
-segLen(rectangleSegmentA004) segAng(rectangleSegmentA004),
], %) -segLen(rectangleSegmentA004)
|> lineTo([profileStartX(%), profileStartY(%)], %) ], %, $rectangleSegmentC003)
|> close(%) |> lineTo([profileStartX(%), profileStartY(%)], %)
sketch003 = startSketchOn(extrude001, seg04) |> close(%)
profile002 = startProfileAt([-209.64, 255.28], sketch003) sketch003 = startSketchOn(extrude001, seg04)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> startProfileAt([-209.64,255.28], %)
|> angledLine([ |> angledLine([0, 11.56], %, $rectangleSegmentA003)
segAng(rectangleSegmentA003) - 90, |> angledLine([
106.84 segAng(rectangleSegmentA003) - 90,
], %) 106.84
|> angledLine([ ], %, $rectangleSegmentB002)
segAng(rectangleSegmentA003), |> angledLine([
-segLen(rectangleSegmentA003) segAng(rectangleSegmentA003),
], %) -segLen(rectangleSegmentA003)
|> lineTo([profileStartX(%), profileStartY(%)], %) ], %, $rectangleSegmentC002)
|> close(%) |> lineTo([profileStartX(%), profileStartY(%)], %)
sketch002 = startSketchOn(extrude001, seg03) |> close(%)
profile001 = startProfileAt([205.96, 254.59], sketch002) sketch002 = startSketchOn(extrude001, seg03)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> startProfileAt([205.96,254.59], %)
|> angledLine([ |> angledLine([0, 11.39], %, $rectangleSegmentA002)
segAng(rectangleSegmentA002) - 90, |> angledLine([
105.26 segAng(rectangleSegmentA002) - 90,
], %) 105.26
|> angledLine([ ], %, $rectangleSegmentB001)
segAng(rectangleSegmentA002), |> angledLine([
-segLen(rectangleSegmentA002) segAng(rectangleSegmentA002),
], %) -segLen(rectangleSegmentA002)
|> lineTo([profileStartX(%), profileStartY(%)], %) ], %, $rectangleSegmentC001)
|> close(%) |> lineTo([profileStartX(%), profileStartY(%)], %)
`, |> close(%)
`,
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) })
@ -416,13 +437,18 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
]}, extrude001)`, ]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
'startProfileAt([205.96, 254.59], sketch002)', afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002) |> angledLine([
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) segAng(rectangleSegmentA002) - 90,
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) 105.26
|>lineTo([profileStartX(%),profileStartY(%)],%) ], %, $rectangleSegmentB001)
|>close(%)`, |> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
@ -452,16 +478,16 @@ chamf = chamfer({
] ]
}, %) }, %)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
profile001 = startProfileAt([205.96, 254.59], sketch002) |> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
], %) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002), segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002) -segLen(rectangleSegmentA002)
], %) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -529,10 +555,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
} }
await test.step(`Start a sketch on the XZ plane`, async () => { await test.step(`Start a sketch on the XZ plane`, async () => {
@ -730,6 +756,17 @@ test(`Offset plane point-and-click`, async ({
}) })
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation(
'Offset Plane',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
}) })
const loftPointAndClickCases = [ const loftPointAndClickCases = [
@ -825,6 +862,173 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
}) })
await scene.expectPixelColor([89, 89, 89], testPoint, 15) await scene.expectPixelColor([89, 89, 89], testPoint, 15)
}) })
await test.step('Delete loft via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
})
// TODO: merge with above test. Right now we're not able to delete a loft
// right after creation via selection for some reason, so we go with a new instance
test('Loft and offset plane deletion via selection', async ({
context,
page,
homePage,
scene,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002])
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
await test.step(`Delete loft`, async () => {
// Check for loft
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
await clickOnSketch1()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 30 }, %)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
await test.step('Delete sketch002', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 20 }, %)
`)
await page.keyboard.press('Backspace')
// Check for plane001
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
})
await test.step('Delete plane001', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
test(`Sweep point-and-click`, 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(-500, %)
|> tangentialArcTo([-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)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
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`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'profile',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '',
},
highlightedHeaderArg: 'profile',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '1 face',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [sweepDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
})
await test.step('Delete sweep via feature tree selection', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
}) })
}) })
@ -1004,4 +1208,104 @@ extrude001 = extrude(40, sketch001)
}) })
await scene.expectPixelColor([49, 49, 49], testPoint, 15) await scene.expectPixelColor([49, 49, 49], testPoint, 15)
}) })
await test.step('Delete shell via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
})
const shellSketchOnFacesCases = [
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
|> extrude(100, %)
sketch002 = startSketchOn(sketch001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
|> extrude(50, %)
`,
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
extrude001 = extrude(100, sketch001)
sketch002 = startSketchOn(extrude001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
extrude002 = extrude(50, sketch002)
`,
]
shellSketchOnFacesCases.forEach((initialCode, index) => {
const hasExtrudesInPipe = index === 0
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
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: 550, y: 295 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
})`
await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
})
})
}) })

View File

@ -115,7 +115,7 @@ test(
) )
test( test(
'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene', 'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -199,7 +199,7 @@ test(
) )
test( test(
'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene', 'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -276,7 +276,7 @@ test(
) )
test( test(
'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene', 'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -1885,3 +1885,48 @@ test.fixme(
}) })
} }
) )
test(
'project name with foreign characters should open',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl')
)
await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible()
await page.getByText('اَلْعَرَبِيَّةُ').click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
}
)

View File

@ -614,6 +614,38 @@ extrude001 = extrude(50, sketch001)
await expect(gizmo).toBeVisible() await expect(gizmo).toBeVisible()
}) })
}) })
test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({
context,
homePage,
scene,
toolbar,
viewport,
}) => {
await context.folderSetupFn(async (dir) => {
const legoDir = path.join(dir, 'lego')
await fsp.mkdir(legoDir, { recursive: true })
await fsp.copyFile(
executorInputPath('lego.kcl'),
path.join(legoDir, 'main.kcl')
)
})
await test.step(`Test setup`, async () => {
await homePage.openProject('lego')
await toolbar.closePane('code')
})
await test.step(`Waiting for the loading spinner to disappear`, async () => {
await scene.loadingIndicator.waitFor({ state: 'detached' })
})
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
await scene.expectPixelColor(
[143, 143, 143],
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
15
)
})
})
}) })
async function clickExportButton(page: Page) { async function clickExportButton(page: Page) {

File diff suppressed because it is too large Load Diff

View File

@ -451,7 +451,8 @@ test(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -473,10 +474,6 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -599,7 +596,8 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` `sketch001 = startSketchOn('XZ')
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
) )
} }
) )
@ -643,7 +641,8 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -661,10 +660,6 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
@ -751,7 +746,8 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` code += `
|> startProfileAt([182.59, -246.32], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -769,10 +765,6 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,7 +1,6 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils' import { commonPoints, getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ test('simulate network down and network little widget', async ({
@ -111,17 +110,18 @@ test.describe('Test network and connection issues', () => {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` .toHaveText(`sketch001 = startSketchOn('XZ')
) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) .toHaveText(`sketch001 = startSketchOn('XZ')
|> xLine(${commonPoints.num1}, %)`) |> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up // Expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
@ -168,9 +168,7 @@ test.describe('Test network and connection issues', () => {
await page.mouse.click(100, 100) await page.mouse.click(100, 100)
// select a line // select a line
await page await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
.click()
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
@ -184,36 +182,11 @@ test.describe('Test network and connection issues', () => {
await page.waitForTimeout(150) await page.waitForTimeout(150)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching // Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([12.34, -12.34], sketch001) |> startProfileAt([12.34, -12.34], %)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
@ -223,7 +196,7 @@ profile001 = startProfileAt([12.34, -12.34], sketch001)
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([12.34, -12.34], sketch001) |> startProfileAt([12.34, -12.34], %)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
|> xLine(-12.34, %) |> xLine(-12.34, %)

View File

@ -19,7 +19,7 @@ test.describe('Testing constraints', () => {
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> xLine(-20, %) |> xLine(-20, %)
` `
) )
}) })

View File

@ -69,34 +69,33 @@ test.describe('Testing selections', () => {
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` .toHaveText(`sketch001 = startSketchOn('XZ')
) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) .toHaveText(`sketch001 = startSketchOn('XZ')
|> xLine(${commonPoints.num1}, %)`) |> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ .toHaveText(`sketch001 = startSketchOn('XZ')
commonPoints.startAt |> startProfileAt(${commonPoints.startAt}, %)
}, sketch001) |> xLine(${commonPoints.num1}, %)
|> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %)`)
|> yLine(${commonPoints.num1 + 0.01}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ .toHaveText(`sketch001 = startSketchOn('XZ')
commonPoints.startAt |> startProfileAt(${commonPoints.startAt}, %)
}, sketch001) |> xLine(${commonPoints.num1}, %)
|> xLine(${commonPoints.num1}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num2 * -1}, %)`)
|> xLine(${commonPoints.num2 * -1}, %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -264,88 +263,66 @@ test.describe('Testing selections', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %) |> startProfileAt([-79.26, 95.04], %)
|> line([112.54, 127.64], %, $seg02) |> line([112.54, 127.64], %, $seg02)
|> line([170.36, -121.61], %, $seg01) |> line([170.36, -121.61], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(50, sketch001) extrude001 = extrude(50, sketch001)
sketch005 = startSketchOn(extrude001, 'END') sketch005 = startSketchOn(extrude001, 'END')
|> startProfileAt([23.24, 136.52], %) |> startProfileAt([23.24, 136.52], %)
|> line([-8.44, 36.61], %) |> line([-8.44, 36.61], %)
|> line([49.4, 2.05], %) |> line([49.4, 2.05], %)
|> line([29.69, -46.95], %) |> line([29.69, -46.95], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch003 = startSketchOn(extrude001, seg01) sketch003 = startSketchOn(extrude001, seg01)
|> startProfileAt([21.23, 17.81], %) |> startProfileAt([21.23, 17.81], %)
|> line([51.97, 21.32], %) |> line([51.97, 21.32], %)
|> line([4.07, -22.75], %) |> line([4.07, -22.75], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch002 = startSketchOn(extrude001, seg02) sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-100.54, 16.99], %) |> startProfileAt([-100.54, 16.99], %)
|> line([0, 20.03], %) |> line([0, 20.03], %)
|> line([62.61, 0], %, $seg03) |> line([62.61, 0], %, $seg03)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude002 = extrude(50, sketch002) extrude002 = extrude(50, sketch002)
sketch004 = startSketchOn(extrude002, seg03) sketch004 = startSketchOn(extrude002, seg03)
|> startProfileAt([57.07, 134.77], %) |> startProfileAt([57.07, 134.77], %)
|> line([-4.72, 22.84], %) |> line([-4.72, 22.84], %)
|> line([28.8, 6.71], %) |> line([28.8, 6.71], %)
|> line([9.19, -25.33], %) |> line([9.19, -25.33], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude003 = extrude(20, sketch004) extrude003 = extrude(20, sketch004)
pipeLength = 40 pipeLength = 40
pipeSmallDia = 10 pipeSmallDia = 10
pipeLargeDia = 20 pipeLargeDia = 20
thickness = 0.5 thickness = 0.5
part009 = startSketchOn('XY') part009 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line([thickness, 0], %) |> line([thickness, 0], %)
|> line([0, -1], %) |> line([0, -1], %)
|> angledLineToX({ |> angledLineToX({
angle = 60, angle = 60,
to = pipeSmallDia + thickness to = pipeSmallDia + thickness
}, %) }, %)
|> line([0, -pipeLength], %) |> line([0, -pipeLength], %)
|> angledLineToX({ |> angledLineToX({
angle = -60, angle = -60,
to = pipeLargeDia + thickness to = pipeLargeDia + thickness
}, %) }, %)
|> line([0, -1], %) |> line([0, -1], %)
|> line([-thickness, 0], %) |> line([-thickness, 0], %)
|> line([0, 1], %) |> line([0, 1], %)
|> angledLineToX({ angle = 120, to = pipeSmallDia }, %) |> angledLineToX({ angle = 120, to = pipeSmallDia }, %)
|> line([0, pipeLength], %) |> line([0, pipeLength], %)
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close(%) |> close(%)
rev = revolve({ axis = 'y' }, part009) rev = revolve({ axis: 'y' }, part009)
sketch006 = startSketchOn('XY') `
profile001 = circle({
center = [42.91, -70.42],
radius = 17.96
}, sketch006)
profile002 = startProfileAt([86.92, -63.81], sketch006)
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
17.05
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line([26.95, 24.21], %)
|> line([20.91, -28.61], %)
|> line([32.46, 18.71], %)
`
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -377,10 +354,9 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const revolve = { x: 635, y: 253 } const revolve = { x: 646, y: 248 }
const parentExtrude = { x: 915, y: 133 } const parentExtrude = { x: 915, y: 133 }
const solid2d = { x: 770, y: 167 } const solid2d = { x: 770, y: 167 }
const individualProfile = { x: 694, y: 432 }
// DELETE REVOLVE // DELETE REVOLVE
await page.mouse.click(revolve.x, revolve.y) await page.mouse.click(revolve.x, revolve.y)
@ -413,25 +389,25 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
plane = { plane = {
origin = { x = 0, y = -50, z = 0 }, origin = { x = 0, y = -50, z = 0 },
x_axis = { x = 1, y = 0, z = 0 }, xAxis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 0, z = 1 }, yAxis = { x = 0, y = 0, z = 1 },
z_axis = { x = 0, y = -1, z = 0 } zAxis = { x = 0, y = -1, z = 0 }
} }
})`) })`)
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
plane = { plane = {
origin = { x = 116.53, y = 0, z = 163.25 }, origin = { x = 116.53, y = 0, z = 163.25 },
x_axis = { x = -0.81, y = 0, z = 0.58 }, xAxis = { x = -0.81, y = 0, z = 0.58 },
y_axis = { x = 0, y = -1, z = 0 }, yAxis = { x = 0, y = -1, z = 0 },
z_axis = { x = 0.58, y = 0, z = 0.81 } zAxis = { x = 0.58, y = 0, z = 0.81 }
} }
})`) })`)
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
plane = { plane = {
origin = { x = -91.74, y = 0, z = 80.89 }, origin = { x = -91.74, y = 0, z = 80.89 },
x_axis = { x = -0.66, y = 0, z = -0.75 }, xAxis = { x = -0.66, y = 0, z = -0.75 },
y_axis = { x = 0, y = -1, z = 0 }, yAxis = { x = 0, y = -1, z = 0 },
z_axis = { x = -0.75, y = 0, z = 0.66 } zAxis = { x = -0.75, y = 0, z = 0.66 }
} }
})`) })`)
@ -446,20 +422,6 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
// Delete a single profile
await page.mouse.click(individualProfile.x, individualProfile.y)
await page.waitForTimeout(100)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line([20.91, -28.61], %)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
}) })
test("Deleting solid that the AST mod can't handle results in a toast message", async ({ test("Deleting solid that the AST mod can't handle results in a toast message", async ({
page, page,
@ -1311,15 +1273,12 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.waitForTimeout(600) await page.waitForTimeout(600)
const firstClickCoords = { x: 650, y: 200 } as const
// Place a point because the line tool will exit if no points are pressed // Place a point because the line tool will exit if no points are pressed
await page.mouse.click(firstClickCoords.x, firstClickCoords.y) await page.mouse.click(650, 200)
await page.waitForTimeout(600) await page.waitForTimeout(600)
// Code before exiting the tool // Code before exiting the tool
let previousCodeContent = ( let previousCodeContent = await page.locator('.cm-content').innerText()
await page.locator('.cm-content').innerText()
).replace(/\s+/g, '')
// deselect the line tool by clicking it // deselect the line tool by clicking it
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -1331,23 +1290,14 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.mouse.click(750, 200) await page.mouse.click(750, 200)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect // expect no change
.poll(async () => { await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
let str = await page.locator('.cm-content').innerText()
str = str.replace(/\s+/g, '')
return str
})
.toBe(previousCodeContent)
// select line tool again // select line tool again
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
await u.closeDebugPanel() await u.closeDebugPanel()
// Click to continue profile
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(100)
// line tool should work as expected again // line tool should work as expected again
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).not.toHaveText( await expect(page.locator('.cm-content')).not.toHaveText(

View File

@ -205,13 +205,8 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Draw a line // Draw a line
await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
const secondMousePosition = { x: 800, y: 250 } await page.mouse.click(800, 250)
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
// Unequip line tool // Unequip line tool
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -220,17 +215,9 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Equip arc tool // Equip arc tool
await page.keyboard.press('a') await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100) await page.mouse.click(1000, 100)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await page.keyboard.press('l') await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
@ -532,9 +519,9 @@ extrude001 = extrude(5 + 7, sketch001)`
await expect.poll(u.normalisedEditorCode).toContain( await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
profile001 = startProfileAt([-12.88, 6.66], sketch002) |> startProfileAt([-12.94, 6.6], %)
|> line([2.71, -0.22], %) |> line([2.45, -0.2], %)
|> line([-2.87, -1.38], %) |> line([-2.6, -1.25], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`) `)
@ -550,8 +537,9 @@ profile001 = startProfileAt([-12.88, 6.66], sketch002)
await page.getByText('startProfileAt([-12').click() await page.getByText('startProfileAt([-12').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) await page.waitForTimeout(400)
await page.setViewportSize({ width: 1200, height: 1200 }) await page.waitForTimeout(150)
await page.setBodyDimensions({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166]) await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel() await u.closeDebugPanel()

18
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1721933792, "lastModified": 1736320768,
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=", "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1", "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1718428119, "lastModified": 1728538411,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1721960387, "lastModified": 1736476219,
"narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=", "narHash": "sha256-+qyv3QqdZCdZ3cSO/cbpEY6tntyYjfe1bB12mdpNFaY=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "9cbf831c5b20a53354fc12758abd05966f9f1699", "rev": "de30cc5963da22e9742bbbbb9a3344570ed237b9",
"type": "github" "type": "github"
}, },
"original": { "original": {

1
interface.d.ts vendored
View File

@ -93,5 +93,6 @@ export interface IElectronAPI {
declare global { declare global {
interface Window { interface Window {
electron: IElectronAPI electron: IElectronAPI
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
} }
} }

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.12", "@kittycad/lib": "2.0.13",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1", "@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -91,8 +91,8 @@
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", "wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client", "lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client", "lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -171,8 +171,6 @@
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
@ -182,9 +180,10 @@
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-notarize": "1.2.2", "electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^16.3.0", "happy-dom": "^16.3.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
@ -200,6 +199,7 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6", "vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
// try to parse the content-length from the headers // try to parse the content-length from the headers
const length = parseInt(match[1]) const length = parseInt(match[1])
if (isNaN(length)) if (Number.isNaN(length))
return Promise.reject(new Error('invalid content length')) return Promise.reject(new Error('invalid content length'))
// slice the headers since we now have the content length // slice the headers since we now have the content length

View File

@ -368,13 +368,20 @@ export class LanguageServerPlugin implements PluginValue {
sortText, sortText,
filterText, filterText,
}) => { }) => {
const detailText = [
deprecated ? 'Deprecated' : undefined,
labelDetails ? labelDetails.detail : detail,
]
// Don't let undefined appear.
.filter(Boolean)
.join(' ')
const completion: Completion & { const completion: Completion & {
filterText: string filterText: string
sortText?: string sortText?: string
apply: string apply: string
} = { } = {
label, label,
detail: labelDetails ? labelDetails.detail : detail, detail: detailText,
apply: label, apply: label,
type: kind && CompletionItemKindMap[kind].toLowerCase(), type: kind && CompletionItemKindMap[kind].toLowerCase(),
sortText: sortText ?? label, sortText: sortText ?? label,
@ -382,7 +389,11 @@ export class LanguageServerPlugin implements PluginValue {
} }
if (documentation) { if (documentation) {
completion.info = () => { completion.info = () => {
const htmlString = formatMarkdownContents(documentation) const deprecatedHtml = deprecated
? '<p><strong>Deprecated</strong></p>'
: ''
const htmlString =
deprecatedHtml + formatMarkdownContents(documentation)
const htmlNode = document.createElement('div') const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents' htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString htmlNode.innerHTML = htmlString

View File

@ -32,10 +32,9 @@ export default defineConfig({
}, },
projects: [ projects: [
{ {
name: 'Google Chrome', name: 'chromium',
use: { use: {
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
channel: 'chrome',
contextOptions: { contextOptions: {
/* Chromium is the only one with these permission types */ /* Chromium is the only one with these permission types */
permissions: ['clipboard-write', 'clipboard-read'], permissions: ['clipboard-write', 'clipboard-read'],

View File

@ -6,6 +6,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -21,7 +22,6 @@ import {
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { isCursorInFunctionDefinition } from 'lang/queryAst'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -38,12 +38,7 @@ export function Toolbar({
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if ( if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
isCursorInFunctionDefinition(
kclManager.ast,
context.selectionRanges.graphSelections[0]
)
)
return false return false
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,

View File

@ -438,8 +438,6 @@ export async function deleteSegment({
if (!sketchDetails) return if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
pathToNode, pathToNode,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst, modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,

File diff suppressed because it is too large Load Diff

View File

@ -696,21 +696,19 @@ export function createProfileStartHandle({
scale = 1, scale = 1,
theme, theme,
isSelected, isSelected,
size = 12,
...rest ...rest
}: { }: {
from: Coords2d from: Coords2d
scale?: number scale?: number
theme: Themes theme: Themes
isSelected?: boolean isSelected?: boolean
size?: number
} & ( } & (
| { isDraft: true } | { isDraft: true }
| { isDraft: false; id: string; pathToNode: PathToNode } | { isDraft: false; id: string; pathToNode: PathToNode }
)) { )) {
const group = new Group() const group = new Group()
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme) const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor const color = isSelected ? 0x0000ff : baseColor
const body = new MeshBasicMaterial({ color }) const body = new MeshBasicMaterial({ color })

View File

@ -21,6 +21,7 @@ import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project' import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
@ -187,25 +188,24 @@ const FileTreeItem = ({
// Because subtrees only render when they are opened, that means this // Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when // only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :) // the ReactNodes are destroyed, so is this listener :)
/** Disabling this in favor of faster file writes until we fix file writing **/ useFileSystemWatcher(
/* useFileSystemWatcher( async (eventType, path) => {
* async (eventType, path) => { // Prevents a cyclic read / write causing editor problems such as
* // Prevents a cyclic read / write causing editor problems such as // misplaced cursor positions.
* // misplaced cursor positions. if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
* if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
* codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false return
* return }
* }
* if (isCurrentFile && eventType === 'change') { if (isCurrentFile && eventType === 'change') {
* let code = await window.electron.readFile(path, { encoding: 'utf-8' }) let code = await window.electron.readFile(path, { encoding: 'utf-8' })
* code = normalizeLineEndings(code) code = normalizeLineEndings(code)
* codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
* } }
* fileSend({ type: 'Refresh' }) fileSend({ type: 'Refresh' })
* }, },
* [fileOrDir.path] [fileOrDir.path]
* ) */ )
const showNewTreeEntry = const showNewTreeEntry =
newTreeEntry !== undefined && newTreeEntry !== undefined &&

View File

@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { import {
isCursorInSketchCommandRange, isCursorInSketchCommandRange,
updateSketchDetailsNodePaths, updatePathToNodeFromMap,
} from 'lang/util' } from 'lang/util'
import { import {
kclManager, kclManager,
@ -64,30 +64,20 @@ import {
replaceValueAtNodePath, replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
splitPipedProfile,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import { import {
artifactIsPlaneWithPaths, artifactIsPlaneWithPaths,
doesSketchPipeNeedSplitting, getNodePathFromSourceRange,
getNodeFromPath, isSingleCursorInPipe,
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst' } from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap, reject } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { import {
ExportIntent, ExportIntent,
@ -99,10 +89,6 @@ import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
@ -171,39 +157,38 @@ export const ModelingMachineProvider = ({
'enable copilot': () => { 'enable copilot': () => {
editorManager.setCopilotEnabled(true) editorManager.setCopilotEnabled(true)
}, },
// tsc reports this typing as perfectly fine, but eslint is complaining. 'sketch exit execute': ({ context: { store } }) => {
// It's actually nonsensical, so I'm quieting. // TODO: Remove this async callback. For some reason eslint wouldn't
// eslint-disable-next-line @typescript-eslint/no-misused-promises // let me disable @typescript-eslint/no-misused-promises for the line.
'sketch exit execute': async ({ ;(async () => {
context: { store }, // When cancelling the sketch mode we should disable sketch mode within the engine.
}): Promise<void> => { await engineCommandManager.sendSceneCommand({
// When cancelling the sketch mode we should disable sketch mode within the engine. type: 'modeling_cmd_req',
await engineCommandManager.sendSceneCommand({ cmd_id: uuidv4(),
type: 'modeling_cmd_req', cmd: { type: 'sketch_mode_disable' },
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
}) })
.catch(reportRejection)
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
})
.catch(reportRejection)
})().catch(reportRejection)
}, },
'Set mouse state': assign(({ context, event }) => { 'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {} if (event.type !== 'Set mouse state') return {}
@ -285,6 +270,7 @@ export const ModelingMachineProvider = ({
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'default_camera_center_to_selection', type: 'default_camera_center_to_selection',
camera_movement: 'vantage',
}, },
}) })
.catch(reportRejection) .catch(reportRejection)
@ -295,7 +281,7 @@ export const ModelingMachineProvider = ({
return { return {
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchEntryNodePath: event.data, sketchPathToNode: event.data,
}, },
} }
}), }),
@ -410,17 +396,9 @@ export const ModelingMachineProvider = ({
selectionRanges: setSelections.selection, selectionRanges: setSelections.selection,
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchEntryNodePath: sketchPathToNode:
setSelections.updatedSketchEntryNodePath || setSelections.updatedPathToNode ||
sketchDetails?.sketchEntryNodePath || sketchDetails?.sketchPathToNode ||
[],
sketchNodePaths:
setSelections.updatedSketchNodePaths ||
sketchDetails?.sketchNodePaths ||
[],
planeNodePath:
setSelections.updatedPlaneNodePath ||
sketchDetails?.planeNodePath ||
[], [],
}, },
} }
@ -574,12 +552,7 @@ export const ModelingMachineProvider = ({
if (artifactIsPlaneWithPaths(selectionRanges)) { if (artifactIsPlaneWithPaths(selectionRanges)) {
return true return true
} }
if ( if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
)
return false return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
@ -610,32 +583,10 @@ export const ModelingMachineProvider = ({
// this assumes no changes have been made to the sketch besides what we did when entering the sketch // this assumes no changes have been made to the sketch besides what we did when entering the sketch
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
const newAst = structuredClone(kclManager.ast) const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.planeNodePath[1][0] const varDecIndex = sketchDetails.sketchPathToNode[1][0]
const varDec = getNodeFromPath<VariableDeclaration>(
newAst,
sketchDetails.planeNodePath,
'VariableDeclaration'
)
if (err(varDec)) return reject(new Error('No varDec'))
const variableName = varDec.node.declaration.id.name
let isIdentifierUsed = false
traverse(newAst, {
enter: (node) => {
if (
node.type === 'Identifier' &&
node.name === variableName
) {
isIdentifierUsed = true
}
},
})
if (isIdentifierUsed) return
// remove body item at varDecIndex // remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst) await kclManager.executeAstMock(newAst)
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
} }
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: () => {}, onClick: () => {},
@ -645,7 +596,7 @@ export const ModelingMachineProvider = ({
} }
), ),
'animate-to-face': fromPromise(async ({ input }) => { 'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return null if (!input) return undefined
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched = const sketched =
input.type === 'extrudeFace' input.type === 'extrudeFace'
@ -672,9 +623,7 @@ export const ModelingMachineProvider = ({
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchEntryNodePath: [], sketchPathToNode: pathToNewSketchNode,
planeNodePath: pathToNewSketchNode,
sketchNodePaths: [],
zAxis: input.zAxis, zAxis: input.zAxis,
yAxis: input.yAxis, yAxis: input.yAxis,
origin: input.position, origin: input.position,
@ -694,9 +643,7 @@ export const ModelingMachineProvider = ({
) )
return { return {
sketchEntryNodePath: [], sketchPathToNode: pathToNode,
planeNodePath: pathToNode,
sketchNodePaths: [],
zAxis: input.zAxis, zAxis: input.zAxis,
yAxis: input.yAxis, yAxis: input.yAxis,
origin: [0, 0, 0], origin: [0, 0, 0],
@ -704,14 +651,12 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sketchPathToNode = const sourceRange =
selectionRanges.graphSelections[0]?.codeRef?.pathToNode selectionRanges.graphSelections[0]?.codeRef?.range
const plane = getPlaneFromArtifact( const sketchPathToNode = getNodePathFromSourceRange(
selectionRanges.graphSelections[0].artifact, kclManager.ast,
engineCommandManager.artifactGraph sourceRange
) )
if (err(plane)) return Promise.reject(plane)
const info = await getSketchOrientationDetails( const info = await getSketchOrientationDetails(
sketchPathToNode || [] sketchPathToNode || []
) )
@ -719,17 +664,8 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
const sketchPaths = getPathsFromArtifact({
artifact: selectionRanges.graphSelections[0].artifact,
sketchPathToNode: sketchPathToNode || [],
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
if (!plane.codeRef)
return Promise.reject(new Error('No plane codeRef'))
return { return {
sketchEntryNodePath: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath: plane.codeRef.pathToNode,
zAxis: info.sketchDetails.zAxis || null, zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null, yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map( origin: info.sketchDetails.origin.map(
@ -741,7 +677,7 @@ export const ModelingMachineProvider = ({
'Get horizontal info': fromPromise( 'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, selectionRanges,
@ -753,23 +689,13 @@ export const ModelingMachineProvider = ({
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -790,15 +716,13 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get vertical info': fromPromise( 'Get vertical info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, selectionRanges,
@ -809,23 +733,13 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -846,9 +760,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -858,15 +770,14 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
}) })
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } = await (info.enabled
await (info.enabled ? applyConstraintAngleBetween({
? applyConstraintAngleBetween({ selectionRanges,
selectionRanges, })
}) : applyConstraintAngleLength({
: applyConstraintAngleLength({ selectionRanges,
selectionRanges, angleOrLength: 'setAngle',
angleOrLength: 'setAngle', }))
}))
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
@ -875,23 +786,13 @@ export const ModelingMachineProvider = ({
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -912,9 +813,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -929,30 +828,20 @@ export const ModelingMachineProvider = ({
length: lengthValue, length: lengthValue,
}) })
if (err(constraintResult)) return Promise.reject(constraintResult) if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } = constraintResult
constraintResult
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -973,15 +862,13 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get perpendicular distance info': fromPromise( 'Get perpendicular distance info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } =
await applyConstraintIntersect({ await applyConstraintIntersect({
selectionRanges, selectionRanges,
}) })
@ -991,22 +878,13 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1027,15 +905,13 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS X info': fromPromise( 'Get ABS X info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'xAbs', constraint: 'xAbs',
selectionRanges, selectionRanges,
@ -1046,22 +922,13 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1082,15 +949,13 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS Y info': fromPromise( 'Get ABS Y info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap, exprInsertIndex } = const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'yAbs', constraint: 'yAbs',
selectionRanges, selectionRanges,
@ -1101,22 +966,13 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
const { sketchDetails.sketchPathToNode,
updatedSketchEntryNodePath, pathToNodeMap
updatedSketchNodePaths, )
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1137,9 +993,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -1159,11 +1013,9 @@ export const ModelingMachineProvider = ({
let result: { let result: {
modifiedAst: Node<Program> modifiedAst: Node<Program>
pathToReplaced: PathToNode | null pathToReplaced: PathToNode | null
exprInsertIndex: number
} = { } = {
modifiedAst: parsed, modifiedAst: parsed,
pathToReplaced: null, pathToReplaced: null,
exprInsertIndex: -1,
} }
// If the user provided a constant name, // If the user provided a constant name,
// we need to insert the named constant // we need to insert the named constant
@ -1193,7 +1045,6 @@ export const ModelingMachineProvider = ({
result = { result = {
modifiedAst: parseResultAfterInsertion.program, modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced, pathToReplaced: astAfterReplacement.pathToReplaced,
exprInsertIndex: astAfterReplacement.exprInsertIndex,
} }
} else if ('valueText' in data.namedValue) { } else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name, // If they didn't provide a constant name,
@ -1224,22 +1075,10 @@ export const ModelingMachineProvider = ({
parsed = parsed as Node<Program> parsed = parsed as Node<Program>
if (!result.pathToReplaced) if (!result.pathToReplaced)
return Promise.reject(new Error('No path to replaced node')) return Promise.reject(new Error('No path to replaced node'))
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex: result.exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedSketchEntryNodePath, result.pathToReplaced || [],
updatedSketchNodePaths,
updatedPlaneNodePath,
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1260,140 +1099,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedSketchEntryNodePath, updatedPathToNode: result.pathToReplaced,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'set-up-draft-circle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-center-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
})
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')
const existingSketchInfoNoOp = {
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
} as const
if (
!sketchDetails.sketchNodePaths.length &&
sketchDetails.planeNodePath.length
) {
// new sketch, no profiles yet
return existingSketchInfoNoOp
}
const doesNeedSplitting = doesSketchPipeNeedSplitting(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
if (!doesNeedSplitting) return existingSketchInfoNoOp
const splitResult = splitPipedProfile(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
await kclManager.executeAstMock(splitResult.modifiedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
splitResult.modifiedAst
)
return {
updatedEntryNodePath: splitResult.pathToProfile,
updatedSketchNodePaths: [splitResult.pathToProfile],
updatedPlaneNodePath: sketchDetails.planeNodePath,
} }
} }
), ),

View File

@ -2,12 +2,7 @@ import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => { export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return ( return (
<svg <svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
data-testid="spinner"
viewBox="0 0 10 10"
className={'w-8 h-8'}
{...props}
>
<circle <circle
cx="5" cx="5"
cy="5" cy="5"

View File

@ -218,20 +218,6 @@ export const Stream = () => {
} }
}, [IDLE, streamState]) }, [IDLE, streamState])
/**
* Play the vid
*/
useEffect(() => {
if (!kclManager.isExecuting) {
setTimeout(() => {
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
}
}, [kclManager.isExecuting])
useEffect(() => { useEffect(() => {
if ( if (
typeof window === 'undefined' || typeof window === 'undefined' ||
@ -243,9 +229,15 @@ export const Stream = () => {
// The browser complains if we try to load a new stream without pausing first. // The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream! // Do not immediately play the stream!
// we instead use a setTimeout to play the stream in the next event loop
try { try {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
videoRef.current.pause() videoRef.current.pause()
setTimeout(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
} catch (e) { } catch (e) {
console.warn('Attempted to pause stream while play was still loading', e) console.warn('Attempted to pause stream while play was still loading', e)
} }

View File

@ -150,4 +150,31 @@ describe('ToastUpdate tests', () => {
expect(restartButton).toBeEnabled() expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled() expect(dismissButton).toBeEnabled()
}) })
test('Happy path: external links render correctly', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- [Zoo](https://zoo.dev/)
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const zooDev = screen.getByText('Zoo', {
selector: 'a',
})
expect(zooDev).toHaveAttribute('href', 'https://zoo.dev/')
expect(zooDev).toHaveAttribute('target', '_blank')
expect(zooDev).toHaveAttribute('onClick')
})
}) })

View File

@ -1,8 +1,9 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown' import { escape, Marked, MarkedOptions, unescape } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings' import { getReleaseUrl } from 'routes/Settings'
import { SafeRenderer } from 'lib/markdown'
export function ToastUpdate({ export function ToastUpdate({
version, version,
@ -19,6 +20,14 @@ export function ToastUpdate({
?.toLocaleLowerCase() ?.toLocaleLowerCase()
.includes('breaking') .includes('breaking')
const markedOptions: MarkedOptions = {
gfm: true,
breaks: true,
sanitize: true,
unescape,
escape,
}
return ( return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md"> <div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -58,9 +67,8 @@ export function ToastUpdate({
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto" className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, { __html: Marked.parse(releaseNotes, {
gfm: true, renderer: new SafeRenderer(markedOptions),
breaks: true, ...markedOptions,
sanitize: true,
}), }),
}} }}
></div> ></div>

View File

@ -136,7 +136,6 @@ export async function applyConstraintIntersect({
}): Promise<{ }): Promise<{
modifiedAst: Node<Program> modifiedAst: Node<Program>
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const info = intersectInfo({ const info = intersectInfo({
selectionRanges, selectionRanges,
@ -175,7 +174,6 @@ export async function applyConstraintIntersect({
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: -1,
} }
} }
// transform again but forcing certain values // transform again but forcing certain values
@ -194,7 +192,6 @@ export async function applyConstraintIntersect({
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
transform2 transform2
let exprInsertIndex = -1
if (variableName) { if (variableName) {
const newBody = [..._modifiedAst.body] const newBody = [..._modifiedAst.body]
newBody.splice( newBody.splice(
@ -207,11 +204,9 @@ export async function applyConstraintIntersect({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1
}) })
exprInsertIndex = newVariableInsertIndex
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap: _pathToNodeMap, pathToNodeMap: _pathToNodeMap,
exprInsertIndex,
} }
} }

View File

@ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({
| Error { | Error {
const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => {
const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode) const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode)
if (tmp instanceof Error) return tmp if (err(tmp)) return tmp
return tmp.node return tmp.node
}) })
const _err1 = _nodes.find(err) const _err1 = _nodes.find(err)

View File

@ -93,7 +93,6 @@ export async function applyConstraintAbsDistance({
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const info = absDistanceInfo({ const info = absDistanceInfo({
selectionRanges, selectionRanges,
@ -133,7 +132,6 @@ export async function applyConstraintAbsDistance({
if (err(transform2)) return Promise.reject(transform2) if (err(transform2)) return Promise.reject(transform2)
const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2 const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2
let exprInsertIndex = -1
if (variableName) { if (variableName) {
const newBody = [..._modifiedAst.body] const newBody = [..._modifiedAst.body]
newBody.splice( newBody.splice(
@ -146,9 +144,8 @@ export async function applyConstraintAbsDistance({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1
}) })
exprInsertIndex = newVariableInsertIndex
} }
return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex } return { modifiedAst: _modifiedAst, pathToNodeMap }
} }
export function applyConstraintAxisAlign({ export function applyConstraintAxisAlign({

View File

@ -86,7 +86,6 @@ export async function applyConstraintAngleBetween({
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const info = angleBetweenInfo({ selectionRanges }) const info = angleBetweenInfo({ selectionRanges })
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
@ -123,7 +122,6 @@ export async function applyConstraintAngleBetween({
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: -1,
} }
} }
@ -143,7 +141,6 @@ export async function applyConstraintAngleBetween({
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } = const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
transformed2 transformed2
let exprInsertIndex = -1
if (variableName) { if (variableName) {
const newBody = [..._modifiedAst.body] const newBody = [..._modifiedAst.body]
newBody.splice( newBody.splice(
@ -156,11 +153,9 @@ export async function applyConstraintAngleBetween({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1
}) })
exprInsertIndex = newVariableInsertIndex
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap: _pathToNodeMap, pathToNodeMap: _pathToNodeMap,
exprInsertIndex,
} }
} }

View File

@ -87,13 +87,15 @@ export function horzVertDistanceInfo({
export async function applyConstraintHorzVertDistance({ export async function applyConstraintHorzVertDistance({
selectionRanges, selectionRanges,
constraint, constraint,
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
isAlign = false,
}: { }: {
selectionRanges: Selections selectionRanges: Selections
constraint: 'setHorzDistance' | 'setVertDistance' constraint: 'setHorzDistance' | 'setVertDistance'
isAlign?: false
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const info = horzVertDistanceInfo({ const info = horzVertDistanceInfo({
selectionRanges: selectionRanges, selectionRanges: selectionRanges,
@ -131,12 +133,13 @@ export async function applyConstraintHorzVertDistance({
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: -1,
} }
} else { } else {
if (!isExprBinaryPart(valueNode)) if (!isExprBinaryPart(valueNode))
return Promise.reject('Invalid valueNode, is not a BinaryPart') return Promise.reject('Invalid valueNode, is not a BinaryPart')
let finalValue = removeDoubleNegatives(valueNode, sign, variableName) let finalValue = isAlign
? createLiteral(0)
: removeDoubleNegatives(valueNode, sign, variableName)
// transform again but forcing certain values // transform again but forcing certain values
const transformed = transformSecondarySketchLinesTagFirst({ const transformed = transformSecondarySketchLinesTagFirst({
ast: kclManager.ast, ast: kclManager.ast,
@ -149,7 +152,6 @@ export async function applyConstraintHorzVertDistance({
if (err(transformed)) return Promise.reject(transformed) if (err(transformed)) return Promise.reject(transformed)
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed
let exprInsertIndex = -1
if (variableName) { if (variableName) {
const newBody = [..._modifiedAst.body] const newBody = [..._modifiedAst.body]
newBody.splice( newBody.splice(
@ -162,12 +164,10 @@ export async function applyConstraintHorzVertDistance({
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1 pathToNode[index][0] = Number(pathToNode[index][0]) + 1
}) })
exprInsertIndex = newVariableInsertIndex
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex,
} }
} }
} }

View File

@ -70,14 +70,10 @@ export async function applyConstraintLength({
}: { }: {
length: KclCommandValue length: KclCommandValue
selectionRanges: Selections selectionRanges: Selections
}): Promise<{ }) {
modifiedAst: Program
pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> {
const ast = kclManager.ast const ast = kclManager.ast
const angleLength = angleLengthInfo({ selectionRanges }) const angleLength = angleLengthInfo({ selectionRanges })
if (err(angleLength)) return Promise.reject(angleLength) if (err(angleLength)) return angleLength
const { transforms } = angleLength const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst let distanceExpression: Expr = length.valueAst
@ -98,7 +94,7 @@ export async function applyConstraintLength({
} }
if (!isExprBinaryPart(distanceExpression)) { if (!isExprBinaryPart(distanceExpression)) {
return Promise.reject('Invalid valueNode, is not a BinaryPart') return new Error('Invalid valueNode, is not a BinaryPart')
} }
const retval = transformAstSketchLines({ const retval = transformAstSketchLines({
@ -116,12 +112,6 @@ export async function applyConstraintLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex:
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
? length.insertIndex
: -1,
} }
} }
@ -134,7 +124,6 @@ export async function applyConstraintAngleLength({
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const angleLength = angleLengthInfo({ selectionRanges, angleOrLength }) const angleLength = angleLengthInfo({ selectionRanges, angleOrLength })
if (err(angleLength)) return Promise.reject(angleLength) if (err(angleLength)) return Promise.reject(angleLength)
@ -219,6 +208,5 @@ export async function applyConstraintAngleLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
} }
} }

View File

@ -10,8 +10,11 @@ import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate' import { ToastUpdate } from 'components/ToastUpdate'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants' import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
import { initializeWindowExceptionHandler } from 'lib/exceptions'
markOnce('code/willAuth') markOnce('code/willAuth')
initializeWindowExceptionHandler()
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
// import { inspect } from '@xstate/inspect' // import { inspect } from '@xstate/inspect'

View File

@ -378,11 +378,14 @@ export class KclManager {
// updateArtifactGraph relies on updated executeState/programMemory // updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph( await this.engineCommandManager.updateArtifactGraph(
this.ast, this.ast,
execState.artifactCommands,
execState.artifacts execState.artifacts
) )
this._executeCallback() this._executeCallback()
if (!isInterrupted) if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
}
this.engineCommandManager.addCommandLog({ this.engineCommandManager.addCommandLog({
type: 'execution-done', type: 'execution-done',
data: null, data: null,
@ -391,6 +394,24 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId) this._cancelTokens.delete(currentExecutionId)
markOnce('code/endExecuteAst') markOnce('code/endExecuteAst')
} }
/**
* This cleanup function is external and internal to the KclSingleton class.
* Since the WASM runtime can panic and the error cannot be caught in executeAst
* we need a global exception handler in exceptions.ts
* This file will interface with this cleanup as if it caught the original error
* to properly restore the TS application state.
*/
executeAstCleanUp() {
this.isExecuting = false
this.executeIsStale = null
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,
})
markOnce('code/endExecuteAst')
}
// NOTE: this always updates the code state and editor. // NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever. // DO NOT CALL THIS from codemirror ever.
async executeAstMock( async executeAstMock(
@ -424,7 +445,6 @@ export class KclManager {
this._logs = logs this._logs = logs
this.addDiagnostics(kclErrorsToDiagnostics(errors)) this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState this._execState = execState
this._programMemory = execState.memory this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
@ -437,7 +457,7 @@ export class KclManager {
// problem this solves, but either way we should strive to remove it. // problem this solves, but either way we should strive to remove it.
Array.from(this.engineCommandManager.artifactGraph).forEach( Array.from(this.engineCommandManager.artifactGraph).forEach(
([commandId, artifact]) => { ([commandId, artifact]) => {
if (!('codeRef' in artifact && artifact.codeRef)) return if (!('codeRef' in artifact)) return
const _node1 = getNodeFromPath<Node<CallExpression>>( const _node1 = getNodeFromPath<Node<CallExpression>>(
this.ast, this.ast,
artifact.codeRef.pathToNode, artifact.codeRef.pathToNode,

View File

@ -47,7 +47,7 @@ describe('parsing errors', () => {
const result = parse(code) const result = parse(code)
if (err(result)) throw result if (err(result)) throw result
const error = result.errors[0] const error = result.errors[0]
expect(error.message).toBe('Unexpected token: (') expect(error.message).toBe('Array is missing a closing bracket(`]`)')
expect(error.sourceRange).toEqual([27, 28, 0]) expect(error.sourceRange).toEqual([28, 29, 0])
}) })
}) })

View File

@ -153,7 +153,7 @@ export default class CodeManager {
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
reject(err) reject(err)
}) })
}, 10) }, 1000)
}) })
} else { } else {
safeLSSetItem(PERSIST_CODE_KEY, this.code) safeLSSetItem(PERSIST_CODE_KEY, this.code)

View File

@ -10,6 +10,7 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Semantic error', msg: 'Semantic error',
sourceRange: [0, 1, true], sourceRange: [0, 1, true],
operations: [], operations: [],
artifactCommands: [],
}, },
{ {
name: '', name: '',
@ -18,6 +19,7 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Type error', msg: 'Type error',
sourceRange: [4, 5, true], sourceRange: [4, 5, true],
operations: [], operations: [],
artifactCommands: [],
}, },
] ]
const diagnostics = kclErrorsToDiagnostics(errors) const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -5,7 +5,7 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state' import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror' import { EditorView } from 'codemirror'
import { SourceRange } from 'lang/wasm' import { ArtifactCommand, SourceRange } from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -14,86 +14,141 @@ export class KCLError extends Error {
sourceRange: SourceRange sourceRange: SourceRange
msg: string msg: string
operations: Operation[] operations: Operation[]
artifactCommands: ArtifactCommand[]
constructor( constructor(
kind: ExtractKind<RustKclError> | 'name', kind: ExtractKind<RustKclError> | 'name',
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[] operations: Operation[],
artifactCommands: ArtifactCommand[]
) { ) {
super() super()
this.kind = kind this.kind = kind
this.msg = msg this.msg = msg
this.sourceRange = sourceRange this.sourceRange = sourceRange
this.operations = operations this.operations = operations
this.artifactCommands = artifactCommands
Object.setPrototypeOf(this, KCLError.prototype) Object.setPrototypeOf(this, KCLError.prototype)
} }
} }
export class KCLLexicalError extends KCLError { export class KCLLexicalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('lexical', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('lexical', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLInternalError extends KCLError { export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('internal', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('internal', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSyntaxError extends KCLError { export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('syntax', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('syntax', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSemanticError extends KCLError { export class KCLSemanticError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('semantic', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('semantic', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSemanticError.prototype) Object.setPrototypeOf(this, KCLSemanticError.prototype)
} }
} }
export class KCLTypeError extends KCLError { export class KCLTypeError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('type', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('type', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLTypeError.prototype) Object.setPrototypeOf(this, KCLTypeError.prototype)
} }
} }
export class KCLUnimplementedError extends KCLError { export class KCLUnimplementedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('unimplemented', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unimplemented', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype) Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
} }
} }
export class KCLUnexpectedError extends KCLError { export class KCLUnexpectedError extends KCLError {
constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('unexpected', msg, sourceRange, operations) msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unexpected', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype) Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
} }
} }
export class KCLValueAlreadyDefined extends KCLError { export class KCLValueAlreadyDefined extends KCLError {
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super( super(
'name', 'name',
`Key ${key} was already defined elsewhere`, `Key ${key} was already defined elsewhere`,
sourceRange, sourceRange,
operations operations,
artifactCommands
) )
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
} }
} }
export class KCLUndefinedValueError extends KCLError { export class KCLUndefinedValueError extends KCLError {
constructor(key: string, sourceRange: SourceRange, operations: Operation[]) { constructor(
super('name', `Key ${key} has not been defined`, sourceRange, operations) key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super(
'name',
`Key ${key} has not been defined`,
sourceRange,
operations,
artifactCommands
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
} }
} }
@ -113,6 +168,7 @@ export function lspDiagnosticsToKclErrors(
'unexpected', 'unexpected',
message, message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true], [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[],
[] []
) )
) )

View File

@ -481,6 +481,7 @@ const theExtrude = startSketchOn('XY')
'undefined_value', 'undefined_value',
'memory item key `myVarZ` is not defined', 'memory item key `myVarZ` is not defined',
[129, 135, true], [129, 135, true],
[],
[] []
) )
) )

View File

@ -67,6 +67,7 @@ export async function executeAst({
: executor(ast, engineCommandManager)) : executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -16,7 +16,6 @@ import {
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
removeSingleConstraintInfo, removeSingleConstraintInfo,
deleteFromSelection, deleteFromSelection,
splitPipedProfile,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
@ -807,9 +806,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({ sketch002 = startSketchOn({
plane = { plane = {
origin = { x = 1, y = 2, z = 3 }, origin = { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 }, xAxis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 }, yAxis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 } zAxis = { x = 10, y = 11, z = 12 }
} }
}) })
|> startProfileAt([-12.55, 2.89], %) |> startProfileAt([-12.55, 2.89], %)
@ -863,9 +862,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({ sketch002 = startSketchOn({
plane = { plane = {
origin = { x = 1, y = 2, z = 3 }, origin = { x = 1, y = 2, z = 3 },
x_axis = { x = 4, y = 5, z = 6 }, xAxis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 }, yAxis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 } zAxis = { x = 10, y = 11, z = 12 }
} }
}) })
|> startProfileAt([-12.55, 2.89], %) |> startProfileAt([-12.55, 2.89], %)
@ -919,63 +918,3 @@ sketch002 = startSketchOn({
} }
) )
}) })
describe('Testing splitPipedProfile', () => {
it('should split the pipe expression correctly', () => {
const codeBefore = `part001 = startSketchOn('XZ')
|> startProfileAt([1, 2], %)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const expectedCodeAfter = `sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const ast = assertParse(codeBefore)
const codeOfInterest = `startSketchOn('XZ')`
const range: [number, number, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
if (err(newCode)) throw newCode
expect(newCode.trim()).toBe(expectedCodeAfter.trim())
})
it('should return error for already split pipe', () => {
const codeBefore = `sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const ast = assertParse(codeBefore)
const codeOfInterest = `startProfileAt([1, 2], sketch001)`
const range: [number, number, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
expect(result instanceof Error).toBe(true)
})
})

View File

@ -29,8 +29,6 @@ import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse, traverse,
getBodyIndex,
isCallExprWithName,
} from './queryAst' } from './queryAst'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import { import {
@ -48,7 +46,6 @@ import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes' import { KclExpressionWithVariable } from 'lib/commandTypes'
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -81,54 +78,41 @@ export function startSketchOnDefault(
} }
} }
export function insertNewStartProfileAt( export function addStartProfileAt(
node: Node<Program>, node: Node<Program>,
sketchEntryNodePath: PathToNode, pathToNode: PathToNode,
sketchNodePaths: PathToNode[], at: [number, number]
planeNodePath: PathToNode, ): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
at: [number, number], const _node1 = getNodeFromPath<VariableDeclaration>(
insertType: 'start' | 'end' = 'end'
):
| {
modifiedAst: Node<Program>
updatedSketchNodePaths: PathToNode[]
updatedEntryNodePath: PathToNode
}
| Error {
const varDec = getNodeFromPath<VariableDeclarator>(
node, node,
planeNodePath, pathToNode,
'VariableDeclarator' 'VariableDeclaration'
) )
if (err(varDec)) return varDec if (err(_node1)) return _node1
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var') const variableDeclaration = _node1.node
if (variableDeclaration.type !== 'VariableDeclaration') {
const newExpression = createVariableDeclaration( return new Error('variableDeclaration.init.type !== PipeExpression')
findUniqueName(node, 'profile'), }
createCallExpressionStdLib('startProfileAt', [ const _node = { ...node }
createArrayExpression([ const init = variableDeclaration.declaration.init
createLiteral(roundOff(at[0])), const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createLiteral(roundOff(at[1])), createArrayExpression([
]), createLiteral(roundOff(at[0])),
createIdentifier(varDec.node.id.name), createLiteral(roundOff(at[1])),
]),
createPipeSubstitution(),
])
if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt)
} else {
variableDeclaration.declaration.init = createPipeExpression([
init,
startProfileAt,
]) ])
) }
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
const _node = structuredClone(node)
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
_node.body.splice(insertIndex, 0, newExpression)
const { updatedEntryNodePath, updatedSketchNodePaths } =
updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
})
return { return {
modifiedAst: _node, modifiedAst: _node,
updatedSketchNodePaths, pathToNode,
updatedEntryNodePath,
} }
} }
@ -269,7 +253,7 @@ export function mutateObjExpProp(
export function extrudeSketch( export function extrudeSketch(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, pathToNode: PathToNode,
artifact?: Artifact, shouldPipe = false,
distance: Expr = createLiteral(4) distance: Expr = createLiteral(4)
): ):
| { | {
@ -278,14 +262,10 @@ export function extrudeSketch(
pathToExtrudeArg: PathToNode pathToExtrudeArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const _node = structuredClone(node) const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode) const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1 if (err(_node1)) return _node1
const { node: sketchExpression } = _node1
// determine if sketchExpression is in a pipeExpression or not // determine if sketchExpression is in a pipeExpression or not
const _node2 = getNodeFromPath<PipeExpression>( const _node2 = getNodeFromPath<PipeExpression>(
@ -294,6 +274,9 @@ export function extrudeSketch(
'PipeExpression' 'PipeExpression'
) )
if (err(_node2)) return _node2 if (err(_node2)) return _node2
const { node: pipeExpression } = _node2
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
const _node3 = getNodeFromPath<VariableDeclarator>( const _node3 = getNodeFromPath<VariableDeclarator>(
_node, _node,
@ -301,23 +284,49 @@ export function extrudeSketch(
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(_node3)) return _node3 if (err(_node3)) return _node3
const { node: variableDeclarator } = _node3 const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
const extrudeCall = createCallExpressionStdLib('extrude', [ const extrudeCall = createCallExpressionStdLib('extrude', [
distance, distance,
createIdentifier(variableDeclarator.id.name), shouldPipe
? createPipeSubstitution()
: createIdentifier(variableDeclarator.id.name),
]) ])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...pipeExpression.body, extrudeCall]
: [sketchExpression as any, extrudeCall]
)
variableDeclarator.init = pipeChain
const pathToExtrudeArg: PathToNode = [
...pathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: _node,
pathToNode,
pathToExtrudeArg,
}
}
// We're not creating a pipe expression, // We're not creating a pipe expression,
// but rather a separate constant for the extrusion // but rather a separate constant for the extrusion
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
const VariableDeclaration = createVariableDeclaration(name, extrudeCall) const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const lastSketchNodePath = const sketchIndexInPathToNode =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1] pathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = pathToDecleration[
console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths) sketchIndexInPathToNode
const sketchIndexInBody = Number(lastSketchNodePath[1][0]) ][0] as number
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) _node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
@ -365,6 +374,37 @@ export function loftSketches(
} }
} }
export function addSweep(
node: Node<Program>,
profileDeclarator: VariableDeclarator,
pathDeclarator: VariableDeclarator
): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
createIdentifier(profileDeclarator.id.name),
])
const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
export function revolveSketch( export function revolveSketch(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, pathToNode: PathToNode,
@ -1140,11 +1180,17 @@ export async function deleteFromSelection(
((selection?.artifact?.type === 'wall' || ((selection?.artifact?.type === 'wall' ||
selection?.artifact?.type === 'cap') && selection?.artifact?.type === 'cap') &&
varDec.node.init.type === 'PipeExpression') || varDec.node.init.type === 'PipeExpression') ||
selection.artifact?.type === 'sweep' selection.artifact?.type === 'sweep' ||
selection.artifact?.type === 'plane' ||
!selection.artifact // aka expected to be a shell at this point
) { ) {
let extrudeNameToDelete = '' let extrudeNameToDelete = ''
let pathToNode: PathToNode | null = null let pathToNode: PathToNode | null = null
if (selection.artifact?.type !== 'sweep') { if (
selection.artifact &&
selection.artifact.type !== 'sweep' &&
selection.artifact.type !== 'plane'
) {
const varDecName = varDec.node.id.name const varDecName = varDec.node.id.name
traverse(astClone, { traverse(astClone, {
enter: (node, path) => { enter: (node, path) => {
@ -1160,6 +1206,17 @@ export async function deleteFromSelection(
pathToNode = path pathToNode = path
extrudeNameToDelete = dec.id.name extrudeNameToDelete = dec.id.name
} }
if (
dec.init.type === 'CallExpression' &&
dec.init.callee.name === 'loft' &&
dec.init.arguments?.[0].type === 'ArrayExpression' &&
dec.init.arguments?.[0].elements.some(
(a) => a.type === 'Identifier' && a.name === varDecName
)
) {
pathToNode = path
extrudeNameToDelete = dec.id.name
}
} }
}, },
}) })
@ -1269,17 +1326,17 @@ export async function deleteFromSelection(
y: roundLiteral(faceDetails.origin.y), y: roundLiteral(faceDetails.origin.y),
z: roundLiteral(faceDetails.origin.z), z: roundLiteral(faceDetails.origin.z),
}), }),
x_axis: createObjectExpression({ xAxis: createObjectExpression({
x: roundLiteral(faceDetails.x_axis.x), x: roundLiteral(faceDetails.x_axis.x),
y: roundLiteral(faceDetails.x_axis.y), y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z), z: roundLiteral(faceDetails.x_axis.z),
}), }),
y_axis: createObjectExpression({ yAxis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x), x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y), y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z), z: roundLiteral(faceDetails.y_axis.z),
}), }),
z_axis: createObjectExpression({ zAxis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x), x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y), y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z), z: roundLiteral(faceDetails.z_axis.z),
@ -1298,8 +1355,7 @@ export async function deleteFromSelection(
const pipeBody = varDec.node.init.body const pipeBody = varDec.node.init.body
if ( if (
pipeBody[0].type === 'CallExpression' && pipeBody[0].type === 'CallExpression' &&
(pipeBody[0].callee.name === 'startSketchOn' || pipeBody[0].callee.name === 'startSketchOn'
pipeBody[0].callee.name === 'startProfileAt')
) { ) {
// remove varDec // remove varDec
const varDecIndex = varDec.shallowPath[1][0] as number const varDecIndex = varDec.shallowPath[1][0] as number
@ -1314,149 +1370,3 @@ export async function deleteFromSelection(
const nonCodeMetaEmpty = () => { const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
} }
export function getInsertIndex(
sketchNodePaths: PathToNode[],
planeNodePath: PathToNode,
insertType: 'start' | 'end'
) {
let minIndex = 0
let maxIndex = 0
for (const path of sketchNodePaths) {
const index = Number(path[1][0])
if (index < minIndex) minIndex = index
if (index > maxIndex) maxIndex = index
}
const insertIndex = !sketchNodePaths.length
? Number(planeNodePath[1][0]) + 1
: insertType === 'start'
? minIndex
: maxIndex + 1
return insertIndex
}
export function updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
}: {
insertIndex: number
insertType: 'start' | 'end'
sketchNodePaths: PathToNode[]
}): {
updatedEntryNodePath: PathToNode
updatedSketchNodePaths: PathToNode[]
} {
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
const newExpressionPathToNode: PathToNode = [
['body', ''],
[insertIndex, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
let updatedSketchNodePaths = structuredClone(sketchNodePaths)
if (insertType === 'start') {
updatedSketchNodePaths = updatedSketchNodePaths.map((path) => {
path[1][0] = Number(path[1][0]) + 1
return path
})
updatedSketchNodePaths.unshift(newExpressionPathToNode)
} else {
updatedSketchNodePaths.push(newExpressionPathToNode)
}
return {
updatedSketchNodePaths,
updatedEntryNodePath: newExpressionPathToNode,
}
}
/**
*
* Split the following pipe expression into
* ```ts
* part001 = startSketchOn('XZ')
|> startProfileAt([1, 2], %)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
into
```ts
sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
```
Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before
making it safe for later code that uses part001 (the extrude in this example)
*
*/
export function splitPipedProfile(
ast: Program,
pathToPipe: PathToNode
):
| {
modifiedAst: Program
pathToProfile: PathToNode
pathToPlane: PathToNode
}
| Error {
const _ast = structuredClone(ast)
const varDec = getNodeFromPath<VariableDeclaration>(
_ast,
pathToPipe,
'VariableDeclaration'
)
if (err(varDec)) return varDec
if (
varDec.node.type !== 'VariableDeclaration' ||
varDec.node.declaration.init.type !== 'PipeExpression'
) {
return new Error('pathToNode does not point to pipe')
}
const init = varDec.node.declaration.init
const firstCall = init.body[0]
if (!isCallExprWithName(firstCall, 'startSketchOn'))
return new Error('First call is not startSketchOn')
const secondCall = init.body[1]
if (!isCallExprWithName(secondCall, 'startProfileAt'))
return new Error('Second call is not startProfileAt')
const varName = varDec.node.declaration.id.name
const newVarName = findUniqueName(_ast, 'sketch')
const secondCallArgs = structuredClone(secondCall.arguments)
secondCallArgs[1] = createIdentifier(newVarName)
const firstCallOfNewPipe = createCallExpression(
'startProfileAt',
secondCallArgs
)
const newSketch = createVariableDeclaration(
newVarName,
varDec.node.declaration.init.body[0]
)
const newProfile = createVariableDeclaration(
varName,
varDec.node.declaration.init.body.length <= 2
? firstCallOfNewPipe
: createPipeExpression([
firstCallOfNewPipe,
...varDec.node.declaration.init.body.slice(2),
])
)
const index = getBodyIndex(pathToPipe)
if (err(index)) return index
_ast.body.splice(index, 1, newSketch, newProfile)
const pathToPlane = structuredClone(pathToPipe)
const pathToProfile = structuredClone(pathToPipe)
pathToProfile[1][0] = index + 1
return {
modifiedAst: _ast,
pathToProfile,
pathToPlane,
}
}

View File

@ -275,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
const selection: Selections = { const selection: Selections = {
graphSelections: segmentRanges.map((segmentRange) => { graphSelections: segmentRanges.map((segmentRange) => {
const maybeArtifact = [...artifactGraph].find(([, a]) => { const maybeArtifact = [...artifactGraph].find(([, a]) => {
if (!('codeRef' in a && a.codeRef)) return false if (!('codeRef' in a)) return false
return isOverlap(a.codeRef.range, segmentRange) return isOverlap(a.codeRef.range, segmentRange)
}) })
return { return {

View File

@ -61,19 +61,18 @@ export interface FilletParameters {
export type EdgeTreatmentParameters = ChamferParameters | FilletParameters export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
// Apply Edge Treatment (Fillet or Chamfer) To Selection // Apply Edge Treatment (Fillet or Chamfer) To Selection
export function applyEdgeTreatmentToSelection( export async function applyEdgeTreatmentToSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selections, selection: Selections,
parameters: EdgeTreatmentParameters parameters: EdgeTreatmentParameters
): void | Error { ): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag // 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters) const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) return result if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast // 2. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
} }
export function modifyAstWithEdgeTreatmentAndTag( export function modifyAstWithEdgeTreatmentAndTag(
@ -291,7 +290,7 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus( async function updateAstAndFocus(
modifiedAst: Node<Program>, modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode> pathToEdgeTreatmentNode: Array<PathToNode>
) { ): Promise<void> {
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToEdgeTreatmentNode, focusPath: pathToEdgeTreatmentNode,
}) })

View File

@ -5,6 +5,7 @@ import {
PathToNode, PathToNode,
Expr, Expr,
CallExpression, CallExpression,
PipeExpression,
VariableDeclarator, VariableDeclarator,
} from 'lang/wasm' } from 'lang/wasm'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
@ -14,6 +15,7 @@ import {
createCallExpressionStdLib, createCallExpressionStdLib,
createObjectExpression, createObjectExpression,
createIdentifier, createIdentifier,
createPipeExpression,
findUniqueName, findUniqueName,
createVariableDeclaration, createVariableDeclaration,
} from 'lang/modifyAst' } from 'lang/modifyAst'
@ -22,13 +24,14 @@ import {
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
getEdgeTagCall, getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment' } from 'lang/modifyAst/addEdgeTreatment'
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
export function revolveSketch( export function revolveSketch(
ast: Node<Program>, ast: Node<Program>,
pathToSketchNode: PathToNode, pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4), angle: Expr = createLiteral(4),
axis: Selections, axisOrEdge: string,
artifact?: Artifact axis: string,
edge: Selections
): ):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -36,40 +39,51 @@ export function revolveSketch(
pathToRevolveArg: PathToNode pathToRevolveArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToSketchNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const clonedAst = structuredClone(ast) const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode if (err(sketchNode)) return sketchNode
// testing code let generatedAxis
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
const lineNode = getNodeFromPath<CallExpression>( if (axisOrEdge === 'Edge') {
clonedAst, const pathToAxisSelection = getNodePathFromSourceRange(
pathToAxisSelection, clonedAst,
'CallExpression' edge.graphSelections[0]?.codeRef.range
) )
if (err(lineNode)) return lineNode const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
// TODO Kevin: What if |> close(%)? const tagResult = mutateAstWithTagForSketchSegment(
// TODO Kevin: What if opposite edge clonedAst,
// TODO Kevin: What if the edge isn't planar to the sketch? pathToAxisSelection
// TODO Kevin: add a tag. )
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated // Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult if (err(tagResult)) return tagResult
const { tag } = tagResult const { tag } = tagResult
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
} else {
generatedAxis = createLiteral(axis)
}
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>( const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst, clonedAst,
@ -77,27 +91,52 @@ export function revolveSketch(
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact if (!generatedAxis) return new Error('Generated axis selection is missing.')
if (!axisSelection) return new Error('Axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [ const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({ createObjectExpression({
angle: angle, angle: angle,
axis: getEdgeTagCall(tag, axisSelection), axis: generatedAxis,
}), }),
createIdentifier(sketchVariableDeclarator.id.name), createIdentifier(sketchVariableDeclarator.id.name),
]) ])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression, // We're not creating a pipe expression,
// but rather a separate constant for the extrusion // but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall) const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const lastSketchNodePath = const sketchIndexInPathToNode =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1] sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = Number(lastSketchNodePath[1][0]) const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number') if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number') return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)

View File

@ -49,17 +49,27 @@ export function addShell({
return new Error("Couldn't find extrude") return new Error("Couldn't find extrude")
} }
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
// Get the sketch ref from the selection
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between. // TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
// We must find a technique for these situations that is robust to intermediate declarations // We must find a technique for these situations that is robust to intermediate declarations
const sketchNode = getNodeFromPath<VariableDeclarator>( const extrudeNode = getNodeFromPath<VariableDeclarator>(
modifiedAst, modifiedAst,
graphSelection.codeRef.pathToNode, extrudeLookupResult.pathToExtrudeNode,
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(sketchNode)) { const segmentNode = getNodeFromPath<VariableDeclarator>(
return sketchNode modifiedAst,
extrudeLookupResult.pathToSegmentNode,
'VariableDeclarator'
)
if (err(extrudeNode) || err(segmentNode)) {
return new Error("Couldn't find extrude")
}
if (extrudeNode.node.init.type === 'CallExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
} else if (segmentNode.node.init.type === 'PipeExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToSegmentNode
} else {
return new Error("Couldn't find extrude")
} }
const selectedArtifact = graphSelection.artifact const selectedArtifact = graphSelection.artifact

View File

@ -33,7 +33,6 @@ import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -598,13 +597,7 @@ export function findAllPreviousVariables(
type ReplacerFn = ( type ReplacerFn = (
_ast: Node<Program>, _ast: Node<Program>,
varName: string varName: string
) => ) => { modifiedAst: Node<Program>; pathToReplaced: PathToNode } | Error
| {
modifiedAst: Node<Program>
pathToReplaced: PathToNode
exprInsertIndex: number
}
| Error
export function isNodeSafeToReplacePath( export function isNodeSafeToReplacePath(
ast: Program, ast: Program,
@ -656,7 +649,7 @@ export function isNodeSafeToReplacePath(
if (err(_nodeToReplace)) return _nodeToReplace if (err(_nodeToReplace)) return _nodeToReplace
const nodeToReplace = _nodeToReplace.node as any const nodeToReplace = _nodeToReplace.node as any
nodeToReplace[last[0]] = identifier nodeToReplace[last[0]] = identifier
return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index } return { modifiedAst: _ast, pathToReplaced }
} }
const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution')
@ -775,15 +768,8 @@ export function isLinesParallelAndConstrained(
if (err(_primarySegment)) return _primarySegment if (err(_primarySegment)) return _primarySegment
const primarySegment = _primarySegment.segment const primarySegment = _primarySegment.segment
const _varDec2 = getNodeFromPath(ast, secondaryPath, 'VariableDeclaration')
if (err(_varDec2)) return _varDec2
const varDec2 = _varDec2.node
const varName2 = (varDec2 as VariableDeclaration)?.declaration.id?.name
const sg2 = sketchFromKclValue(programMemory?.get(varName2), varName2)
if (err(sg2)) return sg2
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sg2, sg,
secondaryLine?.codeRef?.range secondaryLine?.codeRef?.range
) )
if (err(_segment)) return _segment if (err(_segment)) return _segment
@ -1097,57 +1083,3 @@ export function getObjExprProperty(
if (index === -1) return null if (index === -1) return null
return { expr: node.properties[index].value, index } return { expr: node.properties[index].value, index }
} }
export function isCursorInFunctionDefinition(
ast: Node<Program>,
selectionRanges: Selection
): boolean {
if (!selectionRanges?.codeRef?.pathToNode) return false
const node = getNodeFromPath<FunctionExpression>(
ast,
selectionRanges.codeRef.pathToNode,
'FunctionExpression'
)
if (err(node)) return false
if (node.node.type === 'FunctionExpression') return true
return false
}
export function getBodyIndex(pathToNode: PathToNode): number | Error {
const index = Number(pathToNode[1][0])
if (Number.isInteger(index)) return index
return new Error('Expected number index')
}
export function isCallExprWithName(
expr: Expr | CallExpression,
name: string
): expr is CallExpression {
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
return expr.callee.name === name
}
return false
}
export function doesSketchPipeNeedSplitting(
ast: Node<Program>,
pathToPipe: PathToNode
): boolean | Error {
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
pathToPipe,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var')
const pipeExpression = varDec.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const [firstPipe, secondPipe] = pipeExpression.body
if (!firstPipe || !secondPipe) return false
if (
isCallExprWithName(firstPipe, 'startSketchOn') &&
isCallExprWithName(secondPipe, 'startProfileAt')
)
return true
return false
}

View File

@ -212,19 +212,6 @@ Map {
"type": "wall", "type": "wall",
}, },
"UUID-10" => { "UUID-10" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
312,
344,
true,
],
},
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"id": "UUID", "id": "UUID",
"pathIds": [ "pathIds": [

View File

@ -3,11 +3,11 @@ import {
assertParse, assertParse,
initPromise, initPromise,
Program, Program,
ArtifactCommand,
ExecState, ExecState,
} from 'lang/wasm' } from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { import {
OrderedCommand,
ResponseMap, ResponseMap,
createArtifactGraph, createArtifactGraph,
filterArtifacts, filterArtifacts,
@ -115,7 +115,7 @@ sketch002 = startSketchOn(offsetPlane001)
|> line([6.78, 15.01], %) |> line([6.78, 15.01], %)
` `
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests // add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests
const codeToWriteCacheFor = { const codeToWriteCacheFor = {
exampleCode1, exampleCode1,
sketchOnFaceOnFaceEtc, sketchOnFaceOnFaceEtc,
@ -127,7 +127,7 @@ type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = { type CacheShape = {
[key in CodeKey]: { [key in CodeKey]: {
orderedCommands: OrderedCommand[] artifactCommands: ArtifactCommand[]
responseMap: ResponseMap responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts'] execStateArtifacts: ExecState['artifacts']
} }
@ -159,7 +159,7 @@ beforeAll(async () => {
await kclManager.executeAst({ ast }) await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = { cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands, artifactCommands: kclManager.execState.artifactCommands,
responseMap: engineCommandManager.responseMap, responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts, execStateArtifacts: kclManager.execState.artifacts,
} }
@ -186,14 +186,14 @@ describe('testing createArtifactGraph', () => {
it('setup', () => { it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
orderedCommands, artifactCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts, execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes') } = getCommands('exampleCodeOffsetPlanes')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -237,14 +237,14 @@ describe('testing createArtifactGraph', () => {
it('setup', () => { it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
orderedCommands, artifactCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts, execStateArtifacts,
} = getCommands('exampleCode1') } = getCommands('exampleCode1')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -338,14 +338,14 @@ describe('testing createArtifactGraph', () => {
it(`setup`, () => { it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
orderedCommands, artifactCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts, execStateArtifacts,
} = getCommands('exampleCodeNo3D') } = getCommands('exampleCodeNo3D')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -409,14 +409,14 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
it('setup', async () => { it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
orderedCommands, artifactCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts, execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc') } = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -439,11 +439,11 @@ function getCommands(
const file = fs.readFileSync(fullPath, 'utf-8') const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file) const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in // these either already exist from the last run, or were created in
const orderedCommands = parsed[codeKey].orderedCommands const artifactCommands = parsed[codeKey].artifactCommands
const responseMap = parsed[codeKey].responseMap const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts const execStateArtifacts = parsed[codeKey].execStateArtifacts
return { return {
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -672,10 +672,10 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => { describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => { it('should return an array of artifacts to update', () => {
const { orderedCommands, responseMap, ast, execStateArtifacts } = const { artifactCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1') getCommands('exampleCode1')
const map = createArtifactGraph({ const map = createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -683,11 +683,14 @@ describe('testing getArtifactsToUpdate', () => {
const getArtifact = (id: string) => map.get(id) const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1' const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => { 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({ const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand: orderedCommands.find( artifactCommand,
(a) =>
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
)!,
responseMap, responseMap,
getArtifact, getArtifact,
currentPlaneId, currentPlaneId,
@ -820,10 +823,6 @@ describe('testing getArtifactsToUpdate', () => {
}, },
{ {
type: 'wall', type: 'wall',
codeRef: {
pathToNode: [['body', '']],
range: [312, 344, true],
},
id: expect.any(String), id: expect.any(String),
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],

View File

@ -1,8 +1,14 @@
import { ExecState, Expr, PathToNode, Program, SourceRange } from 'lang/wasm' import {
ArtifactCommand,
ExecState,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string export type ArtifactId = string
@ -36,14 +42,14 @@ export interface PathArtifact extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
interface Solid2DArtifact extends BaseArtifact { interface solid2D extends BaseArtifact {
type: 'solid2D' type: 'solid2D'
pathId: ArtifactId pathId: ArtifactId
} }
export interface PathArtifactRich extends BaseArtifact { export interface PathArtifactRich extends BaseArtifact {
type: 'path' type: 'path'
/** A path must always lie on a plane */ /** A path must always lie on a plane */
plane: PlaneArtifact | WallArtifact | CapArtifact plane: PlaneArtifact | WallArtifact
/** A path must always contain 0 or more segments */ /** A path must always contain 0 or more segments */
segments: Array<SegmentArtifact> segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */ /** A path may not result in a sweep artifact */
@ -63,7 +69,7 @@ interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf: WallArtifact surf: WallArtifact
edges: Array<SweepEdgeArtifact> edges: Array<SweepEdge>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CodeRef codeRef: CodeRef
} }
@ -71,7 +77,7 @@ interface SegmentArtifactRich extends BaseArtifact {
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/ /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact { interface SweepArtifact extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string pathId: string
surfaceIds: Array<string> surfaceIds: Array<string>
edgeIds: Array<string> edgeIds: Array<string>
@ -79,10 +85,10 @@ interface SweepArtifact extends BaseArtifact {
} }
interface SweepArtifactRich extends BaseArtifact { interface SweepArtifactRich extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdgeArtifact> edges: Array<SweepEdge>
codeRef: CodeRef codeRef: CodeRef
} }
@ -92,9 +98,6 @@ interface WallArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
} }
interface CapArtifact extends BaseArtifact { interface CapArtifact extends BaseArtifact {
type: 'cap' type: 'cap'
@ -102,12 +105,9 @@ interface CapArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
} }
interface SweepEdgeArtifact extends BaseArtifact { interface SweepEdge extends BaseArtifact {
type: 'sweepEdge' type: 'sweepEdge'
segId: ArtifactId segId: ArtifactId
sweepId: ArtifactId sweepId: ArtifactId
@ -137,10 +137,10 @@ export type Artifact =
| SweepArtifact | SweepArtifact
| WallArtifact | WallArtifact
| CapArtifact | CapArtifact
| SweepEdgeArtifact | SweepEdge
| EdgeCut | EdgeCut
| EdgeCutEdge | EdgeCutEdge
| Solid2DArtifact | solid2D
export type ArtifactGraph = Map<ArtifactId, Artifact> export type ArtifactGraph = Map<ArtifactId, Artifact>
@ -151,22 +151,18 @@ type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
export interface ResponseMap { export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData [commandId: string]: OkWebSocketResponseData
} }
export interface OrderedCommand {
command: EngineCommand
range: SourceRange
}
/** Creates a graph of artifacts from a list of ordered commands and their responses /** 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 * 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. * should return data on how to update the map, and not do so directly.
*/ */
export function createArtifactGraph({ export function createArtifactGraph({
orderedCommands, artifactCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
}: { }: {
orderedCommands: Array<OrderedCommand> artifactCommands: Array<ArtifactCommand>
responseMap: ResponseMap responseMap: ResponseMap
ast: Node<Program> ast: Node<Program>
execStateArtifacts: ExecState['artifacts'] execStateArtifacts: ExecState['artifacts']
@ -176,17 +172,15 @@ export function createArtifactGraph({
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */ /** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = '' let currentPlaneId = ''
orderedCommands.forEach((orderedCommand) => { for (const artifactCommand of artifactCommands) {
if (orderedCommand.command?.type === 'modeling_cmd_req') { if (artifactCommand.command.type === 'enable_sketch_mode') {
if (orderedCommand.command.cmd.type === 'enable_sketch_mode') { currentPlaneId = artifactCommand.command.entity_id
currentPlaneId = orderedCommand.command.cmd.entity_id }
} if (artifactCommand.command.type === 'sketch_mode_disable') {
if (orderedCommand.command.cmd.type === 'sketch_mode_disable') { currentPlaneId = ''
currentPlaneId = ''
}
} }
const artifactsToUpdate = getArtifactsToUpdate({ const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand, artifactCommand,
responseMap, responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id), getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId, currentPlaneId,
@ -197,7 +191,7 @@ export function createArtifactGraph({
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact) const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact) myMap.set(id, mergedArtifact)
}) })
}) }
return myMap return myMap
} }
@ -238,14 +232,14 @@ function mergeArtifacts(
* can remove this. * can remove this.
*/ */
export function getArtifactsToUpdate({ export function getArtifactsToUpdate({
orderedCommand: { command, range }, artifactCommand,
getArtifact, getArtifact,
responseMap, responseMap,
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts, execStateArtifacts,
}: { }: {
orderedCommand: OrderedCommand artifactCommand: ArtifactCommand
responseMap: ResponseMap responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */ /** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined getArtifact: (id: ArtifactId) => Artifact | undefined
@ -256,14 +250,12 @@ export function getArtifactsToUpdate({
id: ArtifactId id: ArtifactId
artifact: Artifact artifact: Artifact
}> { }> {
const range = sourceRangeFromRust(artifactCommand.range)
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
// expect all to be `modeling_cmd_req` as batch commands have const id = artifactCommand.cmdId
// already been expanded before being added to orderedCommands
if (command.type !== 'modeling_cmd_req') return []
const id = command.cmd_id
const response = responseMap[id] const response = responseMap[id]
const cmd = command.cmd const cmd = artifactCommand.command
const returnArr: ReturnType<typeof getArtifactsToUpdate> = [] const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) { if (cmd.type === 'make_plane' && range[1] !== 0) {
@ -297,22 +289,6 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId, sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds, pathIds: existingPlane.pathIds,
codeRef: existingPlane.codeRef,
},
},
]
} else if (existingPlane?.type === 'cap') {
return [
{
id: currentPlaneId,
artifact: {
type: 'cap',
subType: existingPlane.subType,
id: currentPlaneId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef: existingPlane.codeRef,
}, },
}, },
] ]
@ -357,18 +333,6 @@ export function getArtifactsToUpdate({
pathIds: [id], pathIds: [id],
}, },
}) })
} else if (plane?.type === 'cap') {
returnArr.push({
id: currentPlaneId,
artifact: {
type: 'cap',
id: currentPlaneId,
subType: plane.subType,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId,
pathIds: [id],
},
})
} }
return returnArr return returnArr
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') { } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
@ -413,7 +377,11 @@ export function getArtifactsToUpdate({
}) })
} }
return returnArr return returnArr
} else if (cmd.type === 'extrude' || cmd.type === 'revolve') { } else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({ returnArr.push({
id, id,
@ -434,6 +402,33 @@ export function getArtifactsToUpdate({
artifact: { ...path, sweepId: id }, artifact: { ...path, sweepId: id },
}) })
return returnArr 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 ( } else if (
cmd.type === 'solid3d_get_extrusion_face_info' && cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' && response?.type === 'modeling' &&
@ -448,33 +443,16 @@ export function getArtifactsToUpdate({
const path = getArtifact(seg.pathId) const path = getArtifact(seg.pathId)
if (path?.type === 'path' && seg?.type === 'segment') { if (path?.type === 'path' && seg?.type === 'segment') {
lastPath = path lastPath = path
const extraArtifact = Object.values(execStateArtifacts).find(
(a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
)
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const wallArtifact: Artifact = {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
}
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
wallArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({ returnArr.push({
id: face_id, id: face_id,
artifact: wallArtifact, artifact: {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
},
}) })
returnArr.push({ returnArr.push({
id: curve_id, id: curve_id,
@ -498,33 +476,16 @@ export function getArtifactsToUpdate({
if ((cap === 'top' || cap === 'bottom') && face_id) { if ((cap === 'top' || cap === 'bottom') && face_id) {
const path = lastPath const path = lastPath
if (path?.type === 'path') { if (path?.type === 'path') {
const extraArtifact = Object.values(execStateArtifacts).find(
(a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
)
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const capArtifact: Artifact = {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
}
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
capArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({ returnArr.push({
id: face_id, id: face_id,
artifact: capArtifact, artifact: {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
},
}) })
const sweep = getArtifact(path.sweepId) const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return if (sweep?.type !== 'sweep') return
@ -808,7 +769,7 @@ export function getCapCodeRef(
} }
export function getSolid2dCodeRef( export function getSolid2dCodeRef(
solid2D: Solid2DArtifact, solid2D: solid2D,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
@ -832,7 +793,7 @@ export function getWallCodeRef(
} }
export function getSweepEdgeCodeRef( export function getSweepEdgeCodeRef(
edge: SweepEdgeArtifact, edge: SweepEdge,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
@ -947,205 +908,6 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
} }
} }
function getPlaneFromPath(
path: PathArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall', 'cap'] },
graph
)
if (err(plane)) return plane
return plane
}
function getPlaneFromSegment(
segment: SegmentArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: segment.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSolid2D(
solid2D: Solid2DArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromCap(
cap: CapArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromWall(
wall: WallArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) {
const sweep = getArtifactOfTypes(
{ key: edge.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
export function getPlaneFromArtifact(
artifact: Artifact | undefined,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
if (!artifact) return new Error(`Artifact is undefined`)
if (artifact.type === 'plane') return artifact
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
if (artifact.type === 'solid2D') return getPlaneFromSolid2D(artifact, graph)
if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
if (artifact.type === 'sweepEdge')
return getPlaneFromSweepEdge(artifact, graph)
return new Error(`Artifact type ${artifact.type} does not have a plane`)
}
const isExprSafe = (index: number): boolean => {
const expr = kclManager.ast.body?.[index]
if (!expr) {
return false
}
if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') {
return false
}
if (expr.type === 'VariableDeclaration') {
const init = expr.declaration?.init
if (!init) return false
if (init.type === 'CallExpression') {
return false
}
if (init.type === 'BinaryExpression' && isNodeSafe(init)) {
return true
}
if (init.type === 'Literal' || init.type === 'MemberExpression') {
return true
}
}
return false
}
const onlyConsecutivePaths = (
orderedNodePaths: PathToNode[],
originalPath: PathToNode
): PathToNode[] => {
const originalIndex = Number(
orderedNodePaths.find(
(path) => path[1][0] === originalPath[1][0]
)?.[1]?.[0] || 0
)
const minIndex = Number(orderedNodePaths[0][1][0])
const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0])
const pathIndexMap: any = {}
orderedNodePaths.forEach((path) => {
const bodyIndex = Number(path[1][0])
pathIndexMap[bodyIndex] = path
})
const safePaths: PathToNode[] = []
// traverse expressions in either direction from the profile selected
// when the user entered sketch mode
for (let i = originalIndex; i <= maxIndex; i++) {
if (pathIndexMap[i]) {
safePaths.push(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
for (let i = originalIndex - 1; i >= minIndex; i--) {
if (pathIndexMap[i]) {
safePaths.unshift(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
return safePaths
}
export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) {
const nodePaths: PathToNode[] = []
for (const pathId of planeArtifact.pathIds) {
const path = engineCommandManager.artifactGraph.get(pathId)
if (!path) continue
if ('codeRef' in path && path.codeRef) {
// TODO should figure out why upstream the path is bad
const isNodePathBad = path.codeRef.pathToNode.length < 2
nodePaths.push(
isNodePathBad
? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range)
: path.codeRef.pathToNode
)
}
}
return onlyConsecutivePaths(nodePaths, nodePaths[0])
}
export function getPathsFromArtifact({
sketchPathToNode,
artifact,
}: {
sketchPathToNode: PathToNode
artifact?: Artifact
}): PathToNode[] | Error {
const plane = getPlaneFromArtifact(
artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return plane
const paths = getArtifactsOfTypes(
{ keys: plane.pathIds, types: ['path'] },
engineCommandManager.artifactGraph
)
let nodePaths = [...paths.values()]
.map((path) => path.codeRef.pathToNode)
.sort((a, b) => Number(a[1][0]) - Number(b[1][0]))
return onlyConsecutivePaths(nodePaths, sketchPathToNode)
}
function isNodeSafe(node: Expr): boolean {
if (node.type === 'Literal' || node.type === 'MemberExpression') {
return true
}
if (node.type === 'BinaryExpression') {
return isNodeSafe(node.left) && isNodeSafe(node.right)
}
return false
}
/** /**
* Get an artifact from a code source range * Get an artifact from a code source range
*/ */
@ -1154,7 +916,7 @@ export function getArtifactFromRange(
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): Artifact | null { ): Artifact | null {
for (const artifact of artifactGraph.values()) { for (const artifact of artifactGraph.values()) {
if ('codeRef' in artifact && artifact.codeRef) { if ('codeRef' in artifact) {
const match = const match =
artifact.codeRef?.range[0] === range[0] && artifact.codeRef?.range[0] === range[0] &&
artifact.codeRef.range[1] === range[1] artifact.codeRef.range[1] === range[1]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 568 KiB

View File

@ -1,11 +1,10 @@
import { import {
ArtifactCommand,
defaultRustSourceRange, defaultRustSourceRange,
defaultSourceRange,
ExecState, ExecState,
Program, Program,
RustSourceRange, RustSourceRange,
SourceRange, SourceRange,
sourceRangeFromRust,
} from 'lang/wasm' } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
@ -21,7 +20,6 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { import {
ArtifactGraph, ArtifactGraph,
EngineCommand, EngineCommand,
OrderedCommand,
ResponseMap, ResponseMap,
createArtifactGraph, createArtifactGraph,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
@ -1305,7 +1303,7 @@ export enum EngineCommandManagerEvents {
* *
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response. * As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
* *
* Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap} * Also all commands that are sent are kept track of in WASM artifactCommands and their responses are kept in {@link responseMap}
* Both of these data structures are used to process the {@link artifactGraph}. * Both of these data structures are used to process the {@link artifactGraph}.
*/ */
@ -1331,12 +1329,7 @@ export class EngineCommandManager extends EventTarget {
[commandId: string]: PendingMessage [commandId: string]: PendingMessage
} = {} } = {}
/** /**
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long * A map of the responses to the WASM artifactCommands, when processing the commands into the artifactGraph, this response map allow
* list of the individual commands, this is used to process all the commands into the artifactGraph
*/
orderedCommands: Array<OrderedCommand> = []
/**
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
* us to look up the response by command id * us to look up the response by command id
*/ */
responseMap: ResponseMap = {} responseMap: ResponseMap = {}
@ -1832,7 +1825,6 @@ export class EngineCommandManager extends EventTarget {
} }
} }
async startNewSession() { async startNewSession() {
this.orderedCommands = []
this.responseMap = {} this.responseMap = {}
await this.initPlanes() await this.initPlanes()
} }
@ -2075,28 +2067,6 @@ export class EngineCommandManager extends EventTarget {
isSceneCommand, isSceneCommand,
} }
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: sourceRangeFromRust(message.range),
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
cmd: req.cmd,
}
this.orderedCommands.push({
command: cmd,
range,
})
})
}
this.engineConnection?.send(message.command) this.engineConnection?.send(message.command)
return promise return promise
} }
@ -2119,10 +2089,11 @@ export class EngineCommandManager extends EventTarget {
} }
updateArtifactGraph( updateArtifactGraph(
ast: Node<Program>, ast: Node<Program>,
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts'] execStateArtifacts: ExecState['artifacts']
) { ) {
this.artifactGraph = createArtifactGraph({ this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands, artifactCommands,
responseMap: this.responseMap, responseMap: this.responseMap,
ast, ast,
execStateArtifacts, execStateArtifacts,
@ -2219,11 +2190,7 @@ export class EngineCommandManager extends EventTarget {
commandTypeToTarget: string commandTypeToTarget: string
): string | undefined { ): string | undefined {
for (const [artifactId, artifact] of this.artifactGraph) { for (const [artifactId, artifact] of this.artifactGraph) {
if ( if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
'codeRef' in artifact &&
artifact.codeRef &&
isOverlap(range, artifact.codeRef.range)
) {
if (commandTypeToTarget === artifact.type) return artifactId if (commandTypeToTarget === artifact.type) return artifactId
} }
} }

View File

@ -297,20 +297,14 @@ export const lineTo: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const to = segmentInput.to const to = segmentInput.to
const _node = structuredClone(node) const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>( const nodeMeta = getNodeFromPath<PipeExpression>(
_node, _node,
pathToNode, pathToNode,
'PipeExpression' 'PipeExpression'
) )
if (err(nodeMeta)) return nodeMeta if (err(nodeMeta)) return nodeMeta
const varDec = getNodeFromPath<VariableDeclaration>( const { node: pipe } = nodeMeta
_node,
pathToNode,
'VariableDeclaration'
)
if (err(varDec)) return varDec
const dec = varDec.node.declaration
const newVals: [Expr, Expr] = [ const newVals: [Expr, Expr] = [
createLiteral(roundOff(to[0], 2)), createLiteral(roundOff(to[0], 2)),
@ -339,20 +333,14 @@ export const lineTo: SketchLineHelper = {
]) ])
if (err(result)) return result if (err(result)) return result
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
if (dec.init.type === 'PipeExpression') { pipe.body[callIndex] = callExp
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
valueUsedInTransform: valueUsedInTransform, valueUsedInTransform: valueUsedInTransform,
} }
} else if (dec.init.type === 'PipeExpression') {
dec.init.body = [...dec.init.body, newLine]
} else { } else {
dec.init = createPipeExpression([dec.init, newLine]) pipe.body = [...pipe.body, newLine]
} }
return { return {
modifiedAst: _node, modifiedAst: _node,
@ -675,11 +663,11 @@ export const xLine: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput const { from, to } = segmentInput
const _node = structuredClone(node) const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode) const getNode = getNodeFromPathCurry(_node, pathToNode)
const varDec = getNode<VariableDeclaration>('VariableDeclaration') const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(varDec)) return varDec if (err(_node1)) return _node1
const dec = varDec.node.declaration const { node: pipe } = _node1
const newVal = createLiteral(roundOff(to[0] - from[0], 2)) const newVal = createLiteral(roundOff(to[0] - from[0], 2))
@ -694,11 +682,7 @@ export const xLine: SketchLineHelper = {
]) ])
if (err(result)) return result if (err(result)) return result
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
if (dec.init.type === 'PipeExpression') { pipe.body[callIndex] = callExp
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
@ -710,11 +694,7 @@ export const xLine: SketchLineHelper = {
newVal, newVal,
createPipeSubstitution(), createPipeSubstitution(),
]) ])
if (dec.init.type === 'PipeExpression') { pipe.body = [...pipe.body, newLine]
dec.init.body = [...dec.init.body, newLine]
} else {
dec.init = createPipeExpression([dec.init, newLine])
}
return { modifiedAst: _node, pathToNode } return { modifiedAst: _node, pathToNode }
}, },
updateArgs: ({ node, pathToNode, input }) => { updateArgs: ({ node, pathToNode, input }) => {
@ -751,11 +731,11 @@ export const yLine: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput const { from, to } = segmentInput
const _node = structuredClone(node) const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode) const getNode = getNodeFromPathCurry(_node, pathToNode)
const varDec = getNode<VariableDeclaration>('VariableDeclaration') const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(varDec)) return varDec if (err(_node1)) return _node1
const dec = varDec.node.declaration const { node: pipe } = _node1
const newVal = createLiteral(roundOff(to[1] - from[1], 2)) const newVal = createLiteral(roundOff(to[1] - from[1], 2))
if (replaceExistingCallback) { if (replaceExistingCallback) {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode) const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
@ -768,11 +748,7 @@ export const yLine: SketchLineHelper = {
]) ])
if (err(result)) return result if (err(result)) return result
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
if (dec.init.type === 'PipeExpression') { pipe.body[callIndex] = callExp
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
@ -784,11 +760,7 @@ export const yLine: SketchLineHelper = {
newVal, newVal,
createPipeSubstitution(), createPipeSubstitution(),
]) ])
if (dec.init.type === 'PipeExpression') { pipe.body = [...pipe.body, newLine]
dec.init.body = [...dec.init.body, newLine]
} else {
dec.init = createPipeExpression([dec.init, newLine])
}
return { modifiedAst: _node, pathToNode } return { modifiedAst: _node, pathToNode }
}, },
updateArgs: ({ node, pathToNode, input }) => { updateArgs: ({ node, pathToNode, input }) => {
@ -2173,6 +2145,8 @@ function addTagToChamfer(
if (err(variableDec)) return variableDec if (err(variableDec)) return variableDec
const isPipeExpression = pipeExpr.node.type === 'PipeExpression' const isPipeExpression = pipeExpr.node.type === 'PipeExpression'
console.log('pipeExpr', pipeExpr, variableDec)
// const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init
const callExpr = isPipeExpression const callExpr = isPipeExpression
? pipeExpr.node.body[pipeIndex] ? pipeExpr.node.body[pipeIndex]
: variableDec.node.init : variableDec.node.init
@ -2253,6 +2227,7 @@ function addTagToChamfer(
if (isPipeExpression) { if (isPipeExpression) {
pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert) pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
} else { } else {
console.log('yo', createPipeExpression([newExpressionToInsert, callExpr]))
callExpr.arguments[1] = createPipeSubstitution() callExpr.arguments[1] = createPipeSubstitution()
variableDec.node.init = createPipeExpression([ variableDec.node.init = createPipeExpression([
newExpressionToInsert, newExpressionToInsert,

View File

@ -9,47 +9,20 @@ import {
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
/** export function updatePathToNodeFromMap(
* Updates pathToNode body indices to account for the insertion of an expression oldPath: PathToNode,
* PathToNode expression is after the insertion index, that the body index is incremented pathToNodeMap: { [key: number]: PathToNode }
* Negative insertion index means no insertion
*/
export function updatePathToNodePostExprInjection(
pathToNode: PathToNode,
exprInsertIndex: number
): PathToNode { ): PathToNode {
if (exprInsertIndex < 0) return pathToNode const updatedPathToNode = structuredClone(oldPath)
const bodyIndex = Number(pathToNode[1][0]) let max = 0
if (bodyIndex < exprInsertIndex) return pathToNode Object.values(pathToNodeMap).forEach((path) => {
const clone = structuredClone(pathToNode) const index = Number(path[1][0])
clone[1][0] = bodyIndex + 1 if (index > max) {
return clone max = index
} }
})
export function updateSketchDetailsNodePaths({ updatedPathToNode[1][0] = max
sketchEntryNodePath, return updatedPathToNode
sketchNodePaths,
planeNodePath,
exprInsertIndex,
}: {
sketchEntryNodePath: PathToNode
sketchNodePaths: Array<PathToNode>
planeNodePath: PathToNode
exprInsertIndex: number
}) {
return {
updatedSketchEntryNodePath: updatePathToNodePostExprInjection(
sketchEntryNodePath,
exprInsertIndex
),
updatedSketchNodePaths: sketchNodePaths.map((path) =>
updatePathToNodePostExprInjection(path, exprInsertIndex)
),
updatedPlaneNodePath: updatePathToNodePostExprInjection(
planeNodePath,
exprInsertIndex
),
}
} }
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
@ -58,7 +31,7 @@ export function isCursorInSketchCommandRange(
): string | false { ): string | false {
const overlappingEntries = filterArtifacts( const overlappingEntries = filterArtifacts(
{ {
types: ['segment', 'path', 'plane'], types: ['segment', 'path'],
predicate: (artifact) => { predicate: (artifact) => {
return selectionRanges.graphSelections.some( return selectionRanges.graphSelections.some(
(selection) => (selection) =>

View File

@ -1,4 +1,5 @@
import init, { import {
init,
parse_wasm, parse_wasm,
recast_wasm, recast_wasm,
execute, execute,
@ -16,7 +17,9 @@ import init, {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
} from '../wasm-lib/pkg/wasm_lib' reloadModule,
} from 'lib/wasm_lib_wrapper'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -47,8 +50,10 @@ import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs' import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact' import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId' import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' 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 { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration' export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
@ -148,6 +153,7 @@ export const wasmUrl = () => {
// Initialise the wasm module. // Initialise the wasm module.
const initialise = async () => { const initialise = async () => {
try { try {
await reloadModule()
const fullUrl = wasmUrl() const fullUrl = wasmUrl()
const input = await fetch(fullUrl) const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer() const buffer = await input.arrayBuffer()
@ -227,6 +233,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
} }
@ -252,6 +259,7 @@ export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
operations: Operation[] operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact } artifacts: { [key in ArtifactId]?: Artifact }
artifactCommands: ArtifactCommand[]
} }
/** /**
@ -263,6 +271,7 @@ export function emptyExecState(): ExecState {
memory: ProgramMemory.empty(), memory: ProgramMemory.empty(),
operations: [], operations: [],
artifacts: {}, artifacts: {},
artifactCommands: [],
} }
} }
@ -271,6 +280,7 @@ function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
memory: ProgramMemory.fromRaw(execOutcome.memory), memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations, operations: execOutcome.operations,
artifacts: execOutcome.artifacts, artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
} }
} }
@ -512,6 +522,10 @@ export const executor = async (
if (programMemoryOverride !== null && err(programMemoryOverride)) if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride)
try { try {
let jsAppSettings = default_app_settings() let jsAppSettings = default_app_settings()
if (!TEST) { if (!TEST) {
@ -537,7 +551,8 @@ export const executor = async (
parsed.error.kind, parsed.error.kind,
parsed.error.msg, parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]), sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations parsed.operations,
parsed.artifactCommands
) )
return Promise.reject(kclError) return Promise.reject(kclError)
@ -597,6 +612,7 @@ export const modifyAstForSketch = async (
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
@ -666,6 +682,7 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
} }

View File

@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Sweep: {
path: Selections
profile: Selections
}
Loft: { Loft: {
selection: Selections selection: Selections
} }
@ -47,7 +51,9 @@ export type ModelingCommandSchema = {
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axis: Selections axisOrEdge: string
axis: string
edge: Selections
} }
Fillet: { Fillet: {
// todo // todo
@ -290,6 +296,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Sweep: {
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: true,
args: {
profile: {
inputType: 'selection',
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: {
inputType: 'selection',
selectionTypes: ['segment', 'path'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
},
},
},
Loft: { Loft: {
description: 'Create a 3D body by blending between two or more sketches', description: 'Create a 3D body by blending between two or more sketches',
icon: 'loft', icon: 'loft',
@ -324,10 +357,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve', icon: 'revolve',
status: 'development',
needsReview: true, needsReview: true,
args: { args: {
selection: { selection: {
@ -336,9 +369,34 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
},
axisOrEdge: {
inputType: 'options',
required: true,
defaultValue: 'Axis',
options: [
{ name: 'Axis', isCurrent: true, value: 'Axis' },
{ name: 'Edge', isCurrent: false, value: 'Edge' },
],
}, },
axis: { axis: {
required: true, required: (commandContext) =>
['Axis'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'options',
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
],
},
edge: {
required: (commandContext) =>
['Edge'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'selection', inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'], selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false, multiple: false,

View File

@ -68,7 +68,7 @@ export const revolveAxisValidator = async ({
} }
const sketchSelection = artifact.pathId const sketchSelection = artifact.pathId
let edgeSelection = data.axis.graphSelections[0].artifact?.id let edgeSelection = data.edge.graphSelections[0].artifact?.id
if (!sketchSelection) { if (!sketchSelection) {
return 'Unable to revolve, sketch is missing' return 'Unable to revolve, sketch is missing'
@ -101,7 +101,7 @@ export const revolveAxisValidator = async ({
return true return true
} else { } else {
// return error message for the toast // return error message for the toast
return 'Unable to revolve with selected axis' return 'Unable to revolve with selected edge'
} }
} }

View File

@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
LOFT: 'loft', LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell', SHELL: 'shell',
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve', REVOLVE: 'revolve',

View File

@ -15,6 +15,7 @@ import {
StateMachineCommandSetSchema, StateMachineCommandSetSchema,
} from './commandTypes' } from './commandTypes'
import { DEV } from 'env' import { DEV } from 'env'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
interface CreateMachineCommandProps< interface CreateMachineCommandProps<
T extends AnyStateMachine, T extends AnyStateMachine,
@ -84,7 +85,7 @@ export function createMachineCommand<
} else if ('status' in commandConfig) { } else if ('status' in commandConfig) {
const { status } = commandConfig const { status } = commandConfig
if (status === 'inactive') return null if (status === 'inactive') return null
if (status === 'development' && !DEV) return null if (status === 'development' && !(DEV || IS_NIGHTLY_OR_DEBUG)) return null
} }
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined const icon = ('icon' in commandConfig && commandConfig.icon) || undefined

51
src/lib/exceptions.ts Normal file
View File

@ -0,0 +1,51 @@
import { kclManager } from 'lib/singletons'
import { reloadModule, getModule } from 'lib/wasm_lib_wrapper'
import toast from 'react-hot-toast'
import { reportRejection } from './trap'
let initialized = false
/**
* WASM/Rust runtime can panic and the original try/catch/finally blocks will not trigger
* on the await promise. The interface will killed. This means we need to catch the error at
* the global/DOM level. This will have to interface with whatever controlflow that needs to be picked up
* within the error branch in the typescript to cover the application state.
*/
export const initializeWindowExceptionHandler = () => {
if (window && !initialized) {
window.addEventListener('error', (event) => {
void (async () => {
if (matchImportExportErrorCrash(event.message)) {
// do global singleton cleanup
kclManager.executeAstCleanUp()
toast.error(
'You have hit a KCL execution bug! Put your KCL code in a github issue to help us resolve this bug.'
)
try {
await reloadModule()
await getModule().default()
} catch (e) {
console.error('Failed to initialize wasm_lib')
console.error(e)
}
}
})().catch(reportRejection)
})
// Make sure we only initialize this event listener once
initialized = true
} else {
console.error(
`Failed to initialize, window: ${window}, initialized:${initialized}`
)
}
}
/**
* Specifically match a substring of the message error to detect an import export runtime issue
* when the WASM runtime panics
*/
const matchImportExportErrorCrash = (message: string): boolean => {
// called `Result::unwrap_throw()` on an `Err` value
const substringError = '`Result::unwrap_throw()` on an `Err` value'
return message.indexOf(substringError) !== -1 ? true : false
}

View File

@ -155,7 +155,7 @@ export interface components {
color?: string | null color?: string | null
/** @description The material that the filament is made of. */ /** @description The material that the filament is made of. */
material: components['schemas']['FilamentMaterial'] material: components['schemas']['FilamentMaterial']
/** @description The name of the filament, this is likely specfic to the manufacturer. */ /** @description The name of the filament, this is likely specific to the manufacturer. */
name?: string | null name?: string | null
} }
/** @description The material that the filament is made of. */ /** @description The material that the filament is made of. */

52
src/lib/markdown.ts Normal file
View File

@ -0,0 +1,52 @@
import { MarkedOptions, Renderer, unescape } from '@ts-stack/markdown'
import { openExternalBrowserIfDesktop } from './openWindow'
/**
* Main goal of this custom renderer is to prevent links from changing the current location
* this is specially important for the desktop app.
*/
export class SafeRenderer extends Renderer {
constructor(options: MarkedOptions) {
super(options)
// Attach a global function for non-react anchor elements that need safe navigation
window.openExternalLink = (e: React.MouseEvent<HTMLAnchorElement>) => {
openExternalBrowserIfDesktop()(e)
}
}
// Extended from https://github.com/ts-stack/markdown/blob/c5c1925c1153ca2fe9051c356ef0ddc60b3e1d6a/packages/markdown/src/renderer.ts#L116
link(href: string, title: string, text: string): string {
if (this.options.sanitize) {
let prot: string
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase()
} catch (e) {
return text
}
if (
// eslint-disable-next-line no-script-url
prot.indexOf('javascript:') === 0 ||
prot.indexOf('vbscript:') === 0 ||
prot.indexOf('data:') === 0
) {
return text
}
}
let out =
'<a onclick="openExternalLink(event)" target="_blank" href="' + href + '"'
if (title) {
out += ' title="' + title + '"'
}
out += '>' + text + '</a>'
return out
}
}

View File

@ -29,7 +29,7 @@ import {
*/ */
export const getRectangleCallExpressions = ( export const getRectangleCallExpressions = (
rectangleOrigin: [number, number], rectangleOrigin: [number, number],
tag: string tags: [string, string, string]
) => [ ) => [
createCallExpressionStdLib('angledLine', [ createCallExpressionStdLib('angledLine', [
createArrayExpression([ createArrayExpression([
@ -37,28 +37,30 @@ export const getRectangleCallExpressions = (
createLiteral(0), // This will be the width of the rectangle createLiteral(0), // This will be the width of the rectangle
]), ]),
createPipeSubstitution(), createPipeSubstitution(),
createTagDeclarator(tag), createTagDeclarator(tags[0]),
]), ]),
createCallExpressionStdLib('angledLine', [ createCallExpressionStdLib('angledLine', [
createArrayExpression([ createArrayExpression([
createBinaryExpression([ createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]), createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]),
'+', '+',
createLiteral(90), createLiteral(90),
]), // 90 offset from the previous line ]), // 90 offset from the previous line
createLiteral(0), // This will be the height of the rectangle createLiteral(0), // This will be the height of the rectangle
]), ]),
createPipeSubstitution(), createPipeSubstitution(),
createTagDeclarator(tags[1]),
]), ]),
createCallExpressionStdLib('angledLine', [ createCallExpressionStdLib('angledLine', [
createArrayExpression([ createArrayExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), // same angle as the first line
createUnaryExpression( createUnaryExpression(
createCallExpressionStdLib('segLen', [createIdentifier(tag)]), createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]),
'-' '-'
), // negative height ), // negative height
]), ]),
createPipeSubstitution(), createPipeSubstitution(),
createTagDeclarator(tags[2]),
]), ]),
createCallExpressionStdLib('lineTo', [ createCallExpressionStdLib('lineTo', [
createArrayExpression([ createArrayExpression([
@ -83,12 +85,12 @@ export function updateRectangleSketch(
y: number, y: number,
tag: string tag: string
) { ) {
;((pipeExpression.body[1] as CallExpression) ;((pipeExpression.body[2] as CallExpression)
.arguments[0] as ArrayExpression) = createArrayExpression([ .arguments[0] as ArrayExpression) = createArrayExpression([
createLiteral(x >= 0 ? 0 : 180), createLiteral(x >= 0 ? 0 : 180),
createLiteral(Math.abs(x)), createLiteral(Math.abs(x)),
]) ])
;((pipeExpression.body[2] as CallExpression) ;((pipeExpression.body[3] as CallExpression)
.arguments[0] as ArrayExpression) = createArrayExpression([ .arguments[0] as ArrayExpression) = createArrayExpression([
createBinaryExpression([ createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]), createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
@ -118,7 +120,7 @@ export function updateCenterRectangleSketch(
let startY = originY - Math.abs(deltaY) let startY = originY - Math.abs(deltaY)
// pipeExpression.body[1] is startProfileAt // pipeExpression.body[1] is startProfileAt
let callExpression = pipeExpression.body[0] let callExpression = pipeExpression.body[1]
if (isCallExpression(callExpression)) { if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0] const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) { if (isArrayExpression(arrayExpression)) {
@ -132,7 +134,7 @@ export function updateCenterRectangleSketch(
const twoX = deltaX * 2 const twoX = deltaX * 2
const twoY = deltaY * 2 const twoY = deltaY * 2
callExpression = pipeExpression.body[1] callExpression = pipeExpression.body[2]
if (isCallExpression(callExpression)) { if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0] const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) { if (isArrayExpression(arrayExpression)) {
@ -146,7 +148,7 @@ export function updateCenterRectangleSketch(
} }
} }
callExpression = pipeExpression.body[2] callExpression = pipeExpression.body[3]
if (isCallExpression(callExpression)) { if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0] const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) { if (isArrayExpression(arrayExpression)) {

View File

@ -278,19 +278,18 @@ export function getEventForSegmentSelection(
} }
if (!id || !group) return null if (!id || !group) return null
const artifact = engineCommandManager.artifactGraph.get(id) const artifact = engineCommandManager.artifactGraph.get(id)
if (!artifact) return null const codeRefs = getCodeRefsByArtifactId(
const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode) id,
if (err(node)) return null engineCommandManager.artifactGraph
)
if (!artifact || !codeRefs) return null
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
selectionType: 'singleCodeCursor', selectionType: 'singleCodeCursor',
selection: { selection: {
artifact, artifact,
codeRef: { codeRef: codeRefs[0],
pathToNode: group?.userData?.pathToNode,
range: [node.node.start, node.node.end, true],
},
}, },
}, },
} }
@ -569,7 +568,8 @@ export function getSelectionTypeDisplayText(
const selectionsByType = getSelectionCountByType(selection) const selectionsByType = getSelectionCountByType(selection)
if (selectionsByType === 'none') return null if (selectionsByType === 'none') return null
return [...selectionsByType.entries()] return selectionsByType
.entries()
.map( .map(
// Hack for showing "face" instead of "extrude-wall" in command bar text // Hack for showing "face" instead of "extrude-wall" in command bar text
([type, count]) => ([type, count]) =>
@ -578,6 +578,7 @@ export function getSelectionTypeDisplayText(
.replace('solid2D', 'face') .replace('solid2D', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}` .replace('segment', 'face')}${count > 1 ? 's' : ''}`
) )
.toArray()
.join(', ') .join(', ')
} }
@ -587,7 +588,7 @@ export function canSubmitSelectionArg(
) { ) {
return ( return (
selectionsByType !== 'none' && selectionsByType !== 'none' &&
[...selectionsByType.entries()].every(([type, count]) => { selectionsByType.entries().every(([type, count]) => {
const foundIndex = argument.selectionTypes.findIndex((s) => s === type) const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
return ( return (
foundIndex !== -1 && foundIndex !== -1 &&
@ -610,7 +611,7 @@ export function codeToIdSelections(
// TODO #868: loops over all artifacts will become inefficient at a large scale // TODO #868: loops over all artifacts will become inefficient at a large scale
const overlappingEntries = Array.from(engineCommandManager.artifactGraph) const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
.map(([id, artifact]) => { .map(([id, artifact]) => {
if (!('codeRef' in artifact && artifact.codeRef)) return null if (!('codeRef' in artifact)) return null
return isOverlap(artifact.codeRef.range, selection.range) return isOverlap(artifact.codeRef.range, selection.range)
? { ? {
artifact, artifact,
@ -861,6 +862,7 @@ export function updateSelections(
JSON.stringify(pathToNode) JSON.stringify(pathToNode)
) { ) {
artifact = a artifact = a
console.log('found artifact', a)
break break
} }
} }

View File

@ -2,10 +2,13 @@ import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env' import { DEV } from 'env'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { import {
canRectangleOrCircleTool,
isClosedSketch,
isEditingExistingSketch, isEditingExistingSketch,
modelingMachine, modelingMachine,
pipeHasCircle, pipeHasCircle,
} from 'machines/modelingMachine' } from 'machines/modelingMachine'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
import { EventFrom, StateFrom } from 'xstate' import { EventFrom, StateFrom } from 'xstate'
export type ToolbarModeName = 'modeling' | 'sketching' export type ToolbarModeName = 'modeling' | 'sketching'
@ -70,7 +73,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sketch', icon: 'sketch',
status: 'available', status: 'available',
title: ({ sketchPathId }) => title: ({ sketchPathId }) =>
sketchPathId ? 'Edit Sketch' : 'Start Sketch', `${sketchPathId ? 'Edit' : 'Start'} Sketch`,
showTitle: true, showTitle: true,
hotkey: 'S', hotkey: 'S',
description: 'Start drawing a 2D sketch', description: 'Start drawing a 2D sketch',
@ -101,7 +104,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Revolve', groupId: 'modeling' }, data: { name: 'Revolve', groupId: 'modeling' },
}), }),
icon: 'revolve', icon: 'revolve',
status: DEV ? 'available' : 'kcl-only', status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Revolve', title: 'Revolve',
hotkey: 'R', hotkey: 'R',
description: description:
@ -116,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
{ {
id: 'sweep', id: 'sweep',
onClick: () => console.error('Sweep not yet implemented'), onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Sweep', groupId: 'modeling' },
}),
icon: 'sweep', icon: 'sweep',
status: 'unavailable', status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Sweep', title: 'Sweep',
hotkey: 'W', hotkey: 'W',
description: description:
'Create a 3D body by moving a sketch region along an arbitrary path.', 'Create a 3D body by moving a sketch region along an arbitrary path.',
links: [ links: [
{ {
label: 'GitHub discussion', label: 'KCL docs',
url: 'https://github.com/KittyCAD/modeling-app/discussions/498', url: 'https://zoo.dev/docs/kcl/sweep',
}, },
], ],
}, },
@ -159,7 +166,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Fillet', groupId: 'modeling' }, data: { name: 'Fillet', groupId: 'modeling' },
}), }),
icon: 'fillet3d', icon: 'fillet3d',
status: DEV ? 'available' : 'kcl-only', status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Fillet', title: 'Fillet',
hotkey: 'F', hotkey: 'F',
description: 'Round the edges of a 3D solid.', description: 'Round the edges of a 3D solid.',
@ -330,14 +337,22 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
{ {
id: 'line', id: 'line',
onClick: ({ modelingState, modelingSend }) => { onClick: ({ modelingState, modelingSend }) => {
modelingSend({ if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
type: 'change tool', // Exit the sketch state if there are no points and they press ESC
data: { modelingSend({
tool: !modelingState.matches({ Sketch: 'Line tool' }) type: 'Cancel',
? 'line' })
: 'none', } else {
}, // Exit the tool if there are points and they press ESC
}) modelingSend({
type: 'change tool',
data: {
tool: !modelingState.matches({ Sketch: 'Line tool' })
? 'line'
: 'none',
},
})
}
}, },
icon: 'line', icon: 'line',
status: 'available', status: 'available',
@ -348,7 +363,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}) || }) ||
state.matches({ state.matches({
Sketch: { 'Circle tool': 'Awaiting Radius' }, Sketch: { 'Circle tool': 'Awaiting Radius' },
}), }) ||
isClosedSketch(state.context),
title: 'Line', title: 'Line',
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
@ -428,7 +444,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'circle', icon: 'circle',
status: 'available', status: 'available',
title: 'Center circle', title: 'Center circle',
disabled: (state) => state.matches('Sketch no face'), disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' })),
isActive: (state) => state.matches({ Sketch: 'Circle tool' }), isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
@ -476,7 +495,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'rectangle', icon: 'rectangle',
status: 'available', status: 'available',
disabled: (state) => state.matches('Sketch no face'), disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' })),
title: 'Corner rectangle', title: 'Corner rectangle',
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R', state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
@ -499,7 +521,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'arc', icon: 'arc',
status: 'available', status: 'available',
disabled: (state) => state.matches('Sketch no face'), disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Center Rectangle tool' })),
title: 'Center rectangle', title: 'Center rectangle',
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Center Rectangle tool' }) state.matches({ Sketch: 'Center Rectangle tool' })

View File

@ -97,7 +97,3 @@ export function trap<T>(
}) })
return true return true
} }
export function reject(errOrString: Error | string): Promise<never> {
return Promise.reject(errOrString)
}

View File

@ -153,7 +153,10 @@ export function toSync<F extends AsyncFn<F>>(
) => void | PromiseLike<void | null | undefined> | null | undefined ) => void | PromiseLike<void | null | undefined> | null | undefined
): (...args: Parameters<F>) => void { ): (...args: Parameters<F>) => void {
return (...args: Parameters<F>) => { return (...args: Parameters<F>) => {
fn(...args).catch(onReject) void fn(...args).catch((...args) => {
console.error(...args)
return onReject(...args)
})
} }
} }
@ -342,7 +345,7 @@ export function onDragNumberCalculation(text: string, e: MouseEvent) {
) )
const newVal = roundOff(addition, precision) const newVal = roundOff(addition, precision)
if (isNaN(newVal)) { if (Number.isNaN(newVal)) {
return return
} }

108
src/lib/wasm_lib_wrapper.ts Normal file
View File

@ -0,0 +1,108 @@
/**
* This wrapper file is to enable reloading of the wasm_lib.js file.
* When the wasm instance bricks there is no API or interface to restart,
* restore, or re init the WebAssembly instance. The entire application would need
* to restart.
* A way to bypass this is by reloading the entire .js file so the global wasm variable
* gets reinitialized and we do not use that old reference
*/
import {
parse_wasm as ParseWasm,
recast_wasm as RecastWasm,
execute as Execute,
kcl_lint as KclLint,
modify_ast_for_sketch_wasm as ModifyAstForSketch,
is_points_ccw as IsPointsCcw,
get_tangential_arc_to_info as GetTangentialArcToInfo,
program_memory_init as ProgramMemoryInit,
make_default_planes as MakeDefaultPlanes,
coredump as CoreDump,
toml_stringify as TomlStringify,
default_app_settings as DefaultAppSettings,
parse_app_settings as ParseAppSettings,
parse_project_settings as ParseProjectSettings,
default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache,
} from '../wasm-lib/pkg/wasm_lib'
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
// Stores the result of the import of the wasm_lib file
let data: ModuleType
// Imports the .js file again which will clear the old import
// This allows us to reinitialize the wasm instance
export async function reloadModule() {
data = await import(`../wasm-lib/pkg/wasm_lib`)
}
export function getModule(): ModuleType {
return data
}
export async function init(module_or_path: any) {
return await getModule().default(module_or_path)
}
export const parse_wasm: typeof ParseWasm = (...args) => {
return getModule().parse_wasm(...args)
}
export const recast_wasm: typeof RecastWasm = (...args) => {
return getModule().recast_wasm(...args)
}
export const execute: typeof Execute = (...args) => {
return getModule().execute(...args)
}
export const kcl_lint: typeof KclLint = (...args) => {
return getModule().kcl_lint(...args)
}
export const modify_ast_for_sketch_wasm: typeof ModifyAstForSketch = (
...args
) => {
return getModule().modify_ast_for_sketch_wasm(...args)
}
export const is_points_ccw: typeof IsPointsCcw = (...args) => {
return getModule().is_points_ccw(...args)
}
export const get_tangential_arc_to_info: typeof GetTangentialArcToInfo = (
...args
) => {
return getModule().get_tangential_arc_to_info(...args)
}
export const program_memory_init: typeof ProgramMemoryInit = (...args) => {
return getModule().program_memory_init(...args)
}
export const make_default_planes: typeof MakeDefaultPlanes = (...args) => {
return getModule().make_default_planes(...args)
}
export const coredump: typeof CoreDump = (...args) => {
return getModule().coredump(...args)
}
export const toml_stringify: typeof TomlStringify = (...args) => {
return getModule().toml_stringify(...args)
}
export const default_app_settings: typeof DefaultAppSettings = (...args) => {
return getModule().default_app_settings(...args)
}
export const parse_app_settings: typeof ParseAppSettings = (...args) => {
return getModule().parse_app_settings(...args)
}
export const parse_project_settings: typeof ParseProjectSettings = (
...args
) => {
return getModule().parse_project_settings(...args)
}
export const default_project_settings: typeof DefaultProjectSettings = (
...args
) => {
return getModule().default_project_settings(...args)
}
export const base64_decode: typeof Base64Decode = (...args) => {
return getModule().base64_decode(...args)
}
export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
...args
) => {
return getModule().clear_scene_and_bust_cache(...args)
}

File diff suppressed because it is too large Load Diff

View File

@ -41,13 +41,13 @@ export default function Export() {
export to almost any CAD software. export to almost any CAD software.
</p> </p>
<p className="my-4"> <p className="my-4">
Our teammate David is working on the file format, check out{' '} Our teammate Katie is working on the file format, check out{' '}
<a <a
href="https://www.youtube.com/watch?v=8SuW0qkYCZo" href="https://github.com/KhronosGroup/glTF/pull/2343"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
his talk with the Metaverse Standards Forum her standards proposal on GitHub
</a> </a>
! !
</p> </p>

View File

@ -32,6 +32,8 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1 export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0'
export function getReleaseUrl(version: string = APP_VERSION) { export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${ return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : '' IS_NIGHTLY ? 'nightly-' : ''

View File

@ -1819,9 +1819,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.2.79" version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9cab4476455be70ea57643c31444068b056d091bd348cab6044c0d8ad7fcc" checksum = "65e34a8eeb4fff5167666d1f2bc36c95d08ab3a0f736a02c8d33a8cde21cfd8d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1839,6 +1839,7 @@ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
"ts-rs",
"uuid", "uuid",
] ]
@ -1856,9 +1857,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds-macros-impl" name = "kittycad-modeling-cmds-macros-impl"
version = "0.1.12" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b" checksum = "fdb4ee23cc996aa2dca7584d410e8826e08161e1ac4335bb646d5ede33f37cb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

Some files were not shown because too many files have changed in this diff Show More