Compare commits

...

10 Commits

Author SHA1 Message Date
57b366b2d0 Unpin electron and electron-builder dependencies (#5230)
* Bump and unpin electron-builder and -updater version forward
Fixes #4505

* notarize: true

* Remove signingHashAlgorithms from win

* Fix signtoolOptions props after migration

* Disable branch build

* Bump more

* Add back CSC_FOR_PULL_REQUEST

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

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

* Another CSC_FOR_PULL_REQUEST

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

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

* Revert force prod changes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-17 11:03:02 -05:00
6f3f5dbda9 Multi-second blank screen on second instance of the app (#5377)
* Multi-second blank screen on second instance of the app
Fixes #5346

* Add !IS_PLAYWRIGHT

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-15 09:28:58 -05:00
8dd25715fb Fix: E2E playwright test is permantely broken on localhost runtime on ubuntu. (#5391)
* fix: e2e test passes now

* fix: trying to unflake this test

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-14 15:49:06 -05:00
834f7133d8 Allow multiple profiles in the same sketch (#5196)
* Revert "Revert multi-profile (#4812)"

This reverts commit efe8089b08.

* fix poor 1000ms wait UX

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

* trigger CI

* Add Rust side artifacts for startSketchOn face or plane (#4834)

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

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

* lint

* lint

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

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

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

* trigger CI

* chore: disabled file watcher which prevents faster file write (#4835)

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

* partial fixes

* 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

* Fix up all the tests

* Fix partial execution

* wip

* WIP

* wip

* rust changes to make three point confrom to same as others since we're not ready with name params yet

* most of the fix for 3 point circle

* get overlays working for circle three point

* fmt

* fix types

* cargo fmt

* add face codef ref for walls and caps

* fix sketch on face after updates to rust side artifact graph

* some things needed for multi-profile tests

* bad attempts at fixing rust

* more

* more

* fix rust

* more rust fixes

* overlay fix

* remove duplicate test

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

* lint and typing

* maybe fix a unit test

* small thing

* fix circ dep

* fix unit test

* fix some tests

* fix sweep point-and-click test

* fix more tests and add a fix me

* fix more tests

* fix electron specific test

* tsc

* more test tweaks

* update docs

* commint snaps?

* is clippy happy now?

* clippy again

* test works now without me changing anything big-fixed-itself

* small bug

* make three point have cross hair to make it consistent with othe rtools

* fix up state diagram

* fmt

* add draft point for first click of three point circ

* 1 test for three point circle

* 2 test for three point circle

* clean up

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

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

* remove bad doc comment

* remove test skip

* remove onboarding test changes

* Update src/lang/modifyAst.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Update output from simulation tests

* Fix to use correct source ranges

This also reduces cloning.

* Change back to skipping face cap none and both

* Update output after changing back to skipping none and both

* Fix clippy warning

* fix profile start snap bug

* add path ids to cap

* fix going into edit sketch

* make other startSketchOn's work

* fix snapshot test

* explain function name

* Update src/lib/rectangleTool.ts

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

* rename error

* remove file tree from diff

* Update src/clientSideScene/segments.ts

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

* nit

* Prevent double write to KCL code on revolve

* Update output after adding cap-to-path graph edge

* Fix edit/select sketch-on-cap via feature tree

* clean up for face codeRef

* fix changing tools part way through circle/rect tools

* fix delete of circle profile

* fix close profiles

* fix closing profile bug (tangentArcTo being ignored)

* remove stale comment

* Delete paths associated with sketch when the sketch plane is deleted

* Add support for deleting sketches on caps (not walls)

* get delet working for walls

* make delet of extrusions work for multi profile

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

* Delete the sketch statement too on the cap and wall cases

* Don't write to file in `split-sketch-pipe-if-needed` unless necessary

* Don't wait for file write to complete within `updateEditorWithAstAndWriteToFile`
It is already debounced internally. If we await it, we will have to wait for a debounced timeout

* docs

* fix circ dep

* tsc

* fix selection enter sketch weirdness

* test fixes

* comment out and fixme for delete related tests

* add skip wins

* try and get last test to pass

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-02-14 08:57:04 -05:00
8c5662e458 Add type to KclValue::Number (#5380)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-14 00:03:23 +00:00
f37fc357af KCL: Allow comments in CallExpressionKw (#5381)
Before, this would not parse:

```
line(
  end = [44.09, 306.95],
  // tag this for later
  tag = $hello
)
```

Now it does.
2025-02-13 23:18:54 +00:00
e27e9ecc63 test: Add SSI pattern simulation test (#5379)
* Add new SSI pattern test

* Update output since adding new test
2025-02-13 15:04:12 -06:00
78b42ea191 offsetPlane kwargs (#5367)
Previously: `offsetPlane('XY', 75)`
Now: `offsetPlane('XY', offset = 75)`

Pairs with this KCL-samples PR: https://github.com/KittyCAD/kcl-samples/pull/163
2025-02-13 14:37:02 -05:00
5d02a27122 CM KCL: add annotations (#5374)
* CM KCL: add annotations

* Make AnnotationName a token that includes the @

* The text of AnnotationName is now optional (#5324)

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
2025-02-13 19:28:19 +00:00
49d52ce94b Remove KclValue::Int (#5369)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-14 08:28:00 +13:00
190 changed files with 52552 additions and 5439 deletions

File diff suppressed because one or more lines are too long

View File

@ -40,7 +40,7 @@ squareSketch = startSketchOn('XY')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
triangleSketch = startSketchOn(offsetPlane('XY', 75)) triangleSketch = startSketchOn(offsetPlane('XY', offset = 75))
|> startProfileAt([0, 125], %) |> startProfileAt([0, 125], %)
|> line(end = [-15, -30]) |> line(end = [-15, -30])
|> line(end = [30, 0]) |> line(end = [30, 0])
@ -62,10 +62,10 @@ squareSketch = startSketchOn('XY')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch0 = startSketchOn(offsetPlane('XY', 75)) circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
circleSketch1 = startSketchOn(offsetPlane('XY', 150)) circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
|> circle({ center = [0, 100], radius = 20 }, %) |> circle({ center = [0, 100], radius = 20 }, %)
loft([ loft([
@ -87,10 +87,10 @@ squareSketch = startSketchOn('XY')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch0 = startSketchOn(offsetPlane('XY', 75)) circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
circleSketch1 = startSketchOn(offsetPlane('XY', 150)) circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
|> circle({ center = [0, 100], radius = 20 }, %) |> circle({ center = [0, 100], radius = 20 }, %)
loft( loft(

View File

@ -17,8 +17,8 @@ offsetPlane(std_plane: StandardPlane, offset: number) -> Plane
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `std_plane` | [`StandardPlane`](/docs/kcl/types/StandardPlane) | One of the standard planes. | Yes | | `std_plane` | [`StandardPlane`](/docs/kcl/types/StandardPlane) | Which standard plane (e.g. XY) should this new plane be created from? | Yes |
| `offset` | `number` | | Yes | | `offset` | `number` | Distance from the standard plane this new plane will be created at. | Yes |
### Returns ### Returns
@ -37,7 +37,7 @@ squareSketch = startSketchOn('XY')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch = startSketchOn(offsetPlane('XY', 150)) circleSketch = startSketchOn(offsetPlane('XY', offset = 150))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
loft([squareSketch, circleSketch]) loft([squareSketch, circleSketch])
@ -55,7 +55,7 @@ squareSketch = startSketchOn('XZ')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch = startSketchOn(offsetPlane('XZ', 150)) circleSketch = startSketchOn(offsetPlane('XZ', offset = 150))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
loft([squareSketch, circleSketch]) loft([squareSketch, circleSketch])
@ -73,7 +73,7 @@ squareSketch = startSketchOn('YZ')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch = startSketchOn(offsetPlane('YZ', 150)) circleSketch = startSketchOn(offsetPlane('YZ', offset = 150))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
loft([squareSketch, circleSketch]) loft([squareSketch, circleSketch])
@ -91,7 +91,7 @@ squareSketch = startSketchOn('-XZ')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
circleSketch = startSketchOn(offsetPlane('-XZ', -150)) circleSketch = startSketchOn(offsetPlane('-XZ', offset = -150))
|> circle({ center = [0, 100], radius = 50 }, %) |> circle({ center = [0, 100], radius = 50 }, %)
loft([squareSketch, circleSketch]) loft([squareSketch, circleSketch])
@ -106,7 +106,7 @@ startSketchOn("XY")
|> circle({ radius = 10, center = [0, 0] }, %) |> circle({ radius = 10, center = [0, 0] }, %)
// Triangle on the plane 4 units above // Triangle on the plane 4 units above
startSketchOn(offsetPlane("XY", 4)) startSketchOn(offsetPlane("XY", offset = 4))
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [0, 10]) |> line(end = [0, 10])

File diff suppressed because it is too large Load Diff

View File

@ -19,8 +19,8 @@ A face.
| `id` |`string`| The id of the face. | No | | `id` |`string`| The id of the face. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |`string`| The tag of the face. | No | | `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |

View File

@ -59,23 +59,7 @@ Any KCL value.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Number`| | No | | `type` |enum: `Number`| | No |
| `value` |`number`| | No | | `value` |`number`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `ty` |[`NumericType`](/docs/kcl/types/NumericType)| Any KCL value. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Int`| | No |
| `value` |`integer`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -0,0 +1,250 @@
---
title: "NumericType"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Count`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Length`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Angle`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Known`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Default`| | No |
| `len` |[`UnitLen`](/docs/kcl/types/UnitLen)| | No |
| `angle` |[`UnitAngle`](/docs/kcl/types/UnitAngle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Unknown`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Any`| | No |
----

View File

@ -98,6 +98,29 @@ a complete arc
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No | | `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----
A base path.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CircleThreePoint`| | No |
| `p1` |`[number, number]`| Point 1 of the circle | No |
| `p2` |`[number, number]`| Point 2 of the circle | No |
| `p3` |`[number, number]`| Point 3 of the circle | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
---- ----
A path that is horizontal. A path that is horizontal.

View File

@ -20,8 +20,8 @@ A plane.
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -29,8 +29,8 @@ A plane.
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A sketch type. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A sketch type. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -53,8 +53,8 @@ A face.
| `id` |`string`| The id of the face. | No | | `id` |`string`| The id of the face. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |`string`| The tag of the face. | No | | `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |

View File

@ -0,0 +1,47 @@
---
title: "UnitAngle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----

186
docs/kcl/types/UnitType.md Normal file
View File

@ -0,0 +1,186 @@
---
title: "UnitType"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Count`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Length`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Angle`| | No |
----

View File

@ -54,23 +54,26 @@ 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).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toContainText(
|> startProfileAt(${commonPoints.startAt}, %)`) `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
)
} }
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).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator)
|> startProfileAt(${commonPoints.startAt}, %) .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> 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).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator)
|> startProfileAt(${commonPoints.startAt}, %) .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
} else { } else {
@ -79,8 +82,10 @@ 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).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator)
|> startProfileAt(${commonPoints.startAt}, %) .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = 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}, %)`)
@ -137,8 +142,10 @@ async function doBasicSketch(
// Open the code pane. // Open the code pane.
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator)
|> startProfileAt(${commonPoints.startAt}, %) .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
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

@ -43,8 +43,7 @@ test.describe(
}, },
} }
const code = `sketch001 = startSketchOn('${plane}') const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel() await u.openDebugPanel()

View File

@ -24,7 +24,7 @@ sketch001 = startSketchOn('XZ')
revolve001 = revolve({ axis = "X" }, sketch001) revolve001 = revolve({ axis = "X" }, sketch001)
triangle() triangle()
|> extrude(length = 30) |> extrude(length = 30)
plane001 = offsetPlane('XY', 10) plane001 = offsetPlane('XY', offset = 10)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> startProfileAt([-20, 0], %) |> startProfileAt([-20, 0], %)
|> line(end = [5, -15]) |> line(end = [5, -15])
@ -35,7 +35,7 @@ sketch002 = startSketchOn(plane001)
extrude001 = extrude(sketch002, length = 10) extrude001 = extrude(sketch002, length = 10)
` `
const FEAUTRE_TREE_SKETCH_CODE = `sketch001 = startSketchOn('XZ') const FEATURE_TREE_SKETCH_CODE = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> angledLine([0, 4], %, $rectangleSegmentA001) |> angledLine([0, 4], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -54,7 +54,7 @@ sketch002 = startSketchOn(extrude001, rectangleSegmentB001)
center = [-1, 2], center = [-1, 2],
radius = .5 radius = .5
}, %) }, %)
plane001 = offsetPlane('XZ', -5) plane001 = offsetPlane('XZ', offset = -5)
sketch003 = startSketchOn(plane001) sketch003 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 5 }, %) |> circle({ center = [0, 0], radius = 5 }, %)
` `
@ -116,7 +116,7 @@ test.describe('Feature Tree pane', () => {
await testViewSource({ await testViewSource({
operationName: 'Offset Plane', operationName: 'Offset Plane',
operationIndex: 0, operationIndex: 0,
expectedActiveLine: "plane001 = offsetPlane('XY', 10)", expectedActiveLine: "plane001 = offsetPlane('XY', offset = 10)",
}) })
await testViewSource({ await testViewSource({
operationName: 'Extrude', operationName: 'Extrude',
@ -153,33 +153,16 @@ test.describe('Feature Tree pane', () => {
`User can edit sketch (but not on offset plane yet) from the feature tree`, `User can edit sketch (but not on offset plane yet) from the feature tree`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, homePage, scene, editor, toolbar, page }) => { async ({ context, homePage, scene, editor, toolbar, page }) => {
const unavailableToastMessage = page.getByText( await context.addInitScript((initialCode) => {
'Editing sketches on faces or offset planes through the feature tree is not yet supported' localStorage.setItem('persistCode', initialCode)
) }, FEATURE_TREE_SKETCH_CODE)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await context.folderSetupFn(async (dir) => { await test.step('force re-exe', async () => {
const bracketDir = join(dir, 'test-sample') await page.waitForTimeout(1000)
await fsp.mkdir(bracketDir, { recursive: true }) await editor.replaceCode('90', '91')
await fsp.writeFile( await page.waitForTimeout(1500)
join(bracketDir, 'main.kcl'),
FEAUTRE_TREE_SKETCH_CODE,
'utf-8'
)
})
await test.step('setup test', async () => {
await homePage.expectState({
projectCards: [
{
title: 'test-sample',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await homePage.openProject('test-sample')
await scene.waitForExecutionDone()
await toolbar.openFeatureTreePane()
}) })
await test.step('On a default plane should work', async () => { await test.step('On a default plane should work', async () => {
@ -199,24 +182,23 @@ test.describe('Feature Tree pane', () => {
await test.step('On an extrude face should *not* work', async () => { await test.step('On an extrude face should *not* work', async () => {
// Tooltip is getting in the way of clicking, so I'm first closing the pane // Tooltip is getting in the way of clicking, so I'm first closing the pane
await toolbar.closeFeatureTreePane() await toolbar.closeFeatureTreePane()
await page.waitForTimeout(1000)
await editor.replaceCode('91', '90')
await page.waitForTimeout(2000)
await (await toolbar.getFeatureTreeOperation('Sketch', 1)).dblclick() await (await toolbar.getFeatureTreeOperation('Sketch', 1)).dblclick()
await expect( await expect(
unavailableToastMessage, toolbar.exitSketchBtn,
'We should see a toast message about this' 'We should be in sketch mode now'
).toBeVisible() ).toBeVisible()
await unavailableToastMessage.waitFor({ state: 'detached' }) await editor.expectState({
// TODO - turn on once we update the artifactGraph in Rust highlightedCode: '',
// to include the proper source location for the extrude face diagnostics: [],
// await expect( activeLines: [
// toolbar.exitSketchBtn, 'sketch002=startSketchOn(extrude001,rectangleSegmentB001)',
// 'We should be in sketch mode now' ],
// ).toBeVisible() })
// await editor.expectState({ await toolbar.exitSketchBtn.click()
// highlightedCode: '',
// diagnostics: [],
// activeLines: ['|>circle({center=[-1,2],radius=.5},%)'],
// })
// await toolbar.exitSketchBtn.click()
}) })
await test.step('On an offset plane should *not* work', async () => { await test.step('On an offset plane should *not* work', async () => {
@ -226,7 +208,7 @@ test.describe('Feature Tree pane', () => {
await editor.expectState({ await editor.expectState({
highlightedCode: '', highlightedCode: '',
diagnostics: [], diagnostics: [],
activeLines: ['|>circle({center=[0,0],radius=5},%)'], activeLines: ['sketch003=startSketchOn(plane001)'],
}) })
await expect( await expect(
toolbar.exitSketchBtn, toolbar.exitSketchBtn,
@ -342,7 +324,8 @@ test.describe('Feature Tree pane', () => {
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
const testCode = (value: string) => `p = offsetPlane('XY', ${value})` const testCode = (value: string) =>
`p = offsetPlane('XY', offset = ${value})`
const initialInput = '10' const initialInput = '10'
const initialCode = testCode(initialInput) const initialCode = testCode(initialInput)
const newInput = '5 + 10' const newInput = '5 + 10'

View File

@ -1,6 +1,6 @@
import type { Page, Locator } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { uuidv4 } from 'lib/utils' import { isArray, uuidv4 } from 'lib/utils'
import { import {
closeDebugPanel, closeDebugPanel,
doAndWaitForImageDiff, doAndWaitForImageDiff,
@ -9,13 +9,15 @@ 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 }
} }
@ -26,12 +28,12 @@ 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 {
@ -77,17 +79,26 @@ 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 this.page.mouse.click(x, y) return clickParams?.shouldDbClick
? 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,
@ -97,7 +108,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,
@ -114,7 +125,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,
@ -131,7 +142,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,
@ -219,7 +230,7 @@ export class SceneFixture {
} }
expectPixelColor = async ( expectPixelColor = async (
colour: [number, number, number], colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) => { ) => {
@ -241,22 +252,36 @@ export class SceneFixture {
} }
} }
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return isArray(colour[0])
}
export async function expectPixelColor( export async function expectPixelColor(
page: Page, page: Page,
colour: [number, number, number], colour: [number, number, number] | [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(async () => { .poll(
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
if (!isColourArray(colour)) {
return pixel.every( return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff (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

@ -23,7 +23,10 @@ export class ToolbarFixture {
helixButton!: Locator helixButton!: 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
@ -53,7 +56,10 @@ export class ToolbarFixture {
this.helixButton = page.getByTestId('helix') this.helixButton = page.getByTestId('helix')
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"]')
@ -119,6 +125,25 @@ 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()
}
selectCircleThreePoint = async () => {
await this.page
.getByRole('button', { name: 'caret down Center circle:' })
.click()
await expect(
this.page.getByTestId('dropdown-circle-three-points')
).toBeVisible()
await this.page.getByTestId('dropdown-circle-three-points').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

@ -219,18 +219,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) 'startProfileAt([205.96, 254.59], sketch002)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
105.26 |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
], %, $rectangleSegmentB001) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -251,19 +246,15 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) 'startProfileAt([-209.64, 255.28], sketch003)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
segAng(rectangleSegmentA003) - 90, |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
106.84 |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
], %, $rectangleSegmentB002) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [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 },
@ -276,19 +267,14 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch004 = startSketchOn(extrude001, seg05)',
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) 'startProfileAt([82.57, 322.96], sketch004)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
segAng(rectangleSegmentA003) - 90, |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
106.84 |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
], %, $rectangleSegmentB002) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -301,25 +287,18 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch005 = startSketchOn(extrude001, seg06)', 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) 'startProfileAt([-23.43, 19.69], sketch005)',
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
segAng(rectangleSegmentA005) - 90, |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
84.07 |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
], %, $rectangleSegmentB004) |>close()`,
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> line(endAbsolute = [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] |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -332,7 +311,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
], %, $yo) ], %, $yo)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02) |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close() |> close()
extrude001 = extrude(sketch001, length = 100) extrude001 = extrude(sketch001, length = 100)
|> chamfer({ |> chamfer({
length = 30, length = 30,
tags = [getOppositeEdge(seg01)] tags = [getOppositeEdge(seg01)]
@ -346,59 +325,60 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
length = 30, length = 30,
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %, $seg06) }, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06) sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43,19.69], %) profile004 = startProfileAt([-23.43, 19.69], sketch005)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
84.07 84.07
], %, $rectangleSegmentB004) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005), segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005) -segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch004 = startSketchOn(extrude001, seg05) sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57,322.96], %) profile003 = startProfileAt([82.57, 322.96], sketch004)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004) - 90, segAng(rectangleSegmentA004) - 90,
103.07 103.07
], %, $rectangleSegmentB003) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004), segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004) -segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch003 = startSketchOn(extrude001, seg04) sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64,255.28], %) profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
106.84 106.84
], %, $rectangleSegmentB002) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003), segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003) -segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96,254.59], %) profile001 = startProfileAt([205.96, 254.59], sketch002)
|> 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) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
`,
`,
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) })
@ -443,18 +423,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) 'startProfileAt([205.96, 254.59], sketch002)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
105.26 |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
], %, $rectangleSegmentB001) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
@ -484,17 +459,17 @@ chamf = chamfer({
] ]
}, %) }, %)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %) profile001 = startProfileAt([205.96, 254.59], sketch002)
|> 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) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute=[profileStartX(%), profileStartY(%)])
|> close() |> close()
`, `,
{ shouldNormalise: true } { shouldNormalise: true }
@ -561,10 +536,10 @@ sketch002 = startSketchOn(extrude001, seg03)
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
} }
await test.step(`Start a sketch on the XZ plane`, async () => { await test.step(`Start a sketch on the XZ plane`, async () => {
@ -605,6 +580,7 @@ sketch002 = startSketchOn(extrude001, seg03)
expectedCodeSnippets.afterSegmentDraggedOnYAxis expectedCodeSnippets.afterSegmentDraggedOnYAxis
) )
}) })
await editor.page.waitForTimeout(1000)
}) })
test(`Verify user can double-click to edit a sketch`, async ({ test(`Verify user can double-click to edit a sketch`, async ({
@ -1052,7 +1028,7 @@ openSketch = startSketchOn('XY')
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 } const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)` const expectedOutput = `plane001 = offsetPlane('XZ', offset = 5)`
await homePage.goToModelingScene() await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue. // FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
@ -1188,7 +1164,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
` `
@ -1274,7 +1250,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002]) loft001 = loft([sketch001, sketch002])
@ -1321,7 +1297,7 @@ loft001 = loft([sketch001, sketch002])
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await clickOnSketch2() await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(` await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
`) `)
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Check for sketch 1 // Check for sketch 1
@ -1397,12 +1373,12 @@ sketch002 = startSketchOn('XZ')
await clickOnSketch2() await clickOnSketch2()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await toolbar.openPane('code')
await page.waitForTimeout(500) await page.waitForTimeout(500)
}) })
await test.step(`Confirm code is added to the editor, scene has changed`, async () => { await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15) await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration) await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({ await editor.expectState({
diagnostics: [], diagnostics: [],
@ -2472,19 +2448,18 @@ extrude002 = extrude(sketch002, length = 50)
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone() await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 } const testPoint = { x: 580, y: 320 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002' const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)` const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code') await scene.expectPixelColor([113, 113, 113], testPoint, 15)
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 test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {

File diff suppressed because it is too large Load Diff

View File

@ -444,8 +444,7 @@ 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 += ` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|> 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)
@ -456,7 +455,9 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) const lineEndClick = () =>
page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await lineEndClick()
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
@ -467,6 +468,15 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click on the end of the profile to continue it
await page.waitForTimeout(300)
await lineEndClick()
await page.waitForTimeout(100)
// 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)
@ -589,8 +599,7 @@ 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') `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
) )
} }
) )
@ -634,8 +643,7 @@ 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 += ` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|> 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)
@ -653,6 +661,10 @@ 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 += `
@ -739,8 +751,7 @@ 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 += ` code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
|> 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)
@ -758,6 +769,10 @@ 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.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,6 +1,7 @@
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( test(
@ -111,17 +112,16 @@ 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')) await expect(page.locator('.cm-content')).toHaveText(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|> 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') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up // Expect the network to be up
@ -169,7 +169,9 @@ 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.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click() await page
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
.click()
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
@ -183,11 +185,36 @@ 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')
|> startProfileAt([12.34, -12.34], %) profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line(end = [-12.34, 12.34]) |> line(end = [-12.34, 12.34])
@ -197,7 +224,7 @@ test.describe('Test network and connection issues', () => {
await expect.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line(end = [-12.34, 12.34]) |> line(end = [-12.34, 12.34])
|> xLine(-12.34, %) |> xLine(-12.34, %)

View File

@ -19,7 +19,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> xLine(-20, %) |> xLine(-20, %)
` `
) )
}) })
@ -673,7 +673,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
}, },
] as const ] as const
for (const { testName, addVariable, value, constraint } of cases) { for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ context, homePage, page }) => { test(`${testName}`, async ({ context, homePage, page, editor }) => {
// constants and locators // constants and locators
const cmdBarKclInput = page const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value') .getByTestId('cmd-bar-arg-value')
@ -706,7 +706,9 @@ part002 = startSketchOn('XZ')
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await u.waitForPageLoad()
await editor.scrollToText('line(end = [74.36, 130.4])', true)
await page.getByText('line(end = [74.36, 130.4])').click() await page.getByText('line(end = [74.36, 130.4])').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()

View File

@ -66,30 +66,31 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
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')) await expect(page.locator('.cm-content')).toHaveText(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|> 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') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> 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') .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}, %)`)
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') .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}, %)`)
@ -264,40 +265,40 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [170.36, -121.61], tag = $seg01) |> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude001 = extrude(sketch001, length = 50) extrude001 = extrude(sketch001, length = 50)
sketch005 = startSketchOn(extrude001, 'END') sketch005 = startSketchOn(extrude001, 'END')
|> startProfileAt([23.24, 136.52], %) |> startProfileAt([23.24, 136.52], %)
|> line(end = [-8.44, 36.61]) |> line(end = [-8.44, 36.61])
|> line(end = [49.4, 2.05]) |> line(end = [49.4, 2.05])
|> line(end = [29.69, -46.95]) |> line(end = [29.69, -46.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch003 = startSketchOn(extrude001, seg01) sketch003 = startSketchOn(extrude001, seg01)
|> startProfileAt([21.23, 17.81], %) |> startProfileAt([21.23, 17.81], %)
|> line(end = [51.97, 21.32]) |> line(end = [51.97, 21.32])
|> line(end = [4.07, -22.75]) |> line(end = [4.07, -22.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch002 = startSketchOn(extrude001, seg02) sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-100.54, 16.99], %) |> startProfileAt([-100.54, 16.99], %)
|> line(end = [0, 20.03]) |> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03) |> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude002 = extrude(sketch002, length = 50) extrude002 = extrude(sketch002, length = 50)
sketch004 = startSketchOn(extrude002, seg03) sketch004 = startSketchOn(extrude002, seg03)
|> startProfileAt([57.07, 134.77], %) |> startProfileAt([57.07, 134.77], %)
|> line(end = [-4.72, 22.84]) |> line(end = [-4.72, 22.84])
|> line(end = [28.8, 6.71]) |> line(end = [28.8, 6.71])
|> line(end = [9.19, -25.33]) |> line(end = [9.19, -25.33])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude003 = extrude(sketch004, length = 20) extrude003 = extrude(sketch004, length = 20)
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(end = [thickness, 0]) |> line(end = [thickness, 0])
|> line(end = [0, -1]) |> line(end = [0, -1])
@ -317,8 +318,30 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [0, pipeLength]) |> line(end = [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)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(end = [26.95, 24.21])
|> line(end = [20.91, -28.61])
|> line(end = [32.46, 18.71])
`
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -347,9 +370,10 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const revolve = { x: 646, y: 248 } const revolve = { x: 635, y: 253 }
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)
@ -366,43 +390,47 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
`rev = revolve({ axis: 'y' }, part009)` `rev = revolve({ axis: 'y' }, part009)`
) )
// DELETE PARENT EXTRUDE // FIXME (commented section below), this test would select a wall that had a sketch on it, and delete the underlying extrude
await page.mouse.click(parentExtrude.x, parentExtrude.y) // and replace the sketch on face with a hard coded custom plane, but since there was a sketch on that plane maybe it
await page.waitForTimeout(100) // should have delete the sketch? it's broken atm, but not sure if worth fixing since desired behaviour is a little
await expect(page.locator('.cm-activeLine')).toHaveText( // vague
'|> line(end = [170.36, -121.61], tag = $seg01)' // // DELETE PARENT EXTRUDE
) // await page.mouse.click(parentExtrude.x, parentExtrude.y)
await u.clearCommandLogs() // await page.waitForTimeout(100)
await page.keyboard.press('Backspace') // await expect(page.locator('.cm-activeLine')).toHaveText(
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) // '|> line(end = [170.36, -121.61], tag = $seg01)'
await page.waitForTimeout(200) // )
await expect(u.codeLocator).not.toContainText( // await u.clearCommandLogs()
`extrude001 = extrude(sketch001, length = 50)` // await page.keyboard.press('Backspace')
) // await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ // await page.waitForTimeout(200)
plane = { // await expect(u.codeLocator).not.toContainText(
origin = { x = 0, y = -50, z = 0 }, // `extrude001 = extrude(sketch001, length = 50)`
xAxis = { x = 1, y = 0, z = 0 }, // )
yAxis = { x = 0, y = 0, z = 1 }, // await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
zAxis = { x = 0, y = -1, z = 0 } // plane = {
} // origin = { x = 0, y = -50, z = 0 },
})`) // xAxis = { x = 1, y = 0, z = 0 },
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ // yAxis = { x = 0, y = 0, z = 1 },
plane = { // zAxis = { x = 0, y = -1, z = 0 }
origin = { x = 116.53, y = 0, z = 163.25 }, // }
xAxis = { x = -0.81, y = 0, z = 0.58 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
zAxis = { x = 0.58, y = 0, z = 0.81 } // plane = {
} // origin = { x = 116.53, y = 0, z = 163.25 },
})`) // xAxis = { x = -0.81, y = 0, z = 0.58 },
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ // yAxis = { x = 0, y = -1, z = 0 },
plane = { // zAxis = { x = 0.58, y = 0, z = 0.81 }
origin = { x = -91.74, y = 0, z = 80.89 }, // }
xAxis = { x = -0.66, y = 0, z = -0.75 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
zAxis = { x = -0.75, y = 0, z = 0.66 } // plane = {
} // origin = { x = -91.74, y = 0, z = 80.89 },
})`) // xAxis = { x = -0.66, y = 0, z = -0.75 },
// yAxis = { x = 0, y = -1, z = 0 },
// zAxis = { x = -0.75, y = 0, z = 0.66 }
// }
// })`)
// DELETE SOLID 2D // DELETE SOLID 2D
await page.mouse.click(solid2d.x, solid2d.y) await page.mouse.click(solid2d.x, solid2d.y)
@ -415,11 +443,24 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
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(end = [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.fixme(
page, "Deleting solid that the AST mod can't handle results in a toast message",
homePage, async ({ page, homePage }) => {
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -480,7 +521,8 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await expect(page.getByText('Unable to delete selection')).toBeVisible() await expect(page.getByText('Unable to delete selection')).toBeVisible()
}) }
)
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({ test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
page, page,
homePage, homePage,
@ -1216,12 +1258,15 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
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(650, 200) await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(600) await page.waitForTimeout(600)
// Code before exiting the tool // Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = (
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()
@ -1233,14 +1278,23 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.mouse.click(750, 200) await page.mouse.click(750, 200)
await page.waitForTimeout(100) await page.waitForTimeout(100)
// expect no change await expect
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) .poll(async () => {
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

@ -209,8 +209,13 @@ 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 })
await page.mouse.click(800, 250) const secondMousePosition = { x: 800, y: 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.
@ -219,11 +224,23 @@ 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 expect
.poll(async () => {
await page.keyboard.press('l') await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') return lineButton.getAttribute('aria-pressed')
})
.toBe('true')
// Do not close the sketch. // Do not close the sketch.
// On close it will exit sketch mode. // On close it will exit sketch mode.
@ -519,9 +536,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect.poll(u.normalisedEditorCode).toContain( await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %) profile001 = startProfileAt([-12.34, 12.34], sketch002)
|> line(end = [2.45, -0.2]) |> line(end = [12.34, -12.34])
|> line(end = [-2.6, -1.25]) |> line(end = [-12.34, -12.34])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
`) `)
@ -537,9 +554,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
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(400) await page.waitForTimeout(500)
await page.waitForTimeout(150) await page.setViewportSize({ width: 1200, height: 1200 })
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()

View File

@ -16,8 +16,7 @@ mac:
arch: arch:
- x64 - x64
- arm64 - arm64
notarize: notarize: true
teamId: 92H8YB3B95
fileAssociations: fileAssociations:
- ext: kcl - ext: kcl
name: kcl name: kcl
@ -32,9 +31,10 @@ win:
arch: arch:
- x64 - x64
- arm64 - arm64
signtoolOptions:
sign: "./scripts/sign-win.js"
signingHashAlgorithms: signingHashAlgorithms:
- sha256 - sha256
sign: "./scripts/sign-win.js"
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico" icon: "assets/icon.ico"
fileAssociations: fileAssociations:

View File

@ -40,7 +40,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "6.3.0", "electron-updater": "^6.5.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-pattern-transform2/manifest.json", "fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/offset-plane-kwargs/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && 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", "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",
@ -145,10 +145,11 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "7.4.0", "@electron-forge/cli": "^7.6.1",
"@electron-forge/plugin-fuses": "7.4.0", "@electron-forge/plugin-fuses": "^7.6.1",
"@electron-forge/plugin-vite": "7.4.0", "@electron-forge/plugin-vite": "^7.6.1",
"@electron/fuses": "1.8.0", "@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@nabla/vite-plugin-eslint": "^2.0.5", "@nabla/vite-plugin-eslint": "^2.0.5",
@ -175,9 +176,8 @@
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"electron": "32.1.2", "electron": "^34.1.1",
"electron-builder": "24.13.3", "electron-builder": "^26.0.6",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.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",

View File

@ -1,4 +1,5 @@
@precedence { @precedence {
annotation
member member
call call
exp @left exp @left
@ -20,9 +21,12 @@ statement[@isGroup=Statement] {
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } | FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression } |
Annotation { AnnotationName AnnotationList? }
} }
AnnotationList { !annotation "(" commaSep<AnnotationProperty> ")" }
ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")" } ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")" }
Body { "{" statement* "}" } Body { "{" statement* "}" }
@ -59,6 +63,12 @@ UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName (":" | Equals) expression } ObjectProperty { PropertyName (":" | Equals) expression }
AnnotationProperty {
PropertyName
( AddOp | MultOp | ExpOp | LogicOp | BangOp | CompOp | Equals | Arrow | PipeOperator | PipeSubstitution )
expression
}
LabeledArgument { ArgumentLabel Equals expression } LabeledArgument { ArgumentLabel Equals expression }
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" } ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
@ -105,6 +115,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
PipeSubstitution { "%" } PipeSubstitution { "%" }
identifier { (@asciiLetter | "_") (@asciiLetter | @digit | "_")* } identifier { (@asciiLetter | "_") (@asciiLetter | @digit | "_")* }
AnnotationName { "@" identifier? }
PropertyName { identifier } PropertyName { identifier }
TagDeclarator { "$" identifier } TagDeclarator { "$" identifier }

View File

@ -0,0 +1,153 @@
# alone
@a
==>
Program(Annotation(AnnotationName))
# alone and anonymous
@
==>
Program(Annotation(AnnotationName))
# empty
@ann()
==>
Program(Annotation(AnnotationName,
AnnotationList))
# empty and anonymous
@()
==>
Program(Annotation(AnnotationName,
AnnotationList))
# equals
@setting(a=1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# operator
@ann(a*1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number))))
# anonymous
@(a=1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# complex expr
@ann(a=(1+2+f('yes')))
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
ParenthesizedExpression(BinaryExpression(BinaryExpression(Number,
AddOp,
Number),
AddOp,
CallExpression(VariableName,
ArgumentList(String))))))))
# many args
@ann(a=1, b=2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number),
AnnotationProperty(PropertyName,
Equals,
Number))))
# space around op
@ann(a / 1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number))))
# space around sep
@ann(a/1 , b/2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number),
AnnotationProperty(PropertyName,
MultOp,
Number))))
# trailing sep
@ann(a=1,)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# lone sep
@ann(,)
==>
Program(Annotation(AnnotationName,
AnnotationList))
# inside fn
fn f() {
@anno(b=2)
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
ParamList,
Body(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))))
# laxer with space than the language parser is
@anno (b=2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))

View File

@ -5,7 +5,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
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,6 +20,7 @@ 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'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
@ -37,7 +37,12 @@ export function Toolbar({
const buttonBorderClassName = '!border-transparent' const buttonBorderClassName = '!border-transparent'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) if (
isCursorInFunctionDefinition(
kclManager.ast,
context.selectionRanges.graphSelections[0]
)
)
return false return false
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,

View File

@ -124,14 +124,7 @@ export const ClientSideScene = ({
'mouseup', 'mouseup',
toSync(sceneInfra.onMouseUp, reportRejection) toSync(sceneInfra.onMouseUp, reportRejection)
) )
sceneEntitiesManager sceneEntitiesManager.tearDownSketch({ removeAxis: true })
.tearDownSketch()
.then(() => {
// no op
})
.catch((e) => {
console.error(e)
})
} }
}, []) }, [])
@ -152,7 +145,8 @@ export const ClientSideScene = ({
state.matches({ Sketch: 'Line tool' }) || state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) || state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches({ Sketch: 'Circle tool' }) state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'Circle three point tool' })
) { ) {
cursor = 'crosshair' cursor = 'crosshair'
} else { } else {
@ -190,12 +184,15 @@ const Overlays = () => {
style={{ zIndex: '99999999' }} style={{ zIndex: '99999999' }}
> >
{Object.entries(context.segmentOverlays) {Object.entries(context.segmentOverlays)
.filter((a) => a[1].visible) .flatMap((a) =>
.map(([pathToNodeString, overlay], index) => { a[1].map((b) => ({ pathToNodeString: a[0], overlay: b }))
)
.filter((a) => a.overlay.visible)
.map(({ pathToNodeString, overlay }, index) => {
return ( return (
<Overlay <Overlay
overlay={overlay} overlay={overlay}
key={pathToNodeString} key={pathToNodeString + String(index)}
pathToNodeString={pathToNodeString} pathToNodeString={pathToNodeString}
overlayIndex={index} overlayIndex={index}
/> />
@ -236,11 +233,17 @@ const Overlay = ({
const constraints = const constraints =
callExpression.type === 'CallExpression' callExpression.type === 'CallExpression'
? getConstraintInfo(callExpression, codeManager.code, overlay.pathToNode) ? getConstraintInfo(
callExpression,
codeManager.code,
overlay.pathToNode,
overlay.filterValue
)
: getConstraintInfoKw( : getConstraintInfoKw(
callExpression, callExpression,
codeManager.code, codeManager.code,
overlay.pathToNode overlay.pathToNode,
overlay.filterValue
) )
const offset = 20 // px const offset = 20 // px
@ -260,7 +263,6 @@ const Overlay = ({
state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) state.matches({ Sketch: 'Rectangle tool' })
) )
return ( return (
<div className={`absolute w-0 h-0`}> <div className={`absolute w-0 h-0`}>
<div <div
@ -318,7 +320,8 @@ const Overlay = ({
this will likely change soon when we implement multi-profile so we'll leave it for now this will likely change soon when we implement multi-profile so we'll leave it for now
issue: https://github.com/KittyCAD/modeling-app/issues/3910 issue: https://github.com/KittyCAD/modeling-app/issues/3910
*/} */}
{callExpression?.callee?.name !== 'circle' && ( {callExpression?.callee?.name !== 'circle' &&
callExpression?.callee?.name !== 'circleThreePoint' && (
<SegmentMenu <SegmentMenu
verticalPosition={ verticalPosition={
overlay.windowCoords[1] > window.innerHeight / 2 overlay.windowCoords[1] > window.innerHeight / 2
@ -449,6 +452,8 @@ 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

@ -182,13 +182,15 @@ export class SceneInfra {
callbacks: (() => SegmentOverlayPayload | null)[] = [] callbacks: (() => SegmentOverlayPayload | null)[] = []
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
const segmentOverlayPayload: SegmentOverlayPayload = { const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'set-many', type: 'add-many',
overlays: {}, overlays: {},
} }
callbacks.forEach((cb) => { callbacks.forEach((cb) => {
const overlay = cb() const overlay = cb()
if (overlay?.type === 'set-one') { if (overlay?.type === 'set-one') {
segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg
} else if (overlay?.type === 'add-many') {
Object.assign(segmentOverlayPayload.overlays, overlay.overlays)
} }
}) })
this.modelingSend({ this.modelingSend({
@ -213,25 +215,27 @@ export class SceneInfra {
overlayThrottleMap: { [pathToNodeString: string]: number } = {} overlayThrottleMap: { [pathToNodeString: string]: number } = {}
updateOverlayDetails({ updateOverlayDetails({
arrowGroup, handle,
group, group,
isHandlesVisible, isHandlesVisible,
from, from,
to, to,
angle, angle,
hasThreeDotMenu,
}: { }: {
arrowGroup: Group handle: Group
group: Group group: Group
isHandlesVisible: boolean isHandlesVisible: boolean
from: Coords2d from: Coords2d
to: Coords2d to: Coords2d
hasThreeDotMenu: boolean
angle?: number angle?: number
}): SegmentOverlayPayload | null { }): SegmentOverlayPayload | null {
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) { if (!group.userData.draft && group.userData.pathToNode && handle) {
const vector = new Vector3(0, 0, 0) const vector = new Vector3(0, 0, 0)
// Get the position of the object3D in world space // Get the position of the object3D in world space
arrowGroup.getWorldPosition(vector) handle.getWorldPosition(vector)
// Project that position to screen space // Project that position to screen space
vector.project(this.camControls.camera) vector.project(this.camControls.camera)
@ -244,13 +248,16 @@ export class SceneInfra {
return { return {
type: 'set-one', type: 'set-one',
pathToNodeString, pathToNodeString,
seg: { seg: [
{
windowCoords: [x, y], windowCoords: [x, y],
angle: _angle, angle: _angle,
group, group,
pathToNode: group.userData.pathToNode, pathToNode: group.userData.pathToNode,
visible: isHandlesVisible, visible: isHandlesVisible,
hasThreeDotMenu,
}, },
],
} }
} }
return null return null

View File

@ -31,6 +31,12 @@ import {
CIRCLE_SEGMENT, CIRCLE_SEGMENT,
CIRCLE_SEGMENT_BODY, CIRCLE_SEGMENT_BODY,
CIRCLE_SEGMENT_DASH, CIRCLE_SEGMENT_DASH,
CIRCLE_THREE_POINT_HANDLE1,
CIRCLE_THREE_POINT_HANDLE2,
CIRCLE_THREE_POINT_HANDLE3,
CIRCLE_THREE_POINT_SEGMENT,
CIRCLE_THREE_POINT_SEGMENT_BODY,
CIRCLE_THREE_POINT_SEGMENT_DASH,
EXTRA_SEGMENT_HANDLE, EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX, EXTRA_SEGMENT_OFFSET_PX,
HIDE_HOVER_SEGMENT_LENGTH, HIDE_HOVER_SEGMENT_LENGTH,
@ -56,11 +62,16 @@ import {
} from './sceneInfra' } from './sceneInfra'
import { Themes, getThemeColorForThreeJs } from 'lib/theme' import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { normaliseAngle, roundOff } from 'lib/utils' import { normaliseAngle, roundOff } from 'lib/utils'
import { SegmentOverlayPayload } from 'machines/modelingMachine' import {
SegmentOverlay,
SegmentOverlayPayload,
SegmentOverlays,
} from 'machines/modelingMachine'
import { SegmentInputs } from 'lang/std/stdTypes' import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { editorManager, sceneInfra } from 'lib/singletons' import { sceneInfra } from 'lib/singletons'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
interface CreateSegmentArgs { interface CreateSegmentArgs {
@ -307,11 +318,12 @@ class StraightSegment implements SegmentUtils {
} }
return () => return () =>
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, handle: arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible,
from, from,
to, to,
hasThreeDotMenu: true,
}) })
} }
} }
@ -483,12 +495,13 @@ class TangentialArcToSegment implements SegmentUtils {
) )
return () => return () =>
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, handle: arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible,
from, from,
to, to,
angle, angle,
hasThreeDotMenu: true,
}) })
} }
} }
@ -684,35 +697,255 @@ class CircleSegment implements SegmentUtils {
} }
return () => return () =>
sceneInfra.updateOverlayDetails({ sceneInfra.updateOverlayDetails({
arrowGroup, handle: arrowGroup,
group, group,
isHandlesVisible, isHandlesVisible,
from: from, from: from,
to: [center[0], center[1]], to: [center[0], center[1]],
angle: Math.PI / 4, angle: Math.PI / 4,
hasThreeDotMenu: true,
}) })
} }
} }
class CircleThreePointSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({
input,
id,
pathToNode,
isDraftSegment,
scale = 1,
theme,
isSelected = false,
sceneInfra,
prevSegment,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
const group = new Group()
const geometry = createArcGeometry({
center,
radius,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
isDashed: isDraftSegment,
scale,
})
const mat = new MeshBasicMaterial({ color })
const arcMesh = new Mesh(geometry, mat)
const meshType = isDraftSegment
? CIRCLE_THREE_POINT_SEGMENT_DASH
: CIRCLE_THREE_POINT_SEGMENT_BODY
const handle1 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE1,
color
)
const handle2 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE2,
color
)
const handle3 = createCircleThreePointHandle(
scale,
theme,
CIRCLE_THREE_POINT_HANDLE3,
color
)
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: CIRCLE_THREE_POINT_SEGMENT,
draft: isDraftSegment,
id,
p1,
p2,
p3,
ccw: true,
prevSegment,
pathToNode,
isSelected,
baseColor,
}
group.name = CIRCLE_THREE_POINT_SEGMENT
group.add(arcMesh, handle1, handle2, handle3)
const updateOverlaysCallback = this.update({
prevSegment,
input,
group,
scale,
sceneInfra,
})
if (err(updateOverlaysCallback)) return updateOverlaysCallback
return {
group,
updateOverlaysCallback,
}
}
update: SegmentUtils['update'] = ({
input,
group,
scale = 1,
sceneInfra,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
group.userData.p1 = p1
group.userData.p2 = p2
group.userData.p3 = p3
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
const points = [p1, p2, p3]
const handles = [
CIRCLE_THREE_POINT_HANDLE1,
CIRCLE_THREE_POINT_HANDLE2,
CIRCLE_THREE_POINT_HANDLE3,
].map((handle) => group.getObjectByName(handle) as Group)
handles.forEach((handle, i) => {
const point = points[i]
if (handle && point) {
handle.position.set(point[0], point[1], 0)
handle.scale.set(scale, scale, scale)
handle.visible = true
}
})
const pxLength = (2 * radius * Math.PI) / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
const circleSegmentBody = group.children.find(
(child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY
) as Mesh
if (circleSegmentBody) {
const newGeo = createArcGeometry({
radius,
center,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
scale,
})
circleSegmentBody.geometry = newGeo
}
const circleSegmentBodyDashed = group.getObjectByName(
CIRCLE_THREE_POINT_SEGMENT_DASH
)
if (circleSegmentBodyDashed instanceof Mesh) {
// consider throttling the whole updateTangentialArcToSegment
// if there are more perf considerations going forward
circleSegmentBodyDashed.geometry = createArcGeometry({
center,
radius,
ccw: true,
// make the start end where the handle is
startAngle: Math.PI * 0.25,
endAngle: Math.PI * 2.25,
isDashed: true,
scale,
})
}
return () => {
const overlays: SegmentOverlays = {}
const points = [p1, p2, p3]
const overlayDetails = handles.map((handle, index) => {
const currentPoint = points[index]
const angle = Math.atan2(
currentPoint[1] - center[1],
currentPoint[0] - center[0]
)
return sceneInfra.updateOverlayDetails({
handle,
group,
isHandlesVisible,
from: [0, 0],
to: [center[0], center[1]],
angle: angle,
hasThreeDotMenu: index === 0,
})
})
const segmentOverlays: SegmentOverlay[] = []
overlayDetails.forEach((payload, index) => {
if (payload?.type === 'set-one') {
overlays[payload.pathToNodeString] = payload.seg
segmentOverlays.push({
...payload.seg[0],
filterValue: index === 0 ? 'p1' : index === 1 ? 'p2' : 'p3',
})
}
})
const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'set-one',
pathToNodeString:
overlayDetails[0]?.type === 'set-one'
? overlayDetails[0].pathToNodeString
: '',
seg: segmentOverlays,
}
return segmentOverlayPayload
}
}
}
export function createProfileStartHandle({ export function createProfileStartHandle({
from, from,
isDraft = false, isDraft = false,
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(12, 12, 12) // in pixels scaled later const geometry = new BoxGeometry(size, size, size) // 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 })
@ -774,6 +1007,29 @@ function createCircleCenterHandle(
circleCenterGroup.scale.set(scale, scale, scale) circleCenterGroup.scale.set(scale, scale, scale)
return circleCenterGroup return circleCenterGroup
} }
function createCircleThreePointHandle(
scale = 1,
theme: Themes,
name: `circle-three-point-handle${'1' | '2' | '3'}`,
color?: number
): Group {
const circleCenterGroup = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme)
const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body)
circleCenterGroup.add(mesh)
circleCenterGroup.userData = {
type: name,
baseColor,
}
circleCenterGroup.name = name
circleCenterGroup.scale.set(scale, scale, scale)
return circleCenterGroup
}
function createExtraSegmentHandle( function createExtraSegmentHandle(
scale: number, scale: number,
@ -1100,4 +1356,5 @@ export const segmentUtils = {
straight: new StraightSegment(), straight: new StraightSegment(),
tangentialArcTo: new TangentialArcToSegment(), tangentialArcTo: new TangentialArcToSegment(),
circle: new CircleSegment(), circle: new CircleSegment(),
circleThreePoint: new CircleThreePointSegment(),
} as const } as const

View File

@ -25,7 +25,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { import {
isCursorInSketchCommandRange, isCursorInSketchCommandRange,
updatePathToNodeFromMap, updateSketchDetailsNodePaths,
} from 'lang/util' } from 'lang/util'
import { import {
kclManager, kclManager,
@ -65,17 +65,31 @@ import {
replaceValueAtNodePath, replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
splitPipedProfile,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import {
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst' KclValue,
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import {
artifactIsPlaneWithPaths,
doesSketchPipeNeedSplitting,
getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} 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 } from 'lib/trap' import { err, reportRejection, trap, reject } from 'lib/trap'
import { import {
ExportIntent, ExportIntent,
EngineConnectionStateType, EngineConnectionStateType,
@ -86,10 +100,16 @@ 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 {
getFaceCodeRef,
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'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine' import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -254,7 +274,11 @@ export const ModelingMachineProvider = ({
'Set Segment Overlays': assign({ 'Set Segment Overlays': assign({
segmentOverlays: ({ context: { segmentOverlays }, event }) => { segmentOverlays: ({ context: { segmentOverlays }, event }) => {
if (event.type !== 'Set Segment Overlays') return {} if (event.type !== 'Set Segment Overlays') return {}
if (event.data.type === 'set-many') return event.data.overlays if (event.data.type === 'add-many')
return {
...segmentOverlays,
...event.data.overlays,
}
if (event.data.type === 'set-one') if (event.data.type === 'set-one')
return { return {
...segmentOverlays, ...segmentOverlays,
@ -287,7 +311,7 @@ export const ModelingMachineProvider = ({
return { return {
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: event.data, sketchEntryNodePath: event.data,
}, },
} }
}), }),
@ -483,9 +507,17 @@ export const ModelingMachineProvider = ({
selectionRanges: setSelections.selection, selectionRanges: setSelections.selection,
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: sketchEntryNodePath:
setSelections.updatedPathToNode || setSelections.updatedSketchEntryNodePath ||
sketchDetails?.sketchPathToNode || sketchDetails?.sketchEntryNodePath ||
[],
sketchNodePaths:
setSelections.updatedSketchNodePaths ||
sketchDetails?.sketchNodePaths ||
[],
planeNodePath:
setSelections.updatedPlaneNodePath ||
sketchDetails?.planeNodePath ||
[], [],
}, },
} }
@ -638,7 +670,12 @@ export const ModelingMachineProvider = ({
if (artifactIsPlaneWithPaths(selectionRanges)) { if (artifactIsPlaneWithPaths(selectionRanges)) {
return true return true
} }
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
)
return false return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
@ -666,13 +703,33 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails } }) => { async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (kclManager.ast.body.length) { if (kclManager.ast.body.length) {
// 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?
const newAst = structuredClone(kclManager.ast) const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.sketchPathToNode[1][0] const varDecIndex = sketchDetails.planeNodePath[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: () => {},
@ -682,7 +739,7 @@ export const ModelingMachineProvider = ({
} }
), ),
'animate-to-face': fromPromise(async ({ input }) => { 'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return undefined if (!input) return null
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched = const sketched =
input.type === 'extrudeFace' input.type === 'extrudeFace'
@ -709,7 +766,9 @@ export const ModelingMachineProvider = ({
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchPathToNode: pathToNewSketchNode, sketchEntryNodePath: [],
planeNodePath: pathToNewSketchNode,
sketchNodePaths: [],
zAxis: input.zAxis, zAxis: input.zAxis,
yAxis: input.yAxis, yAxis: input.yAxis,
origin: input.position, origin: input.position,
@ -730,7 +789,9 @@ export const ModelingMachineProvider = ({
) )
return { return {
sketchPathToNode: pathToNode, sketchEntryNodePath: [],
planeNodePath: pathToNode,
sketchNodePaths: [],
zAxis: input.zAxis, zAxis: input.zAxis,
yAxis: input.yAxis, yAxis: input.yAxis,
origin: [0, 0, 0], origin: [0, 0, 0],
@ -739,21 +800,70 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sourceRange = const plane = getPlaneFromArtifact(
selectionRanges.graphSelections[0]?.codeRef?.range selectionRanges.graphSelections[0].artifact,
const sketchPathToNode = getNodePathFromSourceRange( engineCommandManager.artifactGraph
kclManager.ast,
sourceRange
)
const info = await getSketchOrientationDetails(
sketchPathToNode || []
) )
if (err(plane)) return Promise.reject(plane)
let sketch: KclValue | null = null
for (const variable of Object.values(
kclManager.execState.variables
)) {
// find programMemory that matches path artifact
if (
variable?.type === 'Sketch' &&
variable.value.artifactId === plane.pathIds[0]
) {
sketch = variable
break
}
if (
// if the variable is an sweep, check if the underlying sketch matches the artifact
variable?.type === 'Solid' &&
variable.value.sketch.on.type === 'plane' &&
variable.value.sketch.artifactId === plane.pathIds[0]
) {
sketch = {
type: 'Sketch',
value: variable.value.sketch,
}
break
}
}
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
const info = await getSketchOrientationDetails(sketch.value)
await letEngineAnimateAndSyncCamAfter( await letEngineAnimateAndSyncCamAfter(
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
const sketchArtifact = engineCommandManager.artifactGraph.get(
plane.pathIds[0]
)
if (sketchArtifact?.type !== 'path')
return Promise.reject(new Error('No sketch artifact'))
const sketchPaths = getPathsFromArtifact({
artifact: engineCommandManager.artifactGraph.get(plane.id),
sketchPathToNode: sketchArtifact?.codeRef?.pathToNode,
artifactGraph: engineCommandManager.artifactGraph,
ast: kclManager.ast,
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
let codeRef = getFaceCodeRef(plane)
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
// codeRef.pathToNode is not always populated correctly
const planeNodePath = getNodePathFromSourceRange(
kclManager.ast,
codeRef.range
)
return { return {
sketchPathToNode: sketchPathToNode || [], sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath,
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(
@ -766,7 +876,7 @@ export const ModelingMachineProvider = ({
'Get horizontal info': fromPromise( 'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, selectionRanges,
@ -778,13 +888,23 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -805,13 +925,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get vertical info': fromPromise( 'Get vertical info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, selectionRanges,
@ -822,13 +944,23 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -849,7 +981,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -859,7 +993,8 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
}) })
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap } = await (info.enabled const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await (info.enabled
? applyConstraintAngleBetween({ ? applyConstraintAngleBetween({
selectionRanges, selectionRanges,
}) })
@ -875,13 +1010,23 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -902,7 +1047,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -917,20 +1064,30 @@ export const ModelingMachineProvider = ({
length: lengthValue, length: lengthValue,
}) })
if (err(constraintResult)) return Promise.reject(constraintResult) if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult const { modifiedAst, pathToNodeMap, exprInsertIndex } =
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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -951,13 +1108,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get perpendicular distance info': fromPromise( 'Get perpendicular distance info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintIntersect({ await applyConstraintIntersect({
selectionRanges, selectionRanges,
}) })
@ -967,13 +1126,22 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -994,13 +1162,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS X info': fromPromise( 'Get ABS X info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'xAbs', constraint: 'xAbs',
selectionRanges, selectionRanges,
@ -1011,13 +1181,22 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1038,13 +1217,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS Y info': fromPromise( 'Get ABS Y info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'yAbs', constraint: 'yAbs',
selectionRanges, selectionRanges,
@ -1055,13 +1236,22 @@ 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(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1082,7 +1272,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -1102,9 +1294,11 @@ 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
@ -1134,6 +1328,7 @@ 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,
@ -1164,10 +1359,22 @@ 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(
result.pathToReplaced || [], updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1188,7 +1395,194 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode: result.pathToReplaced, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'set-up-draft-circle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
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-circle-three-point': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result =
await sceneEntitiesManager.setupDraftCircleThreePoint(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data.p1,
data.p2
)
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')
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')
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,
// We will want to pass sketchTools here
// to add their interactions
})
// We will want to update the context with sketchTools.
// They'll be used for their .destroy() in tearDownSketch
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,
expressionIndexToDelete: -1,
} 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)
let moddedAst: Program = structuredClone(kclManager.ast)
let pathToProfile = sketchDetails.sketchEntryNodePath
let updatedSketchNodePaths = sketchDetails.sketchNodePaths
if (doesNeedSplitting) {
const splitResult = splitPipedProfile(
moddedAst,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
moddedAst = splitResult.modifiedAst
pathToProfile = splitResult.pathToProfile
updatedSketchNodePaths = [pathToProfile]
}
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
if (indexToDelete >= 0) {
// this is the expression that was added when as sketch tool was used but not completed
// i.e first click for the center of the circle, but not the second click for the radius
// we added a circle to editor, but they bailed out early so we should remove it
moddedAst.body.splice(indexToDelete, 1)
// make sure the deleted expression is removed from the sketchNodePaths
updatedSketchNodePaths = updatedSketchNodePaths.filter(
(path) => path[1][0] !== indexToDelete
)
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
// as a safe default
pathToProfile =
pathToProfile[1][0] !== indexToDelete
? pathToProfile
: updatedSketchNodePaths[0]
}
if (doesNeedSplitting) {
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
}
return {
updatedEntryNodePath: pathToProfile,
updatedSketchNodePaths: updatedSketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1,
} }
} }
), ),

View File

@ -13,12 +13,7 @@ import {
getOperationLabel, getOperationLabel,
stdLibMap, stdLibMap,
} from 'lib/operations' } from 'lib/operations'
import { import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
codeManager,
editorManager,
engineCommandManager,
kclManager,
} from 'lib/singletons'
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react' import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { Actor, Prop } from 'xstate' import { Actor, Prop } from 'xstate'
@ -67,7 +62,7 @@ export const FeatureTreePane = () => {
) )
: null : null
if (!artifact || !('codeRef' in artifact)) { if (!artifact) {
modelingSend({ modelingSend({
type: 'Set selection', type: 'Set selection',
data: { data: {

View File

@ -2,7 +2,12 @@ import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => { export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return ( return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}> <svg
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

@ -136,6 +136,7 @@ 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,
@ -174,6 +175,7 @@ export async function applyConstraintIntersect({
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: -1,
} }
} }
// transform again but forcing certain values // transform again but forcing certain values
@ -192,6 +194,7 @@ 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(
@ -204,9 +207,11 @@ 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 (err(tmp)) return tmp if (tmp instanceof Error) return tmp
return tmp.node return tmp.node
}) })
const _err1 = _nodes.find(err) const _err1 = _nodes.find(err)

View File

@ -92,6 +92,7 @@ export async function applyConstraintAbsDistance({
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap
exprInsertIndex: number
}> { }> {
const info = absDistanceInfo({ const info = absDistanceInfo({
selectionRanges, selectionRanges,
@ -131,6 +132,7 @@ 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(
@ -143,8 +145,9 @@ 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 } return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex }
} }
export function applyConstraintAxisAlign({ export function applyConstraintAxisAlign({

View File

@ -86,6 +86,7 @@ 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)
@ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: -1,
} }
} }
@ -141,6 +143,7 @@ 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(
@ -153,9 +156,11 @@ 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,15 +87,13 @@ 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,
@ -133,13 +131,12 @@ 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 = isAlign let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
? 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,
@ -152,6 +149,7 @@ 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(
@ -164,10 +162,12 @@ 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

@ -74,10 +74,14 @@ 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 angleLength if (err(angleLength)) return Promise.reject(angleLength)
const { transforms } = angleLength const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst let distanceExpression: Expr = length.valueAst
@ -98,7 +102,7 @@ export async function applyConstraintLength({
} }
if (!isExprBinaryPart(distanceExpression)) { if (!isExprBinaryPart(distanceExpression)) {
return new Error('Invalid valueNode, is not a BinaryPart') return Promise.reject('Invalid valueNode, is not a BinaryPart')
} }
const retval = transformAstSketchLines({ const retval = transformAstSketchLines({
@ -116,6 +120,12 @@ export async function applyConstraintLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex:
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
? length.insertIndex
: -1,
} }
} }
@ -128,6 +138,7 @@ 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)
@ -212,5 +223,6 @@ export async function applyConstraintAngleLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
} }
} }

View File

@ -413,7 +413,6 @@ export class KclManager {
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,
@ -465,6 +464,7 @@ export class KclManager {
this._logs = logs this._logs = logs
this.addDiagnostics(kclErrorsToDiagnostics(errors)) this.addDiagnostics(kclErrorsToDiagnostics(errors))
this._execState = execState this._execState = execState
this._variables = execState.variables this._variables = execState.variables
if (!errors.length) { if (!errors.length) {

View File

@ -8,7 +8,7 @@ import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state' import { Annotation, Transaction } from '@codemirror/state'
import { EditorView, KeyBinding } from '@codemirror/view' import { EditorView, KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm' import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { Compartment } from '@codemirror/state' import { Compartment } from '@codemirror/state'
import { history } from '@codemirror/commands' import { history } from '@codemirror/commands'
@ -168,7 +168,7 @@ export default class CodeManager {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return if (err(newCode)) return
this.updateCodeStateEditor(newCode) this.updateCodeStateEditor(newCode)
await this.writeToFile() this.writeToFile().catch(reportRejection)
} }
} }

View File

@ -239,6 +239,7 @@ const newVar = myVar + 1`
expect(mem['three']).toEqual({ expect(mem['three']).toEqual({
type: 'Number', type: 'Number',
value: 3, value: 3,
ty: expect.any(Object),
__meta: [ __meta: [
{ {
sourceRange: [14, 15, 0], sourceRange: [14, 15, 0],
@ -248,12 +249,23 @@ const newVar = myVar + 1`
expect(mem['yo']).toEqual({ expect(mem['yo']).toEqual({
type: 'Array', type: 'Array',
value: [ value: [
{ type: 'Number', value: 1, __meta: [{ sourceRange: [28, 29, 0] }] }, {
type: 'Number',
value: 1,
ty: expect.any(Object),
__meta: [{ sourceRange: [28, 29, 0] }],
},
{ type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] }, { type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] },
{ type: 'Number', value: 3, __meta: [{ sourceRange: [14, 15, 0] }] }, {
type: 'Number',
value: 3,
ty: expect.any(Object),
__meta: [{ sourceRange: [14, 15, 0] }],
},
{ {
type: 'Number', type: 'Number',
value: 9, value: 9,
ty: expect.any(Object),
__meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }], __meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }],
}, },
], ],
@ -281,16 +293,19 @@ const newVar = myVar + 1`
anum: { anum: {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: expect.any(Object),
__meta: [{ sourceRange: [47, 48, 0] }], __meta: [{ sourceRange: [47, 48, 0] }],
}, },
identifier: { identifier: {
type: 'Number', type: 'Number',
value: 3, value: 3,
ty: expect.any(Object),
__meta: [{ sourceRange: [14, 15, 0] }], __meta: [{ sourceRange: [14, 15, 0] }],
}, },
binExp: { binExp: {
type: 'Number', type: 'Number',
value: 9, value: 9,
ty: expect.any(Object),
__meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }], __meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }],
}, },
}, },
@ -404,6 +419,7 @@ describe('testing math operators', () => {
], ],
type: 'Number', type: 'Number',
value: 1, value: 1,
ty: expect.any(Object),
}, },
{ {
__meta: [ __meta: [
@ -413,6 +429,7 @@ describe('testing math operators', () => {
], ],
type: 'Number', type: 'Number',
value: -3, value: -3,
ty: expect.any(Object),
}, },
]) ])
}) })

View File

@ -32,7 +32,7 @@ child_process.spawnSync('git', [
'clone', 'clone',
'--single-branch', '--single-branch',
'--branch', '--branch',
'achalmers/kw-pattern-transform2', 'achalmers/offset-plane-kwargs',
URL_GIT_KCL_SAMPLES, URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES, DIR_KCL_SAMPLES,
]) ])

View File

@ -27,6 +27,7 @@ export type ToolTip =
| 'angledLineThatIntersects' | 'angledLineThatIntersects'
| 'tangentialArcTo' | 'tangentialArcTo'
| 'circle' | 'circle'
| 'circleThreePoint'
export const toolTips: Array<ToolTip> = [ export const toolTips: Array<ToolTip> = [
'line', 'line',
@ -42,6 +43,7 @@ export const toolTips: Array<ToolTip> = [
'yLineTo', 'yLineTo',
'angledLineThatIntersects', 'angledLineThatIntersects',
'tangentialArcTo', 'tangentialArcTo',
'circleThreePoint',
] ]
export async function executeAst({ export async function executeAst({
@ -71,7 +73,6 @@ export async function executeAst({
: executeWithEngine(ast, engineCommandManager, path)) : executeWithEngine(ast, engineCommandManager, path))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -3,7 +3,6 @@ import {
recast, recast,
initPromise, initPromise,
Identifier, Identifier,
SourceRange,
topLevelRange, topLevelRange,
LiteralValue, LiteralValue,
Literal, Literal,
@ -25,6 +24,7 @@ import {
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
removeSingleConstraintInfo, removeSingleConstraintInfo,
deleteFromSelection, deleteFromSelection,
splitPipedProfile,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe } from './queryAst' import { findUsesOfTagInPipe } from './queryAst'
@ -821,144 +821,146 @@ sketch003 = startSketchOn('XZ')
type: 'segment', type: 'segment',
}, },
], ],
[ // TODO FIXME, similar to fix me in e2e/playwright/testing-selections.spec.ts
'delete extrude', // also related to deleting, deleting in general probably is due for a refactor
{ // [
codeBefore: `sketch001 = startSketchOn('XZ') // 'delete extrude',
|> startProfileAt([3.29, 7.86], %) // {
|> line(end = [2.48, 2.44]) // codeBefore: `sketch001 = startSketchOn('XZ')
|> line(end = [2.66, 1.17]) // |> startProfileAt([3.29, 7.86], %)
|> line(end = [3.75, 0.46]) // |> line(end = [2.48, 2.44])
|> line(end = [4.99, -0.46], tag = $seg01) // |> line(end = [2.66, 1.17])
|> line(end = [-3.86, -2.73]) // |> line(end = [3.75, 0.46])
|> line(end = [-17.67, 0.85]) // |> line(end = [4.99, -0.46], tag = $seg01)
|> close() // |> line(end = [-3.86, -2.73])
const extrude001 = extrude(sketch001, length = 10)`, // |> line(end = [-17.67, 0.85])
codeAfter: `sketch001 = startSketchOn('XZ') // |> close()
|> startProfileAt([3.29, 7.86], %) // const extrude001 = extrude(sketch001, length = 10)`,
|> line(end = [2.48, 2.44]) // codeAfter: `sketch001 = startSketchOn('XZ')
|> line(end = [2.66, 1.17]) // |> startProfileAt([3.29, 7.86], %)
|> line(end = [3.75, 0.46]) // |> line(end = [2.48, 2.44])
|> line(end = [4.99, -0.46], tag = $seg01) // |> line(end = [2.66, 1.17])
|> line(end = [-3.86, -2.73]) // |> line(end = [3.75, 0.46])
|> line(end = [-17.67, 0.85]) // |> line(end = [4.99, -0.46], tag = $seg01)
|> close()\n`, // |> line(end = [-3.86, -2.73])
lineOfInterest: 'line(end = [2.66, 1.17])', // |> line(end = [-17.67, 0.85])
type: 'wall', // |> close()\n`,
}, // lineOfInterest: 'line(end = [2.66, 1.17])',
], // type: 'wall',
[ // },
'delete extrude with sketch on it', // ],
{ // [
codeBefore: `myVar = 5 // 'delete extrude with sketch on it',
sketch001 = startSketchOn('XZ') // {
|> startProfileAt([4.46, 5.12], %, $tag) // codeBefore: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
const extrude001 = extrude(sketch001, length = 5) // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch002 = startSketchOn(extrude001, seg01) // |> close()
|> startProfileAt([-12.55, 2.89], %) // const extrude001 = extrude(sketch001, length = 5)
|> line(end = [3.02, 1.9]) // sketch002 = startSketchOn(extrude001, seg01)
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close()`, // |> line(end = [0.3, 0.84])
codeAfter: `myVar = 5 // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch001 = startSketchOn('XZ') // |> close()`,
|> startProfileAt([4.46, 5.12], %, $tag) // codeAfter: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
sketch002 = startSketchOn({ // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
plane = { // |> close()
origin = { x = 1, y = 2, z = 3 }, // sketch002 = startSketchOn({
xAxis = { x = 4, y = 5, z = 6 }, // plane = {
yAxis = { x = 7, y = 8, z = 9 }, // origin = { x = 1, y = 2, z = 3 },
zAxis = { x = 10, y = 11, z = 12 } // xAxis = { x = 4, y = 5, z = 6 },
} // yAxis = { x = 7, y = 8, z = 9 },
}) // zAxis = { x = 10, y = 11, z = 12 }
|> startProfileAt([-12.55, 2.89], %) // }
|> line(end = [3.02, 1.9]) // })
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close() // |> line(end = [0.3, 0.84])
`, // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
lineOfInterest: 'line(end = [-11.18, -2.15])', // |> close()
type: 'wall', // `,
}, // lineOfInterest: 'line(end = [-11.18, -2.15])',
], // type: 'wall',
[ // },
'delete extrude with sketch on it 2', // ],
{ // [
codeBefore: `myVar = 5 // 'delete extrude with sketch on it 2',
sketch001 = startSketchOn('XZ') // {
|> startProfileAt([4.46, 5.12], %, $tag) // codeBefore: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
const extrude001 = extrude(sketch001, length = 5) // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch002 = startSketchOn(extrude001, seg01) // |> close()
|> startProfileAt([-12.55, 2.89], %) // const extrude001 = extrude(sketch001, length = 5)
|> line(end = [3.02, 1.9]) // sketch002 = startSketchOn(extrude001, seg01)
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close()`, // |> line(end = [0.3, 0.84])
codeAfter: `myVar = 5 // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch001 = startSketchOn('XZ') // |> close()`,
|> startProfileAt([4.46, 5.12], %, $tag) // codeAfter: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
sketch002 = startSketchOn({ // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
plane = { // |> close()
origin = { x = 1, y = 2, z = 3 }, // sketch002 = startSketchOn({
xAxis = { x = 4, y = 5, z = 6 }, // plane = {
yAxis = { x = 7, y = 8, z = 9 }, // origin = { x = 1, y = 2, z = 3 },
zAxis = { x = 10, y = 11, z = 12 } // xAxis = { x = 4, y = 5, z = 6 },
} // yAxis = { x = 7, y = 8, z = 9 },
}) // zAxis = { x = 10, y = 11, z = 12 }
|> startProfileAt([-12.55, 2.89], %) // }
|> line(end = [3.02, 1.9]) // })
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close() // |> line(end = [0.3, 0.84])
`, // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)', // |> close()
type: 'cap', // `,
}, // lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)',
], // type: 'cap',
// },
// ],
] as const ] as const
test.each(cases)( test.each(cases)(
'%s', '%s',
@ -980,6 +982,7 @@ sketch002 = startSketchOn({
artifact, artifact,
}, },
execState.variables, execState.variables,
execState.artifactGraph,
async () => { async () => {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
return { return {
@ -996,3 +999,63 @@ 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, number] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
0,
]
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, number] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
0,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
expect(result instanceof Error).toBe(true)
})
})

View File

@ -21,8 +21,11 @@ import {
SourceRange, SourceRange,
sketchFromKclValue, sketchFromKclValue,
isPathToNodeNumber, isPathToNodeNumber,
parse,
formatNumber, formatNumber,
ArtifactGraph,
VariableMap, VariableMap,
KclValue,
} from './wasm' } from './wasm'
import { import {
isNodeSafeToReplacePath, isNodeSafeToReplacePath,
@ -31,8 +34,11 @@ import {
getNodeFromPath, getNodeFromPath,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse, traverse,
getBodyIndex,
isCallExprWithName,
ARG_INDEX_FIELD, ARG_INDEX_FIELD,
LABELED_ARG_FIELD, LABELED_ARG_FIELD,
UNLABELED_ARG,
} from './queryAst' } from './queryAst'
import { import {
addTagForSketchOnFace, addTagForSketchOnFace,
@ -48,7 +54,7 @@ import {
transformAstSketchLines, transformAstSketchLines,
} from './std/sketchcombos' } from './std/sketchcombos'
import { DefaultPlaneStr } from 'lib/planes' import { DefaultPlaneStr } from 'lib/planes'
import { isOverlap, roundOff } from 'lib/utils' import { isArray, isOverlap, roundOff } from 'lib/utils'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { SimplifiedArgDetails } from './std/stdTypes' import { SimplifiedArgDetails } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
@ -56,6 +62,17 @@ 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,
expandCap,
expandPlane,
expandWall,
getArtifactOfTypes,
getArtifactsOfTypes,
getFaceCodeRef,
getPathsFromArtifact,
} from './std/artifactGraph'
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
import { findKwArg } from './util' import { findKwArg } from './util'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
@ -90,41 +107,54 @@ export function startSketchOnDefault(
} }
} }
export function addStartProfileAt( export function insertNewStartProfileAt(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, sketchEntryNodePath: PathToNode,
at: [number, number] sketchNodePaths: PathToNode[],
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error { planeNodePath: PathToNode,
const _node1 = getNodeFromPath<VariableDeclaration>( at: [number, number],
node, insertType: 'start' | 'end' = 'end'
pathToNode, ):
'VariableDeclaration' | {
) modifiedAst: Node<Program>
if (err(_node1)) return _node1 updatedSketchNodePaths: PathToNode[]
const variableDeclaration = _node1.node updatedEntryNodePath: PathToNode
if (variableDeclaration.type !== 'VariableDeclaration') {
return new Error('variableDeclaration.init.type !== PipeExpression')
} }
const _node = { ...node } | Error {
const init = variableDeclaration.declaration.init const varDec = getNodeFromPath<VariableDeclarator>(
const startProfileAt = createCallExpressionStdLib('startProfileAt', [ node,
planeNodePath,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
const newExpression = createVariableDeclaration(
findUniqueName(node, 'profile'),
createCallExpressionStdLib('startProfileAt', [
createArrayExpression([ createArrayExpression([
createLiteral(roundOff(at[0])), createLiteral(roundOff(at[0])),
createLiteral(roundOff(at[1])), createLiteral(roundOff(at[1])),
]), ]),
createPipeSubstitution(), createIdentifier(varDec.node.id.name),
]) ])
if (init.type === 'PipeExpression') { )
init.body.splice(1, 0, startProfileAt) const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
} else {
variableDeclaration.declaration.init = createPipeExpression([ const _node = structuredClone(node)
init, // TODO the rest of this function will not be robust to work for sketches defined within a function declaration
startProfileAt, _node.body.splice(insertIndex, 0, newExpression)
])
} const { updatedEntryNodePath, updatedSketchNodePaths } =
updateSketchNodePathsWithInsertIndex({
insertIndex,
insertType,
sketchNodePaths,
})
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, updatedSketchNodePaths,
updatedEntryNodePath,
} }
} }
@ -224,8 +254,21 @@ export function mutateKwArg(
for (let i = 0; i < node.arguments.length; i++) { for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i] const arg = node.arguments[i]
if (arg.label.name === label) { if (arg.label.name === label) {
if (isLiteralArrayOrStatic(val) && isLiteralArrayOrStatic(arg.arg)) {
node.arguments[i].arg = val node.arguments[i].arg = val
return true return true
} else if (
arg.arg.type === 'ArrayExpression' &&
val.type === 'ArrayExpression'
) {
const arrExp = arg.arg
arrExp.elements.forEach((element, i) => {
if (isLiteralArrayOrStatic(element)) {
arrExp.elements[i] = val.elements[i]
}
})
return true
}
} }
} }
node.arguments.push(createLabeledArg(label, val)) node.arguments.push(createLabeledArg(label, val))
@ -288,15 +331,17 @@ export function mutateObjExpProp(
export function extrudeSketch({ export function extrudeSketch({
node, node,
pathToNode, pathToNode,
shouldPipe = false,
distance = createLiteral(4), distance = createLiteral(4),
extrudeName, extrudeName,
artifact,
artifactGraph,
}: { }: {
node: Node<Program> node: Node<Program>
pathToNode: PathToNode pathToNode: PathToNode
shouldPipe?: boolean
distance: Expr distance: Expr
extrudeName?: string extrudeName?: string
artifactGraph: ArtifactGraph
artifact?: Artifact
}): }):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -304,10 +349,16 @@ export function extrudeSketch({
pathToExtrudeArg: PathToNode pathToExtrudeArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToNode,
artifactGraph,
ast: node,
})
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>(
@ -316,9 +367,6 @@ 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,
@ -326,54 +374,27 @@ export function extrudeSketch({
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(_node3)) return _node3 if (err(_node3)) return _node3
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3 const { node: variableDeclarator } = _node3
const sketchToExtrude = shouldPipe const extrudeCall = createCallExpressionStdLibKw(
? createPipeSubstitution() 'extrude',
: createIdentifier(variableDeclarator.id.name) createIdentifier(variableDeclarator.id.name),
const extrudeCall = createCallExpressionStdLibKw('extrude', sketchToExtrude, [ [createLabeledArg('length', distance)]
createLabeledArg('length', distance), )
])
// index of the 'length' arg above. If you reorder the labeled args above, // index of the 'length' arg above. If you reorder the labeled args above,
// make sure to update this too. // make sure to update this too.
const argIndex = 0 const argIndex = 0
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', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
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 = const name =
extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE) extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
const VariableDeclaration = createVariableDeclaration(name, extrudeCall) const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const sketchIndexInPathToNode = const lastSketchNodePath =
pathToDecleration.findIndex((a) => a[0] === 'body') + 1 orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = pathToDecleration[
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 = [
@ -656,10 +677,11 @@ export function addOffsetPlane({
const newPlane = createVariableDeclaration( const newPlane = createVariableDeclaration(
newPlaneName, newPlaneName,
createCallExpressionStdLib('offsetPlane', [ createCallExpressionStdLibKw(
'offsetPlane',
createLiteral(defaultPlane.toUpperCase()), createLiteral(defaultPlane.toUpperCase()),
offset, [createLabeledArg('offset', offset)]
]) )
) )
const insertAt = const insertAt =
@ -677,8 +699,7 @@ export function addOffsetPlane({
[insertAt, 'index'], [insertAt, 'index'],
['declaration', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['unlabeled', UNLABELED_ARG],
[0, 'index'],
] ]
return { return {
modifiedAst, modifiedAst,
@ -963,6 +984,7 @@ export function createCallExpressionStdLibKw(
end: 0, end: 0,
moduleId: 0, moduleId: 0,
outerAttrs: [], outerAttrs: [],
nonCodeMeta: nonCodeMetaEmpty(),
callee: { callee: {
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
@ -1370,10 +1392,53 @@ export async function deleteFromSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selection, selection: Selection,
variables: VariableMap, variables: VariableMap,
artifactGraph: ArtifactGraph,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () => getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any) ({} as any)
): Promise<Node<Program> | Error> { ): Promise<Node<Program> | Error> {
const astClone = structuredClone(ast) const astClone = structuredClone(ast)
if (
(selection.artifact?.type === 'plane' ||
selection.artifact?.type === 'cap' ||
selection.artifact?.type === 'wall') &&
selection.artifact?.pathIds?.length
) {
const plane =
selection.artifact.type === 'plane'
? expandPlane(selection.artifact, artifactGraph)
: selection.artifact.type === 'wall'
? expandWall(selection.artifact, artifactGraph)
: expandCap(selection.artifact, artifactGraph)
for (const path of plane.paths.sort(
(a, b) => b.codeRef.range?.[0] - a.codeRef.range?.[0]
)) {
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
path.codeRef.pathToNode,
'VariableDeclarator'
)
if (err(varDec)) return varDec
const bodyIndex = Number(varDec.shallowPath[1][0])
astClone.body.splice(bodyIndex, 1)
}
// If it's a cap, we're not going to continue and try to
// delete the extrusion
if (
selection.artifact.type === 'cap' ||
selection.artifact.type === 'wall'
) {
// Delete the sketch node, which would not work if
// we continued down the traditional code path below.
// faceCodeRef's pathToNode is empty for some reason
// so using source range instead
const codeRef = getFaceCodeRef(selection.artifact)
if (!codeRef) return new Error('Could not find face code ref')
const sketchVarDec = getNodePathFromSourceRange(astClone, codeRef.range)
const sketchBodyIndex = Number(sketchVarDec[1][0])
astClone.body.splice(sketchBodyIndex, 1)
return astClone
}
}
const varDec = getNodeFromPath<VariableDeclarator>( const varDec = getNodeFromPath<VariableDeclarator>(
ast, ast,
selection?.codeRef?.pathToNode, selection?.codeRef?.pathToNode,
@ -1452,59 +1517,108 @@ export async function deleteFromSelection(
if (extrudeNameToDelete) { if (extrudeNameToDelete) {
await new Promise((resolve) => { await new Promise((resolve) => {
;(async () => { ;(async () => {
let currentVariableName = ''
const pathsDependingOnExtrude: Array<{ const pathsDependingOnExtrude: Array<{
path: PathToNode path: PathToNode
sketchName: string variable: KclValue
}> = [] }> = []
traverse(astClone, {
leave: (node) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = ''
}
},
enter: (node, path) => {
;(async () => {
if (node.type === 'VariableDeclaration') {
currentVariableName = node.declaration.id.name
}
if (
// match startSketchOn(${extrudeNameToDelete})
node.type === 'CallExpression' &&
node.callee.name === 'startSketchOn' &&
node.arguments[0].type === 'Identifier' &&
node.arguments[0].name === extrudeNameToDelete
) {
pathsDependingOnExtrude.push({
path,
sketchName: currentVariableName,
})
}
})().catch(reportRejection)
},
})
const roundLiteral = (x: number) => createLiteral(roundOff(x)) const roundLiteral = (x: number) => createLiteral(roundOff(x))
const modificationDetails: { const modificationDetails: {
parent: PipeExpression['body'] parentPipe: PipeExpression['body']
parentInit: VariableDeclarator
faceDetails: Models['FaceIsPlanar_type'] faceDetails: Models['FaceIsPlanar_type']
lastKey: number lastKey: number | string
}[] = [] }[] = []
for (const { path, sketchName } of pathsDependingOnExtrude) { const wallArtifact =
const parent = getNodeFromPath<PipeExpression['body']>( selection.artifact?.type === 'wall'
? selection.artifact
: selection.artifact?.type === 'segment' &&
selection.artifact.surfaceId
? getArtifactOfTypes(
{ key: selection.artifact.surfaceId, types: ['wall'] },
artifactGraph
)
: null
if (err(wallArtifact)) return
if (wallArtifact) {
const sweep = getArtifactOfTypes(
{ key: wallArtifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(sweep)) return
const wallsWithDependencies = Array.from(
getArtifactsOfTypes(
{ keys: sweep.surfaceIds, types: ['wall', 'cap'] },
artifactGraph
).values()
).filter((wall) => wall?.pathIds?.length)
const wallIds = wallsWithDependencies.map((wall) => wall.id)
Object.entries(variables).forEach(([key, _var]) => {
if (
_var?.type === 'Face' &&
wallIds.includes(_var.value.artifactId)
) {
const pathToStartSketchOn = getNodePathFromSourceRange(
astClone,
_var.value.__meta[0].sourceRange
)
pathsDependingOnExtrude.push({
path: pathToStartSketchOn,
variable: _var,
})
}
if (
_var?.type === 'Sketch' &&
_var.value.on.type === 'face' &&
wallIds.includes(_var.value.on.artifactId)
) {
const pathToStartSketchOn = getNodePathFromSourceRange(
astClone,
_var.value.on.__meta[0].sourceRange
)
pathsDependingOnExtrude.push({
path: pathToStartSketchOn,
variable: {
type: 'Face',
value: _var.value.on,
},
})
}
})
}
for (const { path, variable } of pathsDependingOnExtrude) {
// `parentPipe` and `parentInit` are the exact same node, but because it could either be an array or on object node
// putting them in two different variables was the only way to get TypeScript to stop complaining
// the reason why we're grabbing the parent and the last key is because we want to mutate the ast
// so `parent[lastKey]` does the trick, if there's a better way of doing this I'm all years
const parentPipe = getNodeFromPath<PipeExpression['body']>(
astClone, astClone,
path.slice(0, -1) path.slice(0, -1)
) )
if (err(parent)) { const parentInit = getNodeFromPath<VariableDeclarator>(
astClone,
path.slice(0, -1)
)
if (err(parentPipe) || err(parentInit)) {
return return
} }
const sketchToPreserve = sketchFromKclValue( if (!variable) return new Error('Could not find sketch')
variables[sketchName], const artifactId =
sketchName variable.type === 'Sketch'
) ? variable.value.artifactId
if (err(sketchToPreserve)) return sketchToPreserve : variable.type === 'Face'
? variable.value.artifactId
: ''
if (!artifactId) return new Error('Sketch not on anything')
const onId =
variable.type === 'Sketch'
? variable.value.on.id
: variable.type === 'Face'
? variable.value.id
: ''
if (!onId) return new Error('Sketch not on anything')
// Can't kick off multiple requests at once as getFaceDetails // Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict // is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id) const faceDetails = await getFaceDetails(onId)
if ( if (
!( !(
faceDetails.origin && faceDetails.origin &&
@ -1515,14 +1629,20 @@ export async function deleteFromSelection(
) { ) {
return return
} }
const lastKey = Number(path.slice(-1)[0][0]) const lastKey = path.slice(-1)[0][0]
modificationDetails.push({ modificationDetails.push({
parent: parent.node, parentPipe: parentPipe.node,
parentInit: parentInit.node,
faceDetails, faceDetails,
lastKey, lastKey,
}) })
} }
for (const { parent, faceDetails, lastKey } of modificationDetails) { for (const {
parentInit,
parentPipe,
faceDetails,
lastKey,
} of modificationDetails) {
if ( if (
!( !(
faceDetails.origin && faceDetails.origin &&
@ -1533,7 +1653,7 @@ export async function deleteFromSelection(
) { ) {
continue continue
} }
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [ const expression = createCallExpressionStdLib('startSketchOn', [
createObjectExpression({ createObjectExpression({
plane: createObjectExpression({ plane: createObjectExpression({
origin: createObjectExpression({ origin: createObjectExpression({
@ -1559,6 +1679,14 @@ export async function deleteFromSelection(
}), }),
}), }),
]) ])
if (
parentInit.type === 'VariableDeclarator' &&
lastKey === 'init'
) {
parentInit[lastKey] = expression
} else if (isArray(parentPipe) && typeof lastKey === 'number') {
parentPipe[lastKey] = expression
}
} }
resolve(true) resolve(true)
})().catch(reportRejection) })().catch(reportRejection)
@ -1570,24 +1698,199 @@ export async function deleteFromSelection(
return deleteEdgeTreatment(astClone, selection) return deleteEdgeTreatment(astClone, selection)
} else if (varDec.node.init.type === 'PipeExpression') { } else if (varDec.node.init.type === 'PipeExpression') {
const pipeBody = varDec.node.init.body const pipeBody = varDec.node.init.body
const doNotDeleteProfileIfItHasBeenExtruded = !(
selection?.artifact?.type === 'segment' && selection?.artifact?.surfaceId
)
if ( if (
pipeBody[0].type === 'CallExpression' && pipeBody[0].type === 'CallExpression' &&
pipeBody[0].callee.name === 'startSketchOn' doNotDeleteProfileIfItHasBeenExtruded &&
(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
astClone.body.splice(varDecIndex, 1) astClone.body.splice(varDecIndex, 1)
return astClone return astClone
} }
} else if (
// single expression profiles
(varDec.node.init.type === 'CallExpressionKw' ||
varDec.node.init.type === 'CallExpression') &&
['circleThreePoint', 'circle'].includes(varDec.node.init.callee.name)
) {
const varDecIndex = varDec.shallowPath[1][0] as number
astClone.body.splice(varDecIndex, 1)
return astClone
} }
return new Error('Selection not recognised, could not delete') return new Error('Selection not recognised, could not delete')
} }
const nonCodeMetaEmpty = () => { export const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
} }
export const createLabeledArg = (name: string, arg: Expr): LabeledArg => { export function getInsertIndex(
return { label: createIdentifier(name), arg, type: 'LabeledArg' } 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,
}
}
export function createNodeFromExprSnippet(
strings: TemplateStringsArray,
...expressions: any[]
): Node<BodyItem> | Error {
const code = strings.reduce(
(acc, str, i) => acc + str + (expressions[i] || ''),
''
)
let program = parse(code)
if (err(program)) return program
const node = program.program?.body[0]
if (!node) return new Error('No node found')
return node
}
export const createLabeledArg = (label: string, arg: Expr): LabeledArg => {
return { label: createIdentifier(label), arg, type: 'LabeledArg' }
} }

View File

@ -5,9 +5,9 @@ import {
PathToNode, PathToNode,
Expr, Expr,
CallExpression, CallExpression,
PipeExpression,
VariableDeclarator, VariableDeclarator,
CallExpressionKw, CallExpressionKw,
ArtifactGraph,
} from 'lang/wasm' } from 'lang/wasm'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
@ -16,7 +16,6 @@ import {
createCallExpressionStdLib, createCallExpressionStdLib,
createObjectExpression, createObjectExpression,
createIdentifier, createIdentifier,
createPipeExpression,
findUniqueName, findUniqueName,
createVariableDeclaration, createVariableDeclaration,
} from 'lang/modifyAst' } from 'lang/modifyAst'
@ -26,14 +25,18 @@ import {
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
getEdgeTagCall, getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment' } from 'lang/modifyAst/addEdgeTreatment'
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
import { kclManager } from 'lib/singletons'
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),
axisOrEdge: string, axisOrEdge: string,
axis: string, axis: string,
edge: Selections edge: Selections,
artifactGraph: ArtifactGraph,
artifact?: Artifact
): ):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -41,6 +44,13 @@ export function revolveSketch(
pathToRevolveArg: PathToNode pathToRevolveArg: PathToNode
} }
| Error { | Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToSketchNode,
artifactGraph,
ast: kclManager.ast,
})
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
@ -82,29 +92,13 @@ export function revolveSketch(
generatedAxis = createLiteral(axis) 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,
pathToSketchNode, pathToSketchNode,
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const { const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
if (!generatedAxis) return new Error('Generated axis selection is missing.') if (!generatedAxis) return new Error('Generated axis selection is missing.')
@ -116,41 +110,16 @@ export function revolveSketch(
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 sketchIndexInPathToNode = const lastSketchNodePath =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] let sketchIndexInBody = Number(lastSketchNodePath[1][0])
let insertIndex = sketchIndexInBody if (typeof sketchIndexInBody !== 'number') {
return new Error('expected sketchIndexInBody to be a number')
if (typeof insertIndex !== 'number') }
return new Error('expected insertIndex to be a number')
// If an axis was selected in KCL, find the max index to insert the revolve command // If an axis was selected in KCL, find the max index to insert the revolve command
if (axisDeclaration) { if (axisDeclaration) {
@ -161,14 +130,14 @@ export function revolveSketch(
if (typeof axisIndex !== 'number') if (typeof axisIndex !== 'number')
return new Error('expected axisIndex to be a number') return new Error('expected axisIndex to be a number')
insertIndex = Math.max(insertIndex, axisIndex) sketchIndexInBody = Math.max(sketchIndexInBody, axisIndex)
} }
clonedAst.body.splice(insertIndex + 1, 0, VariableDeclaration) clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [ const pathToRevolveArg: PathToNode = [
['body', ''], ['body', ''],
[insertIndex + 1, 'index'], [sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],

View File

@ -582,7 +582,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
it('finds sketch001 and sketch002 pipes to be lofted', async () => { it('finds sketch001 and sketch002 pipes to be lofted', async () => {
const exampleCode = `sketch001 = startSketchOn('XZ') const exampleCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
plane001 = offsetPlane('XZ', 2) plane001 = offsetPlane('XZ', offset = 2)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 3 }, %) |> circle({ center = [0, 0], radius = 3 }, %)
` `

View File

@ -2,7 +2,6 @@ import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { import {
ArrayExpression, ArrayExpression,
ArtifactGraph,
BinaryExpression, BinaryExpression,
CallExpression, CallExpression,
CallExpressionKw, CallExpressionKw,
@ -22,6 +21,7 @@ import {
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
recast, recast,
ArtifactGraph,
kclSettings, kclSettings,
unitLenToUnitLength, unitLenToUnitLength,
unitAngToUnitAngle, unitAngToUnitAngle,
@ -37,13 +37,15 @@ import {
getConstraintType, getConstraintType,
} from './std/sketchcombos' } from './std/sketchcombos'
import { err, Reason } from 'lib/trap' import { err, Reason } from 'lib/trap'
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 { findKwArg } from './util' import { findKwArg } from './util'
import { codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes' import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
export const UNLABELED_ARG = 'unlabeled first arg'
export const ARG_INDEX_FIELD = 'arg index' export const ARG_INDEX_FIELD = 'arg index'
/** /**
@ -357,7 +359,13 @@ 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,
@ -409,7 +417,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 } return { modifiedAst: _ast, pathToReplaced, exprInsertIndex: index }
} }
const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution') const hasPipeSub = isTypeInValue(finVal as Expr, 'PipeSubstitution')
@ -518,8 +526,15 @@ 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(memVars[varName2], varName2)
if (err(sg2)) return sg2
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sg, sg2,
secondaryLine?.codeRef?.range secondaryLine?.codeRef?.range
) )
if (err(_segment)) return _segment if (err(_segment)) return _segment
@ -871,6 +886,59 @@ export function getObjExprProperty(
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
}
/** /**
* Given KCL, returns the settings annotation object if it exists. * Given KCL, returns the settings annotation object if it exists.
*/ */

View File

@ -82,6 +82,7 @@ function moreNodePathFromSourceRange(
return moreNodePathFromSourceRange(arg, sourceRange, path) return moreNodePathFromSourceRange(arg, sourceRange, path)
} }
} }
return path
} }
return path return path
} }

View File

@ -1,4 +1,5 @@
import { import {
Expr,
Artifact, Artifact,
ArtifactGraph, ArtifactGraph,
ArtifactId, ArtifactId,
@ -18,7 +19,8 @@ import {
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { codeManager } from 'lib/singletons' import { Cap, Plane, Wall } from 'wasm-lib/kcl/bindings/Artifact'
import { CapSubType } from 'wasm-lib/kcl/bindings/Artifact'
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm' export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
@ -37,10 +39,28 @@ export interface PlaneArtifactRich extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
export interface CapArtifactRich extends BaseArtifact {
type: 'cap'
subType: CapSubType
faceCodeRef: CodeRef
edgeCuts: Array<EdgeCut>
paths: Array<PathArtifact>
sweep?: SweepArtifact
}
export interface WallArtifactRich extends BaseArtifact {
type: 'wall'
id: ArtifactId
segment: PathArtifact
edgeCuts: Array<EdgeCut>
sweep: SweepArtifact
paths: Array<PathArtifact>
faceCodeRef: CodeRef
}
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 plane: PlaneArtifact | WallArtifact | CapArtifact
/** 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 */
@ -51,7 +71,7 @@ export interface PathArtifactRich extends BaseArtifact {
interface SegmentArtifactRich extends BaseArtifact { interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf?: WallArtifact surf: WallArtifact
edges: Array<SweepEdge> edges: Array<SweepEdge>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CodeRef codeRef: CodeRef
@ -151,6 +171,73 @@ export function expandPlane(
} }
} }
export function expandWall(
wall: WallArtifact,
artifactGraph: ArtifactGraph
): WallArtifactRich {
const { pathIds, sweepId: _s, edgeCutEdgeIds, ...keptProperties } = wall
const paths = pathIds?.length
? Array.from(
getArtifactsOfTypes(
{ keys: wall.pathIds, types: ['path'] },
artifactGraph
).values()
)
: []
const sweep = artifactGraph.get(wall.sweepId) as SweepArtifact
const edgeCuts = edgeCutEdgeIds?.length
? Array.from(
getArtifactsOfTypes(
{ keys: wall.edgeCutEdgeIds, types: ['edgeCut'] },
artifactGraph
).values()
)
: []
const segment = artifactGraph.get(wall.segId) as PathArtifact
return {
type: 'wall',
...keptProperties,
paths,
sweep,
segment,
edgeCuts,
}
}
export function expandCap(
cap: CapArtifact,
artifactGraph: ArtifactGraph
): CapArtifactRich {
const { pathIds, sweepId: _s, edgeCutEdgeIds, ...keptProperties } = cap
const paths = pathIds?.length
? Array.from(
getArtifactsOfTypes(
{ keys: cap.pathIds, types: ['path'] },
artifactGraph
).values()
)
: []
const maybeSweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
artifactGraph
)
const sweep = err(maybeSweep) ? undefined : maybeSweep
const edgeCuts = edgeCutEdgeIds?.length
? Array.from(
getArtifactsOfTypes(
{ keys: cap.edgeCutEdgeIds, types: ['edgeCut'] },
artifactGraph
).values()
)
: []
return {
type: 'cap',
...keptProperties,
paths,
sweep,
edgeCuts,
}
}
export function expandPath( export function expandPath(
path: PathArtifact, path: PathArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
@ -239,6 +326,7 @@ export function expandSegment(
if (err(path)) return path if (err(path)) return path
if (err(surf)) return surf if (err(surf)) return surf
if (err(edgeCut)) return edgeCut if (err(edgeCut)) return edgeCut
if (!surf) return new Error('Segment does not have a surface')
return { return {
type: 'segment', type: 'segment',
@ -410,6 +498,220 @@ 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: Solid2D,
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: SweepEdge, 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 (
// if the user selects a face with sketch on it (pathIds.length), they probably wanted to edit that sketch,
// not the sketch for the underlying sweep sketch
(artifact.type === 'wall' || artifact.type === 'cap') &&
artifact?.pathIds?.length
)
return artifact
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 onlyConsecutivePaths = (
orderedNodePaths: PathToNode[],
originalPath: PathToNode,
ast: Program
): PathToNode[] => {
const isExprSafe = (index: number, ast: Program): boolean => {
// we allow expressions between profiles, but only basic math expressions 5 + 6 etc
// because 5 + doSomeMath() might be okay, but we can't know if it's an abstraction on a stdlib
// call that involves a engine call, and we can't have that in sketch-mode/mock-execution
const expr = 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 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, ast)) {
break
}
}
for (let i = originalIndex - 1; i >= minIndex; i--) {
if (pathIndexMap[i]) {
safePaths.unshift(pathIndexMap[i])
} else if (!isExprSafe(i, ast)) {
break
}
}
return safePaths
}
export function getPathsFromPlaneArtifact(
planeArtifact: PlaneArtifact,
artifactGraph: ArtifactGraph,
ast: Program
): PathToNode[] {
const nodePaths: PathToNode[] = []
for (const pathId of planeArtifact.pathIds) {
const path = 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(ast, path.codeRef.range)
: path.codeRef.pathToNode
)
}
}
return onlyConsecutivePaths(nodePaths, nodePaths[0], ast)
}
export function getPathsFromArtifact({
sketchPathToNode,
artifact,
artifactGraph,
ast,
}: {
sketchPathToNode: PathToNode
artifact?: Artifact
artifactGraph: ArtifactGraph
ast: Program
}): PathToNode[] | Error {
const plane = getPlaneFromArtifact(artifact, artifactGraph)
if (err(plane)) return plane
const paths = getArtifactsOfTypes(
{ keys: plane.pathIds, types: ['path'] },
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, ast)
}
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
*/ */
@ -418,12 +720,24 @@ 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) { const codeRef = getFaceCodeRef(artifact)
if (codeRef) {
const match = const match =
artifact.codeRef?.range[0] === range[0] && codeRef?.range[0] === range[0] && codeRef.range[1] === range[1]
artifact.codeRef.range[1] === range[1]
if (match) return artifact if (match) return artifact
} }
} }
return null return null
} }
export function getFaceCodeRef(
artifact: Artifact | Plane | Wall | Cap
): CodeRef | null {
if ('faceCodeRef' in artifact) {
return artifact.faceCodeRef
}
if ('codeRef' in artifact) {
return artifact.codeRef
}
return null
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

@ -30,6 +30,7 @@ import { toolTips, ToolTip } from 'lang/langHelpers'
import { import {
createPipeExpression, createPipeExpression,
mutateKwArg, mutateKwArg,
nonCodeMetaEmpty,
splitPathAtPipeExpression, splitPathAtPipeExpression,
} from '../modifyAst' } from '../modifyAst'
@ -66,7 +67,12 @@ import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine' import { EdgeCutInfo } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg, findKwArgAny, findKwArgAnyIndex } from 'lang/util' import {
findKwArg,
findKwArgWithIndex,
findKwArgAny,
findKwArgAnyIndex,
} from 'lang/util'
export const ARG_TAG = 'tag' export const ARG_TAG = 'tag'
export const ARG_END = 'end' export const ARG_END = 'end'
@ -76,6 +82,9 @@ const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"' 'Invalid input, expected "straight-segment"'
) )
const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"') const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error(
'Invalid input, expected "circle-three-point-segment"'
)
export type Coords2d = [number, number] export type Coords2d = [number, number]
@ -171,7 +180,8 @@ const commonConstraintInfoHelper = (
} }
], ],
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
) => { ) => {
if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw') if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw')
return [] return []
@ -295,7 +305,8 @@ const horzVertConstraintInfoHelper = (
stdLibFnName: ConstrainInfo['stdLibFnName'], stdLibFnName: ConstrainInfo['stdLibFnName'],
abbreviatedInput: AbbreviatedInput, abbreviatedInput: AbbreviatedInput,
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
) => { ) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
const firstArg = callExp.arguments?.[0] const firstArg = callExp.arguments?.[0]
@ -502,13 +513,14 @@ export const lineTo: SketchLineHelperKw = {
}) => { }) => {
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 = { ...node } const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>( const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>(
_node, _node,
pathToNode, pathToNode,
'PipeExpression' 'PipeExpression'
) )
if (err(nodeMeta)) return nodeMeta if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta const { node: pipe } = nodeMeta
const nodeMeta2 = getNodeFromPath<VariableDeclarator>( const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
_node, _node,
@ -783,11 +795,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 = { ...node } const _node = structuredClone(node)
const getNode = getNodeFromPathCurry(_node, pathToNode) const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression') const varDec = getNode<VariableDeclaration>('VariableDeclaration')
if (err(_node1)) return _node1 if (err(varDec)) return varDec
const { node: pipe } = _node1 const dec = varDec.node.declaration
const newVal = createLiteral(roundOff(to[0] - from[0], 2)) const newVal = createLiteral(roundOff(to[0] - from[0], 2))
@ -802,7 +814,11 @@ export const xLine: SketchLineHelper = {
]) ])
if (err(result)) return result if (err(result)) return result
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp if (dec.init.type === 'PipeExpression') {
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
@ -814,7 +830,11 @@ export const xLine: SketchLineHelper = {
newVal, newVal,
createPipeSubstitution(), createPipeSubstitution(),
]) ])
pipe.body = [...pipe.body, newLine] if (dec.init.type === 'PipeExpression') {
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 }) => {
@ -851,11 +871,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 = { ...node } const _node = structuredClone(node)
const getNode = getNodeFromPathCurry(_node, pathToNode) const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression') const varDec = getNode<VariableDeclaration>('VariableDeclaration')
if (err(_node1)) return _node1 if (err(varDec)) return varDec
const { node: pipe } = _node1 const dec = varDec.node.declaration
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)
@ -868,7 +888,11 @@ export const yLine: SketchLineHelper = {
]) ])
if (err(result)) return result if (err(result)) return result
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp if (dec.init.type === 'PipeExpression') {
dec.init.body[callIndex] = callExp
} else {
dec.init = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
pathToNode, pathToNode,
@ -880,7 +904,11 @@ export const yLine: SketchLineHelper = {
newVal, newVal,
createPipeSubstitution(), createPipeSubstitution(),
]) ])
pipe.body = [...pipe.body, newLine] if (dec.init.type === 'PipeExpression') {
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 }) => {
@ -1220,6 +1248,295 @@ export const circle: SketchLineHelper = {
] ]
}, },
} }
export const circleThreePoint: SketchLineHelperKw = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'circle-three-point-segment') {
return CIRCLE_THREE_POINT_SEGMENT_ERR
}
const { p1, p2, p3 } = segmentInput
const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<VariableDeclaration>(
_node,
pathToNode,
'VariableDeclaration'
)
if (err(nodeMeta)) return nodeMeta
const { node: varDec } = nodeMeta
const createRoundedLiteral = (val: number) =>
createLiteral(roundOff(val, 2))
if (replaceExistingCallback) {
const result = replaceExistingCallback([
{
type: 'arrayInObject',
index: 0,
key: 'p1',
argType: 'xAbsolute',
expr: createRoundedLiteral(p1[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p1',
argType: 'yAbsolute',
expr: createRoundedLiteral(p1[1]),
},
{
type: 'arrayInObject',
index: 0,
key: 'p2',
argType: 'xAbsolute',
expr: createRoundedLiteral(p2[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p2',
argType: 'yAbsolute',
expr: createRoundedLiteral(p2[1]),
},
{
type: 'arrayInObject',
index: 0,
key: 'p3',
argType: 'xAbsolute',
expr: createRoundedLiteral(p3[0]),
},
{
type: 'arrayInObject',
index: 1,
key: 'p3',
argType: 'yAbsolute',
expr: createRoundedLiteral(p3[1]),
},
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
varDec.declaration.init = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
}
return new Error('replaceExistingCallback is missing')
},
updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'circle-three-point-segment') {
return CIRCLE_THREE_POINT_SEGMENT_ERR
}
const { p1, p2, p3 } = input
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression, shallowPath } = nodeMeta
const createRounded2DPointArr = (point: [number, number]) =>
createArrayExpression([
createLiteral(roundOff(point[0], 2)),
createLiteral(roundOff(point[1], 2)),
])
const newP1 = createRounded2DPointArr(p1)
const newP2 = createRounded2DPointArr(p2)
const newP3 = createRounded2DPointArr(p3)
mutateKwArg('p1', callExpression, newP1)
mutateKwArg('p2', callExpression, newP2)
mutateKwArg('p3', callExpression, newP3)
return {
modifiedAst: _node,
pathToNode: shallowPath,
}
},
getTag: getTagKwArg(),
addTag: addTagKw(),
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
if (callExp.type !== 'CallExpressionKw') return []
const p1Details = findKwArgWithIndex('p1', callExp)
const p2Details = findKwArgWithIndex('p2', callExp)
const p3Details = findKwArgWithIndex('p3', callExp)
if (!p1Details || !p2Details || !p3Details) return []
if (
p1Details.expr.type !== 'ArrayExpression' ||
p2Details.expr.type !== 'ArrayExpression' ||
p3Details.expr.type !== 'ArrayExpression'
)
return []
const pathToP1ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p1Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP2ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p2Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP3ArrayExpression: PathToNode = [
...pathToNode,
['arguments', 'CallExpressionKw'],
[p3Details.argIndex, 'arg index'],
['arg', 'labeledArg -> Arg'],
['elements', 'ArrayExpression'],
]
const pathToP1XArg: PathToNode = [...pathToP1ArrayExpression, [0, 'index']]
const pathToP1YArg: PathToNode = [...pathToP1ArrayExpression, [1, 'index']]
const pathToP2XArg: PathToNode = [...pathToP2ArrayExpression, [0, 'index']]
const pathToP2YArg: PathToNode = [...pathToP2ArrayExpression, [1, 'index']]
const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']]
const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']]
const constraints: (ConstrainInfo & { filterValue: string })[] = [
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[0]),
sourceRange: [
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end,
0,
],
pathToNode: pathToP1XArg,
value: code.slice(
p1Details.expr.elements[0].start,
p1Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p1',
},
filterValue: 'p1',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[1]),
sourceRange: [
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end,
0,
],
pathToNode: pathToP1YArg,
value: code.slice(
p1Details.expr.elements[1].start,
p1Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p1',
},
filterValue: 'p1',
},
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[0]),
sourceRange: [
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end,
0,
],
pathToNode: pathToP2XArg,
value: code.slice(
p2Details.expr.elements[0].start,
p2Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p2',
},
filterValue: 'p2',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[1]),
sourceRange: [
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end,
0,
],
pathToNode: pathToP2YArg,
value: code.slice(
p2Details.expr.elements[1].start,
p2Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p2',
},
filterValue: 'p2',
},
{
stdLibFnName: 'circleThreePoint',
type: 'xAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[0]),
sourceRange: [
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end,
0,
],
pathToNode: pathToP3XArg,
value: code.slice(
p3Details.expr.elements[0].start,
p3Details.expr.elements[0].end
),
argPosition: {
type: 'arrayInObject',
index: 0,
key: 'p3',
},
filterValue: 'p3',
},
{
stdLibFnName: 'circleThreePoint',
type: 'yAbsolute',
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[1]),
sourceRange: [
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end,
0,
],
pathToNode: pathToP3YArg,
value: code.slice(
p3Details.expr.elements[1].start,
p3Details.expr.elements[1].end
),
argPosition: {
type: 'arrayInObject',
index: 1,
key: 'p3',
},
filterValue: 'p3',
},
]
const finalConstraints: ConstrainInfo[] = []
constraints.forEach((constraint) => {
if (!filterValue) {
finalConstraints.push(constraint)
}
if (filterValue && constraint.filterValue === filterValue) {
finalConstraints.push(constraint)
}
})
return finalConstraints
},
}
export const angledLine: SketchLineHelper = { export const angledLine: 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
@ -1984,6 +2301,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = { export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = {
line, line,
lineTo, lineTo,
circleThreePoint,
} as const } as const
export function changeSketchArguments( export function changeSketchArguments(
@ -2051,30 +2369,36 @@ export function changeSketchArguments(
export function getConstraintInfo( export function getConstraintInfo(
callExpression: Node<CallExpression>, callExpression: Node<CallExpression>,
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
): ConstrainInfo[] { ): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || '' const fnName = callExpression?.callee?.name || ''
if (!(fnName in sketchLineHelperMap)) return [] if (!(fnName in sketchLineHelperMap)) return []
return sketchLineHelperMap[fnName].getConstraintInfo( return sketchLineHelperMap[fnName].getConstraintInfo(
callExpression, callExpression,
code, code,
pathToNode pathToNode,
filterValue
) )
} }
export function getConstraintInfoKw( export function getConstraintInfoKw(
callExpression: Node<CallExpressionKw>, callExpression: Node<CallExpressionKw>,
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
): ConstrainInfo[] { ): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || '' const fnName = callExpression?.callee?.name || ''
const isAbsolute = findKwArg('endAbsolute', callExpression) !== undefined const isAbsolute =
fnName === 'circleThreePoint' ||
findKwArg('endAbsolute', callExpression) !== undefined
if (!(fnName in sketchLineHelperMapKw)) return [] if (!(fnName in sketchLineHelperMapKw)) return []
const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName
return sketchLineHelperMapKw[correctFnName].getConstraintInfo( return sketchLineHelperMapKw[correctFnName].getConstraintInfo(
callExpression, callExpression,
code, code,
pathToNode pathToNode,
filterValue
) )
} }
@ -2298,8 +2622,6 @@ 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
@ -2380,7 +2702,6 @@ 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,
@ -2509,6 +2830,7 @@ function addTagKw(): addTagFn {
unlabeled: callExpr.node.arguments.length unlabeled: callExpr.node.arguments.length
? callExpr.node.arguments[0] ? callExpr.node.arguments[0]
: null, : null,
nonCodeMeta: nonCodeMetaEmpty(),
arguments: [], arguments: [],
} }
const tagArg = findKwArg(ARG_TAG, primaryCallExp) const tagArg = findKwArg(ARG_TAG, primaryCallExp)
@ -2719,6 +3041,8 @@ export function isAbsoluteLine(lineCall: CallExpressionKw): boolean | Error {
return new Error( return new Error(
`line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params` `line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params`
) )
case 'circleThreePoint':
return false
} }
return new Error(`Unknown sketch function ${name}`) return new Error(`Unknown sketch function ${name}`)
} }

View File

@ -21,7 +21,6 @@ import {
Literal, Literal,
SourceRange, SourceRange,
LiteralValue, LiteralValue,
recast,
LabeledArg, LabeledArg,
VariableMap, VariableMap,
} from '../wasm' } from '../wasm'
@ -217,14 +216,19 @@ function createStdlibCallExpressionKw(
tool: ToolTip, tool: ToolTip,
labeled: LabeledArg[], labeled: LabeledArg[],
tag?: Expr, tag?: Expr,
valueUsedInTransform?: number valueUsedInTransform?: number,
unlabeled?: Expr
): CreatedSketchExprResult { ): CreatedSketchExprResult {
const args = labeled const args = labeled
if (tag) { if (tag) {
args.push(createLabeledArg(ARG_TAG, tag)) args.push(createLabeledArg(ARG_TAG, tag))
} }
return { return {
callExp: createCallExpressionStdLibKw(tool, null, args), callExp: createCallExpressionStdLibKw(
tool,
unlabeled ? unlabeled : null,
args
),
valueUsedInTransform, valueUsedInTransform,
} }
} }
@ -1306,6 +1310,12 @@ export function getRemoveConstraintsTransform(
}, },
} }
if (
sketchFnExp.type === 'CallExpressionKw' &&
sketchFnExp.callee.name === 'circleThreePoint'
) {
return false
}
const isAbsolute = const isAbsolute =
// isAbsolute doesn't matter if the call is positional. // isAbsolute doesn't matter if the call is positional.
sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp) sketchFnExp.type === 'CallExpression' ? false : isAbsoluteLine(sketchFnExp)
@ -1320,7 +1330,6 @@ export function getRemoveConstraintsTransform(
? getFirstArg(sketchFnExp) ? getFirstArg(sketchFnExp)
: getArgForEnd(sketchFnExp) : getArgForEnd(sketchFnExp)
if (err(firstArg)) { if (err(firstArg)) {
console.error(firstArg)
return false return false
} }
@ -1351,7 +1360,7 @@ export function getRemoveConstraintsTransform(
export function removeSingleConstraint({ export function removeSingleConstraint({
pathToCallExp, pathToCallExp,
inputDetails, inputDetails: inputToReplace,
ast, ast,
}: { }: {
pathToCallExp: PathToNode pathToCallExp: PathToNode
@ -1384,12 +1393,12 @@ export function removeSingleConstraint({
// So we should update the call expression to use the inputs, except for // So we should update the call expression to use the inputs, except for
// the inputDetails, input where we should use the rawValue(s) // the inputDetails, input where we should use the rawValue(s)
if (inputDetails.type === 'arrayItem') { if (inputToReplace.type === 'arrayItem') {
const values = inputs.map((arg) => { const values = inputs.map((arg) => {
if ( if (
!( !(
(arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') && (arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') &&
arg.index === inputDetails.index arg.index === inputToReplace.index
) )
) )
return arg.expr return arg.expr
@ -1397,9 +1406,9 @@ export function removeSingleConstraint({
(rawValue) => (rawValue) =>
(rawValue.type === 'arrayItem' || (rawValue.type === 'arrayItem' ||
rawValue.type === 'arrayOrObjItem') && rawValue.type === 'arrayOrObjItem') &&
rawValue.index === inputDetails.index rawValue.index === inputToReplace.index
)?.expr )?.expr
return (arg.index === inputDetails.index && literal) || arg.expr return (arg.index === inputToReplace.index && literal) || arg.expr
}) })
if (callExp.node.type === 'CallExpression') { if (callExp.node.type === 'CallExpression') {
return createStdlibCallExpression( return createStdlibCallExpression(
@ -1428,66 +1437,110 @@ export function removeSingleConstraint({
} }
} }
if ( if (
inputDetails.type === 'arrayInObject' || inputToReplace.type === 'arrayInObject' ||
inputDetails.type === 'objectProperty' inputToReplace.type === 'objectProperty'
) { ) {
const arrayDetailsNameBetterLater: { const arrayInput: {
[key: string]: Parameters<typeof createArrayExpression>[0] [key: string]: Parameters<typeof createArrayExpression>[0]
} = {} } = {}
const otherThing: Parameters<typeof createObjectExpression>[0] = {} const objInput: Parameters<typeof createObjectExpression>[0] = {}
inputs.forEach((arg) => { const kwArgInput: ReturnType<typeof createLabeledArg>[] = []
inputs.forEach((currentArg) => {
if ( if (
arg.type !== 'objectProperty' && // should be one of these, return early to make TS happy.
arg.type !== 'arrayOrObjItem' && currentArg.type !== 'objectProperty' &&
arg.type !== 'arrayInObject' currentArg.type !== 'arrayOrObjItem' &&
currentArg.type !== 'arrayInObject'
) )
return return
const rawLiteralArrayInObject = rawArgs.find( const rawLiteralArrayInObject = rawArgs.find(
(rawValue) => (rawValue) =>
rawValue.type === 'arrayInObject' && rawValue.type === 'arrayInObject' &&
rawValue.key === inputDetails.key && rawValue.key === currentArg.key &&
rawValue.index === (arg.type === 'arrayInObject' ? arg.index : -1) rawValue.index ===
(currentArg.type === 'arrayInObject' ? currentArg.index : -1)
) )
const rawLiteralObjProp = rawArgs.find( const rawLiteralObjProp = rawArgs.find(
(rawValue) => (rawValue) =>
(rawValue.type === 'objectProperty' || (rawValue.type === 'objectProperty' ||
rawValue.type === 'arrayOrObjItem' || rawValue.type === 'arrayOrObjItem' ||
rawValue.type === 'arrayInObject') && rawValue.type === 'arrayInObject') &&
rawValue.key === inputDetails.key rawValue.key === inputToReplace.key
) )
if ( if (
inputDetails.type === 'arrayInObject' && inputToReplace.type === 'arrayInObject' &&
rawLiteralArrayInObject?.type === 'arrayInObject' && rawLiteralArrayInObject?.type === 'arrayInObject' &&
rawLiteralArrayInObject?.index === inputDetails.index && rawLiteralArrayInObject?.index === inputToReplace.index &&
rawLiteralArrayInObject?.key === inputDetails.key rawLiteralArrayInObject?.key === inputToReplace.key
) { ) {
if (!arrayDetailsNameBetterLater[arg.key]) if (!arrayInput[currentArg.key]) {
arrayDetailsNameBetterLater[arg.key] = [] arrayInput[currentArg.key] = []
arrayDetailsNameBetterLater[inputDetails.key][inputDetails.index] = }
arrayInput[inputToReplace.key][inputToReplace.index] =
rawLiteralArrayInObject.expr rawLiteralArrayInObject.expr
let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key
)
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
currentArg.key,
createArrayExpression([])
)
kwArgInput.push(existingKwgForKey)
}
if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[inputToReplace.index] =
rawLiteralArrayInObject.expr
}
} else if ( } else if (
inputDetails.type === 'objectProperty' && inputToReplace.type === 'objectProperty' &&
(rawLiteralObjProp?.type === 'objectProperty' || (rawLiteralObjProp?.type === 'objectProperty' ||
rawLiteralObjProp?.type === 'arrayOrObjItem') && rawLiteralObjProp?.type === 'arrayOrObjItem') &&
rawLiteralObjProp?.key === inputDetails.key && rawLiteralObjProp?.key === inputToReplace.key &&
arg.key === inputDetails.key currentArg.key === inputToReplace.key
) { ) {
otherThing[inputDetails.key] = rawLiteralObjProp.expr objInput[inputToReplace.key] = rawLiteralObjProp.expr
} else if (arg.type === 'arrayInObject') { } else if (currentArg.type === 'arrayInObject') {
if (!arrayDetailsNameBetterLater[arg.key]) if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = []
arrayDetailsNameBetterLater[arg.key] = [] arrayInput[currentArg.key][currentArg.index] = currentArg.expr
arrayDetailsNameBetterLater[arg.key][arg.index] = arg.expr let existingKwgForKey = kwArgInput.find(
} else if (arg.type === 'objectProperty') { (kwArg) => kwArg.label.name === currentArg.key
otherThing[arg.key] = arg.expr )
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
currentArg.key,
createArrayExpression([])
)
kwArgInput.push(existingKwgForKey)
}
if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[currentArg.index] = currentArg.expr
}
} else if (currentArg.type === 'objectProperty') {
objInput[currentArg.key] = currentArg.expr
} }
}) })
const createObjParam: Parameters<typeof createObjectExpression>[0] = {} const createObjParam: Parameters<typeof createObjectExpression>[0] = {}
Object.entries(arrayDetailsNameBetterLater).forEach(([key, value]) => { Object.entries(arrayInput).forEach(([key, value]) => {
createObjParam[key] = createArrayExpression(value) createObjParam[key] = createArrayExpression(value)
}) })
if (
callExp.node.callee.name === 'circleThreePoint' &&
callExp.node.type === 'CallExpressionKw'
) {
// it's kwarg
const inputPlane = callExp.node.unlabeled as Expr
return createStdlibCallExpressionKw(
callExp.node.callee.name as any,
kwArgInput,
tag,
undefined,
inputPlane
)
}
const objExp = createObjectExpression({ const objExp = createObjectExpression({
...createObjParam, ...createObjParam,
...otherThing, ...objInput,
}) })
return createStdlibCallExpression( return createStdlibCallExpression(
callExp.node.callee.name as any, callExp.node.callee.name as any,
@ -1571,6 +1624,16 @@ function getTransformMapPathKw(
} }
| false { | false {
const name = sketchFnExp.callee.name as ToolTip const name = sketchFnExp.callee.name as ToolTip
if (name === 'circleThreePoint') {
const info = transformMap?.circleThreePoint?.free?.[constraintType]
if (info)
return {
toolTip: 'circleThreePoint',
lineInputType: 'free',
constraintType,
}
return false
}
const isAbsolute = findKwArg(ARG_END_ABSOLUTE, sketchFnExp) !== undefined const isAbsolute = findKwArg(ARG_END_ABSOLUTE, sketchFnExp) !== undefined
const nameAbsolute = name === 'line' ? 'lineTo' : name const nameAbsolute = name === 'line' ? 'lineTo' : name
if (!toolTips.includes(name)) { if (!toolTips.includes(name)) {
@ -1989,6 +2052,13 @@ export function transformAstSketchLines({
radius: seg.radius, radius: seg.radius,
from, from,
} }
: seg.type === 'CircleThreePoint'
? {
type: 'circle-three-point-segment',
p1: seg.p1,
p2: seg.p2,
p3: seg.p3,
}
: { : {
type: 'straight-segment', type: 'straight-segment',
to, to,

View File

@ -45,6 +45,13 @@ interface ArcSegmentInput {
center: [number, number] center: [number, number]
radius: number radius: number
} }
/** Inputs for three point circle */
interface CircleThreePointSegmentInput {
type: 'circle-three-point-segment'
p1: [number, number]
p2: [number, number]
p3: [number, number]
}
/** /**
* SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput. * SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput.
@ -52,7 +59,10 @@ interface ArcSegmentInput {
* - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to). * - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to).
* - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius. * - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius.
*/ */
export type SegmentInputs = StraightSegmentInput | ArcSegmentInput export type SegmentInputs =
| StraightSegmentInput
| ArcSegmentInput
| CircleThreePointSegmentInput
/** /**
* Interface for adding or replacing a sketch stblib call expression to a sketch. * Interface for adding or replacing a sketch stblib call expression to a sketch.
@ -85,6 +95,9 @@ export type InputArgKeys =
| 'intersectTag' | 'intersectTag'
| 'radius' | 'radius'
| 'center' | 'center'
| 'p1'
| 'p2'
| 'p3'
export interface SingleValueInput<T> { export interface SingleValueInput<T> {
type: 'singleValue' type: 'singleValue'
argType: LineInputsType argType: LineInputsType
@ -239,7 +252,8 @@ export interface SketchLineHelper {
getConstraintInfo: ( getConstraintInfo: (
callExp: Node<CallExpression>, callExp: Node<CallExpression>,
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
) => ConstrainInfo[] ) => ConstrainInfo[]
} }
@ -267,6 +281,7 @@ export interface SketchLineHelperKw {
getConstraintInfo: ( getConstraintInfo: (
callExp: Node<CallExpressionKw>, callExp: Node<CallExpressionKw>,
code: string, code: string,
pathToNode: PathToNode pathToNode: PathToNode,
filterValue?: string
) => ConstrainInfo[] ) => ConstrainInfo[]
} }

View File

@ -11,23 +11,50 @@ import {
LiteralValue, LiteralValue,
NumericSuffix, NumericSuffix,
} from './wasm' } from './wasm'
import { filterArtifacts } from 'lang/std/artifactGraph' import { filterArtifacts, getFaceCodeRef } from 'lang/std/artifactGraph'
import { isArray, isOverlap } from 'lib/utils' import { isArray, isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap( /**
oldPath: PathToNode, * Updates pathToNode body indices to account for the insertion of an expression
pathToNodeMap: { [key: number]: PathToNode } * PathToNode expression is after the insertion index, that the body index is incremented
* Negative insertion index means no insertion
*/
export function updatePathToNodePostExprInjection(
pathToNode: PathToNode,
exprInsertIndex: number
): PathToNode { ): PathToNode {
const updatedPathToNode = structuredClone(oldPath) if (exprInsertIndex < 0) return pathToNode
let max = 0 const bodyIndex = Number(pathToNode[1][0])
Object.values(pathToNodeMap).forEach((path) => { if (bodyIndex < exprInsertIndex) return pathToNode
const index = Number(path[1][0]) const clone = structuredClone(pathToNode)
if (index > max) { clone[1][0] = bodyIndex + 1
max = index return clone
}
export function updateSketchDetailsNodePaths({
sketchEntryNodePath,
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
),
} }
})
updatedPathToNode[1][0] = max
return updatedPathToNode
} }
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
@ -36,20 +63,30 @@ export function isCursorInSketchCommandRange(
): string | false { ): string | false {
const overlappingEntries = filterArtifacts( const overlappingEntries = filterArtifacts(
{ {
types: ['segment', 'path'], types: ['segment', 'path', 'plane', 'cap', 'wall'],
predicate: (artifact) => { predicate: (artifact) => {
const codeRefRange = getFaceCodeRef(artifact)?.range
return selectionRanges.graphSelections.some( return selectionRanges.graphSelections.some(
(selection) => (selection) =>
isArray(selection?.codeRef?.range) && isArray(selection?.codeRef?.range) &&
isArray(artifact?.codeRef?.range) && isArray(codeRefRange) &&
isOverlap(selection?.codeRef?.range, artifact.codeRef.range) isOverlap(selection?.codeRef?.range, codeRefRange)
) )
}, },
}, },
artifactGraph artifactGraph
) )
const firstEntry = [...overlappingEntries.values()]?.[0] const firstEntry = [...overlappingEntries.values()]?.[0]
const parentId = firstEntry?.type === 'segment' ? firstEntry.pathId : false const parentId =
firstEntry?.type === 'segment'
? firstEntry.pathId
: ((firstEntry?.type === 'plane' ||
firstEntry?.type === 'cap' ||
firstEntry?.type === 'wall') &&
firstEntry.pathIds?.length) ||
false
? firstEntry.pathIds[0]
: false
return parentId return parentId
? parentId ? parentId
@ -81,11 +118,27 @@ export function findKwArg(
label: string, label: string,
call: CallExpressionKw call: CallExpressionKw
): Expr | undefined { ): Expr | undefined {
return call.arguments.find((arg) => { return call?.arguments?.find((arg) => {
return arg.label.name === label return arg.label.name === label
})?.arg })?.arg
} }
/**
Search the keyword arguments from a call for an argument with this label,
returns the index of the argument as well.
*/
export function findKwArgWithIndex(
label: string,
call: CallExpressionKw
): { expr: Expr; argIndex: number } | undefined {
const index = call.arguments.findIndex((arg) => {
return arg.label.name === label
})
return index >= 0
? { expr: call.arguments[index].arg, argIndex: index }
: undefined
}
/** /**
Search the keyword arguments from a call for an argument with one of these labels. Search the keyword arguments from a call for an argument with one of these labels.
*/ */

View File

@ -58,7 +58,7 @@ export type ModelingCommandSchema = {
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axisOrEdge: string axisOrEdge: 'Axis' | 'Edge'
axis: string axis: string
edge: Selections edge: Selections
} }

View File

@ -14,6 +14,7 @@ describe('KCL expression calculations', () => {
variables['x'] = { variables['x'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue('1 + x', variables) const actual = await getCalculatedKclExpressionValue('1 + x', variables)
@ -32,6 +33,7 @@ describe('KCL expression calculations', () => {
variables['y'] = { variables['y'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue('1 + x', variables) const actual = await getCalculatedKclExpressionValue('1 + x', variables)
@ -44,6 +46,7 @@ describe('KCL expression calculations', () => {
variables['x'] = { variables['x'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue( const actual = await getCalculatedKclExpressionValue(

View File

@ -126,8 +126,7 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
if ( if (
operation.type !== 'StdLibCall' || operation.type !== 'StdLibCall' ||
!operation.labeledArgs || !operation.labeledArgs ||
!('std_plane' in operation.labeledArgs) || !operation.unlabeledArg ||
!operation.labeledArgs.std_plane ||
!('offset' in operation.labeledArgs) || !('offset' in operation.labeledArgs) ||
!operation.labeledArgs.offset !operation.labeledArgs.offset
) { ) {
@ -135,11 +134,9 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
} }
// TODO: Implement conversion to arbitrary plane selection // TODO: Implement conversion to arbitrary plane selection
// once the Offset Plane command supports it. // once the Offset Plane command supports it.
const stdPlane = operation.unlabeledArg
const planeName = codeManager.code const planeName = codeManager.code
.slice( .slice(stdPlane.sourceRange[0], stdPlane.sourceRange[1])
operation.labeledArgs.std_plane.sourceRange[0],
operation.labeledArgs.std_plane.sourceRange[1]
)
.replaceAll(`'`, ``) .replaceAll(`'`, ``)
if (!isDefaultPlaneStr(planeName)) { if (!isDefaultPlaneStr(planeName)) {

View File

@ -75,9 +75,9 @@ segAng(rectangleSegmentA001),
// ast is edited in place from the updateCenterRectangleSketch // ast is edited in place from the updateCenterRectangleSketch
const expectedSourceCode = `sketch001 = startSketchOn('XZ') const expectedSourceCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([80, 120], %) |> startProfileAt([120.37, 80], %)
|> angledLine([0, 80], %, $rectangleSegmentA001) |> angledLine([0, 0], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) + 90, 120], %, $rectangleSegmentB001) |> angledLine([segAng(rectangleSegmentA001) + 90, 0], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001)

View File

@ -37,7 +37,7 @@ import {
*/ */
export const getRectangleCallExpressions = ( export const getRectangleCallExpressions = (
rectangleOrigin: [number, number], rectangleOrigin: [number, number],
tags: [string, string, string] tag: string
) => [ ) => [
createCallExpressionStdLib('angledLine', [ createCallExpressionStdLib('angledLine', [
createArrayExpression([ createArrayExpression([
@ -45,30 +45,28 @@ 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(tags[0]), createTagDeclarator(tag),
]), ]),
createCallExpressionStdLib('angledLine', [ createCallExpressionStdLib('angledLine', [
createArrayExpression([ createArrayExpression([
createBinaryExpression([ createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tags[0])]), createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
'+', '+',
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(tags[0])]), // same angle as the first line createCallExpressionStdLib('segAng', [createIdentifier(tag)]), // same angle as the first line
createUnaryExpression( createUnaryExpression(
createCallExpressionStdLib('segLen', [createIdentifier(tags[0])]), createCallExpressionStdLib('segLen', [createIdentifier(tag)]),
'-' '-'
), // negative height ), // negative height
]), ]),
createPipeSubstitution(), createPipeSubstitution(),
createTagDeclarator(tags[2]),
]), ]),
createCallExpressionStdLibKw('line', null, [ createCallExpressionStdLibKw('line', null, [
createLabeledArg( createLabeledArg(
@ -95,12 +93,12 @@ export function updateRectangleSketch(
y: number, y: number,
tag: string tag: string
) { ) {
;((pipeExpression.body[2] as CallExpression) ;((pipeExpression.body[1] 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[3] as CallExpression) ;((pipeExpression.body[2] as CallExpression)
.arguments[0] as ArrayExpression) = createArrayExpression([ .arguments[0] as ArrayExpression) = createArrayExpression([
createBinaryExpression([ createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]), createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
@ -129,8 +127,7 @@ export function updateCenterRectangleSketch(
let startX = originX - Math.abs(deltaX) let startX = originX - Math.abs(deltaX)
let startY = originY - Math.abs(deltaY) let startY = originY - Math.abs(deltaY)
// 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)) {
@ -144,7 +141,7 @@ export function updateCenterRectangleSketch(
const twoX = deltaX * 2 const twoX = deltaX * 2
const twoY = deltaY * 2 const twoY = deltaY * 2
callExpression = pipeExpression.body[2] 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)) {
@ -160,7 +157,7 @@ export function updateCenterRectangleSketch(
} }
} }
callExpression = pipeExpression.body[3] 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)) {

View File

@ -40,6 +40,7 @@ import {
CodeRef, CodeRef,
getCodeRefsByArtifactId, getCodeRefsByArtifactId,
ArtifactId, ArtifactId,
getFaceCodeRef,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { DefaultPlaneStr } from './planes' import { DefaultPlaneStr } from './planes'
@ -276,18 +277,19 @@ 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)
const codeRefs = getCodeRefsByArtifactId( if (!artifact) return null
id, const node = getNodeFromPath<Expr>(kclManager.ast, group.userData.pathToNode)
engineCommandManager.artifactGraph if (err(node)) return null
)
if (!artifact || !codeRefs) return null
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
selectionType: 'singleCodeCursor', selectionType: 'singleCodeCursor',
selection: { selection: {
artifact, artifact,
codeRef: codeRefs[0], codeRef: {
pathToNode: group?.userData?.pathToNode,
range: [node.node.start, node.node.end, 0],
},
}, },
}, },
} }
@ -572,8 +574,7 @@ export function getSelectionTypeDisplayText(
const selectionsByType = getSelectionCountByType(selection) const selectionsByType = getSelectionCountByType(selection)
if (selectionsByType === 'none') return null if (selectionsByType === 'none') return null
return selectionsByType return [...selectionsByType.entries()]
.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]) =>
@ -581,7 +582,6 @@ export function getSelectionTypeDisplayText(
count > 1 ? 's' : '' count > 1 ? 's' : ''
}` }`
) )
.toArray()
.join(', ') .join(', ')
} }
@ -591,7 +591,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 &&
@ -614,8 +614,9 @@ 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 const codeRef = getFaceCodeRef(artifact)
return isOverlap(artifact.codeRef.range, selection.range) if (!codeRef) return null
return isOverlap(codeRef.range, selection.range)
? { ? {
artifact, artifact,
selection, selection,
@ -672,6 +673,27 @@ export function codeToIdSelections(
id: entry.artifact.solid2dId, id: entry.artifact.solid2dId,
} }
} }
if (entry.artifact.type === 'plane') {
bestCandidate = {
artifact: entry.artifact,
selection,
id: entry.id,
}
}
if (entry.artifact.type === 'cap') {
bestCandidate = {
artifact: entry.artifact,
selection,
id: entry.id,
}
}
if (entry.artifact.type === 'wall') {
bestCandidate = {
artifact: entry.artifact,
selection,
id: entry.id,
}
}
if (type === 'extrude-wall' && entry.artifact.type === 'segment') { if (type === 'extrude-wall' && entry.artifact.type === 'segment') {
if (!entry.artifact.surfaceId) return if (!entry.artifact.surfaceId) return
const wall = engineCommandManager.artifactGraph.get( const wall = engineCommandManager.artifactGraph.get(
@ -867,7 +889,6 @@ export function updateSelections(
JSON.stringify(pathToNode) JSON.stringify(pathToNode)
) { ) {
artifact = a artifact = a
console.log('found artifact', a)
break break
} }
} }

View File

@ -1,9 +1,7 @@
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env' import { DEV } from 'env'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { import {
canRectangleOrCircleTool,
isClosedSketch,
isEditingExistingSketch, isEditingExistingSketch,
modelingMachine, modelingMachine,
pipeHasCircle, pipeHasCircle,
@ -72,7 +70,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sketch', icon: 'sketch',
status: 'available', status: 'available',
title: ({ sketchPathId }) => title: ({ sketchPathId }) =>
`${sketchPathId ? 'Edit' : 'Start'} Sketch`, sketchPathId ? 'Edit Sketch' : 'Start Sketch',
showTitle: true, showTitle: true,
hotkey: 'S', hotkey: 'S',
description: 'Start drawing a 2D sketch', description: 'Start drawing a 2D sketch',
@ -366,13 +364,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
{ {
id: 'line', id: 'line',
onClick: ({ modelingState, modelingSend }) => { onClick: ({ modelingState, modelingSend }) => {
if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
// Exit the sketch state if there are no points and they press ESC
modelingSend({
type: 'Cancel',
})
} else {
// Exit the tool if there are points and they press ESC
modelingSend({ modelingSend({
type: 'change tool', type: 'change tool',
data: { data: {
@ -381,7 +372,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
: 'none', : 'none',
}, },
}) })
}
}, },
icon: 'line', icon: 'line',
status: 'available', status: 'available',
@ -392,8 +382,7 @@ 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',
@ -473,14 +462,10 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'circle', icon: 'circle',
status: 'available', status: 'available',
title: 'Center circle', title: 'Center circle',
disabled: (state) => disabled: (state) => state.matches('Sketch no face'),
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' }) &&
!state.matches({ Sketch: 'circle3PointToolSelect' })),
isActive: (state) => isActive: (state) =>
state.matches({ Sketch: 'Circle tool' }) || state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'circle3PointToolSelect' }), state.matches({ Sketch: 'Circle three point tool' }),
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
showTitle: false, showTitle: false,
@ -494,9 +479,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
type: 'change tool', type: 'change tool',
data: { data: {
tool: !modelingState.matches({ tool: !modelingState.matches({
Sketch: 'circle3PointToolSelect', Sketch: 'Circle three point tool',
}) })
? 'circle3Points' ? 'circleThreePointNeo'
: 'none', : 'none',
}, },
}), }),
@ -522,10 +507,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'rectangle', icon: 'rectangle',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) => state.matches('Sketch no face'),
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',
@ -548,10 +530,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'arc', icon: 'arc',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) => state.matches('Sketch no face'),
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,3 +97,7 @@ export function trap<T>(
}) })
return true return true
} }
export function reject(errOrString: Error | string): Promise<never> {
return Promise.reject(errOrString)
}

File diff suppressed because one or more lines are too long

View File

@ -53,6 +53,7 @@ process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
console.log('process.env', process.env) console.log('process.env', process.env)
/// Register our application to handle all "zoo-studio:" protocols. /// Register our application to handle all "zoo-studio:" protocols.
const singleInstanceLock = app.requestSingleInstanceLock()
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [ app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
@ -65,7 +66,13 @@ if (process.defaultApp) {
// Global app listeners // Global app listeners
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() // Checking against this lock is needed for Windows and Linux, see
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#windows-and-linux-code
if (!singleInstanceLock && !process.env.IS_PLAYWRIGHT) {
app.quit()
} else {
registerStartupListeners()
}
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => { const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
let newWindow let newWindow

View File

@ -188,6 +188,9 @@ pub struct Wall {
pub sweep_id: ArtifactId, pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>, pub path_ids: Vec<ArtifactId>,
/// This is for the sketch-on-face plane, not for the wall itself. Traverse
/// to the extrude and/or segment to get the wall's code_ref.
pub face_code_ref: CodeRef,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
@ -201,6 +204,9 @@ pub struct Cap {
pub sweep_id: ArtifactId, pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>, pub path_ids: Vec<ArtifactId>,
/// This is for the sketch-on-face plane, not for the cap itself. Traverse
/// to the extrude and/or segment to get the cap's code_ref.
pub face_code_ref: CodeRef,
} }
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)] #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
@ -584,7 +590,7 @@ fn artifacts_to_update(
responses: &FnvHashMap<Uuid, OkModelingCmdResponse>, responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
current_plane_id: Option<Uuid>, current_plane_id: Option<Uuid>,
_ast: &Node<Program>, _ast: &Node<Program>,
_exec_artifacts: &IndexMap<ArtifactId, Artifact>, exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<Vec<Artifact>, KclError> { ) -> Result<Vec<Artifact>, KclError> {
// TODO: Build path-to-node from artifact_command source range. Right now, // TODO: Build path-to-node from artifact_command source range. Right now,
// we're serializing an empty array, and the TS wrapper fills it in with the // we're serializing an empty array, and the TS wrapper fills it in with the
@ -634,6 +640,17 @@ fn artifacts_to_update(
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id, sweep_id: wall.sweep_id,
path_ids: wall.path_ids.clone(), path_ids: wall.path_ids.clone(),
face_code_ref: wall.face_code_ref.clone(),
})]);
}
Some(Artifact::Cap(cap)) => {
return Ok(vec![Artifact::Cap(Cap {
id: current_plane_id.into(),
sub_type: cap.sub_type,
edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
sweep_id: cap.sweep_id,
path_ids: cap.path_ids.clone(),
face_code_ref: cap.face_code_ref.clone(),
})]); })]);
} }
Some(_) | None => { Some(_) | None => {
@ -683,6 +700,17 @@ fn artifacts_to_update(
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(), edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id, sweep_id: wall.sweep_id,
path_ids: vec![id], path_ids: vec![id],
face_code_ref: wall.face_code_ref.clone(),
}));
}
if let Some(Artifact::Cap(cap)) = plane {
return_arr.push(Artifact::Cap(Cap {
id: current_plane_id.into(),
sub_type: cap.sub_type,
edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
sweep_id: cap.sweep_id,
path_ids: vec![id],
face_code_ref: cap.face_code_ref.clone(),
})); }));
} }
return Ok(return_arr); return Ok(return_arr);
@ -809,12 +837,31 @@ fn artifacts_to_update(
source_ranges: vec![range], source_ranges: vec![range],
}) })
})?; })?;
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
*id == face_id.0
} else {
false
}
});
let sketch_on_face_source_range = extra_artifact
.and_then(|a| match a {
Artifact::StartSketchOnFace { source_range, .. } => Some(*source_range),
// TODO: If we didn't find it, it's probably a bug.
_ => None,
})
.unwrap_or_default();
return_arr.push(Artifact::Wall(Wall { return_arr.push(Artifact::Wall(Wall {
id: face_id, id: face_id,
seg_id: curve_id, seg_id: curve_id,
edge_cut_edge_ids: Vec::new(), edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id, sweep_id: path_sweep_id,
path_ids: vec![], path_ids: Vec::new(),
face_code_ref: CodeRef {
range: sketch_on_face_source_range,
path_to_node: Vec::new(),
},
})); }));
let mut new_seg = seg.clone(); let mut new_seg = seg.clone();
new_seg.surface_id = Some(face_id); new_seg.surface_id = Some(face_id);
@ -843,12 +890,29 @@ fn artifacts_to_update(
source_ranges: vec![range], source_ranges: vec![range],
}) })
})?; })?;
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace { face_id: id, .. } = a {
*id == face_id.0
} else {
false
}
});
let sketch_on_face_source_range = extra_artifact
.and_then(|a| match a {
Artifact::StartSketchOnFace { source_range, .. } => Some(*source_range),
_ => None,
})
.unwrap_or_default();
return_arr.push(Artifact::Cap(Cap { return_arr.push(Artifact::Cap(Cap {
id: face_id, id: face_id,
sub_type, sub_type,
edge_cut_edge_ids: Vec::new(), edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id, sweep_id: path_sweep_id,
path_ids: Vec::new(), path_ids: Vec::new(),
face_code_ref: CodeRef {
range: sketch_on_face_source_range,
path_to_node: Vec::new(),
},
})); }));
let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else { let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
continue; continue;

View File

@ -9,8 +9,8 @@ use crate::{
execution::{ execution::{
annotations, annotations,
cad_op::{OpArg, Operation}, cad_op::{OpArg, Operation},
kcl_value::NumericType,
memory, memory,
memory::ProgramMemory,
state::ModuleState, state::ModuleState,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, TagEngineInfo, BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, TagEngineInfo,
TagIdentifier, TagIdentifier,
@ -437,7 +437,7 @@ impl ExecutorContext {
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
let item = match init { let item = match init {
Expr::None(none) => KclValue::from(none), Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal), Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), &exec_state.mod_local.settings),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Identifier(identifier) => { Expr::Identifier(identifier) => {
let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone(); let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone();
@ -518,7 +518,10 @@ impl BinaryPart {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
match self { match self {
BinaryPart::Literal(literal) => Ok(literal.into()), BinaryPart::Literal(literal) => Ok(KclValue::from_literal(
(**literal).clone(),
&exec_state.mod_local.settings,
)),
BinaryPart::Identifier(identifier) => { BinaryPart::Identifier(identifier) => {
let value = exec_state.memory().get(&identifier.name, identifier.into())?; let value = exec_state.memory().get(&identifier.name, identifier.into())?;
Ok(value.clone()) Ok(value.clone())
@ -704,26 +707,32 @@ impl Node<BinaryExpression> {
BinaryOperator::Add => KclValue::Number { BinaryOperator::Add => KclValue::Number {
value: left + right, value: left + right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Sub => KclValue::Number { BinaryOperator::Sub => KclValue::Number {
value: left - right, value: left - right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Mul => KclValue::Number { BinaryOperator::Mul => KclValue::Number {
value: left * right, value: left * right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Div => KclValue::Number { BinaryOperator::Div => KclValue::Number {
value: left / right, value: left / right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Mod => KclValue::Number { BinaryOperator::Mod => KclValue::Number {
value: left % right, value: left % right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Pow => KclValue::Number { BinaryOperator::Pow => KclValue::Number {
value: left.powf(right), value: left.powf(right),
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Neq => KclValue::Bool { BinaryOperator::Neq => KclValue::Bool {
value: left != right, value: left != right,
@ -786,19 +795,14 @@ impl Node<UnaryExpression> {
let value = &self.argument.get_result(exec_state, ctx).await?; let value = &self.argument.get_result(exec_state, ctx).await?;
match value { match value {
KclValue::Number { value, meta: _ } => { KclValue::Number { value, ty, .. } => {
let meta = vec![Metadata {
source_range: self.into(),
}];
Ok(KclValue::Number { value: -value, meta })
}
KclValue::Int { value, meta: _ } => {
let meta = vec![Metadata { let meta = vec![Metadata {
source_range: self.into(), source_range: self.into(),
}]; }];
Ok(KclValue::Number { Ok(KclValue::Number {
value: (-value) as f64, value: -value,
meta, meta,
ty: ty.clone(),
}) })
} }
_ => Err(KclError::Semantic(KclErrorDetails { _ => Err(KclError::Semantic(KclErrorDetails {
@ -1299,8 +1303,9 @@ impl Node<ArrayRangeExpression> {
Ok(KclValue::Array { Ok(KclValue::Array {
value: range value: range
.into_iter() .into_iter()
.map(|num| KclValue::Int { .map(|num| KclValue::Number {
value: num, value: num as f64,
ty: NumericType::Unknown,
meta: meta.clone(), meta: meta.clone(),
}) })
.collect(), .collect(),
@ -1342,8 +1347,6 @@ fn article_for(s: &str) -> &'static str {
pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> { pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> {
if let KclValue::Number { value: n, .. } = &v { if let KclValue::Number { value: n, .. } = &v {
Ok(*n) Ok(*n)
} else if let KclValue::Int { value: n, .. } = &v {
Ok(*n as f64)
} else { } else {
let actual_type = v.human_friendly_type(); let actual_type = v.human_friendly_type();
let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) { let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
@ -1460,16 +1463,7 @@ fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -
})) }))
}; };
match value { match value {
KclValue::Int { value:num, meta: _ } => { KclValue::Number{value: num, .. } => {
let maybe_int: Result<usize, _> = (*num).try_into();
if let Ok(uint) = maybe_int {
Ok(Property::UInt(uint))
}
else {
make_err(format!("'{num}' is negative, so you can't index an array with it"))
}
}
KclValue::Number{value: num, meta:_} => {
let num = *num; let num = *num;
if num < 0.0 { if num < 0.0 {
return make_err(format!("'{num}' is negative, so you can't index an array with it")) return make_err(format!("'{num}' is negative, so you can't index an array with it"))
@ -1510,7 +1504,7 @@ impl Node<PipeExpression> {
fn assign_args_to_params( fn assign_args_to_params(
function_expression: NodeRef<'_, FunctionExpression>, function_expression: NodeRef<'_, FunctionExpression>,
args: Vec<Arg>, args: Vec<Arg>,
fn_memory: &mut ProgramMemory, exec_state: &mut ExecState,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
let num_args = function_expression.number_of_args(); let num_args = function_expression.number_of_args();
let (min_params, max_params) = num_args.into_inner(); let (min_params, max_params) = num_args.into_inner();
@ -1530,12 +1524,15 @@ fn assign_args_to_params(
return Err(err_wrong_number_args); return Err(err_wrong_number_args);
} }
let mem = &mut exec_state.global.memory;
let settings = &exec_state.mod_local.settings;
// Add the arguments to the memory. A new call frame should have already // Add the arguments to the memory. A new call frame should have already
// been created. // been created.
for (index, param) in function_expression.params.iter().enumerate() { for (index, param) in function_expression.params.iter().enumerate() {
if let Some(arg) = args.get(index) { if let Some(arg) = args.get(index) {
// Argument was provided. // Argument was provided.
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
arg.value.clone(), arg.value.clone(),
(&param.identifier).into(), (&param.identifier).into(),
@ -1545,9 +1542,9 @@ fn assign_args_to_params(
if let Some(ref default_val) = param.default_value { if let Some(ref default_val) = param.default_value {
// If the corresponding parameter is optional, // If the corresponding parameter is optional,
// then it's fine, the user doesn't need to supply it. // then it's fine, the user doesn't need to supply it.
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
default_val.clone().into(), KclValue::from_default_param(default_val.clone(), settings),
(&param.identifier).into(), (&param.identifier).into(),
)?; )?;
} else { } else {
@ -1563,18 +1560,21 @@ fn assign_args_to_params(
fn assign_args_to_params_kw( fn assign_args_to_params_kw(
function_expression: NodeRef<'_, FunctionExpression>, function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs, mut args: crate::std::args::KwArgs,
fn_memory: &mut ProgramMemory, exec_state: &mut ExecState,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
// Add the arguments to the memory. A new call frame should have already // Add the arguments to the memory. A new call frame should have already
// been created. // been created.
let source_ranges = vec![function_expression.into()]; let source_ranges = vec![function_expression.into()];
let mem = &mut exec_state.global.memory;
let settings = &exec_state.mod_local.settings;
for param in function_expression.params.iter() { for param in function_expression.params.iter() {
if param.labeled { if param.labeled {
let arg = args.labeled.get(&param.identifier.name); let arg = args.labeled.get(&param.identifier.name);
let arg_val = match arg { let arg_val = match arg {
Some(arg) => arg.value.clone(), Some(arg) => arg.value.clone(),
None => match param.default_value { None => match param.default_value {
Some(ref default_val) => KclValue::from(default_val.clone()), Some(ref default_val) => KclValue::from_default_param(default_val.clone(), settings),
None => { None => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
source_ranges, source_ranges,
@ -1586,7 +1586,7 @@ fn assign_args_to_params_kw(
} }
}, },
}; };
fn_memory.add(param.identifier.name.clone(), arg_val, (&param.identifier).into())?; mem.add(param.identifier.name.clone(), arg_val, (&param.identifier).into())?;
} else { } else {
let Some(unlabeled) = args.unlabeled.take() else { let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.identifier.name; let param_name = &param.identifier.name;
@ -1603,7 +1603,7 @@ fn assign_args_to_params_kw(
}) })
}); });
}; };
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
unlabeled.value.clone(), unlabeled.value.clone(),
(&param.identifier).into(), (&param.identifier).into(),
@ -1624,7 +1624,7 @@ pub(crate) async fn call_user_defined_function(
// variables shadow variables in the parent scope. The new environment's // variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure. // parent should be the environment of the closure.
exec_state.mut_memory().push_new_env_for_call(memory); exec_state.mut_memory().push_new_env_for_call(memory);
if let Err(e) = assign_args_to_params(function_expression, args, exec_state.mut_memory()) { if let Err(e) = assign_args_to_params(function_expression, args, exec_state) {
exec_state.mut_memory().pop_env(); exec_state.mut_memory().pop_env();
return Err(e); return Err(e);
} }
@ -1657,7 +1657,7 @@ pub(crate) async fn call_user_defined_function_kw(
// variables shadow variables in the parent scope. The new environment's // variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure. // parent should be the environment of the closure.
exec_state.mut_memory().push_new_env_for_call(memory); exec_state.mut_memory().push_new_env_for_call(memory);
if let Err(e) = assign_args_to_params_kw(function_expression, args, exec_state.mut_memory()) { if let Err(e) = assign_args_to_params_kw(function_expression, args, exec_state) {
exec_state.mut_memory().pop_env(); exec_state.mut_memory().pop_env();
return Err(e); return Err(e);
} }
@ -1721,7 +1721,7 @@ impl JsonSchema for FunctionParam<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
execution::parse_execute, execution::{memory::ProgramMemory, parse_execute},
parsing::ast::types::{DefaultParamVal, Identifier, Parameter}, parsing::ast::types::{DefaultParamVal, Identifier, Parameter},
}; };
@ -1731,8 +1731,9 @@ mod test {
fn test_assign_args_to_params() { fn test_assign_args_to_params() {
// Set up a little framework for this test. // Set up a little framework for this test.
fn mem(number: usize) -> KclValue { fn mem(number: usize) -> KclValue {
KclValue::Int { KclValue::Number {
value: number as i64, value: number as f64,
ty: NumericType::count(),
meta: Default::default(), meta: Default::default(),
} }
} }
@ -1838,8 +1839,8 @@ mod test {
digest: None, digest: None,
}); });
let args = args.into_iter().map(Arg::synthetic).collect(); let args = args.into_iter().map(Arg::synthetic).collect();
let mut actual = ProgramMemory::new(); let mut exec_state = ExecState::new(&Default::default());
let actual = assign_args_to_params(func_expr, args, &mut actual).map(|_| actual); let actual = assign_args_to_params(func_expr, args, &mut exec_state).map(|_| exec_state.global.memory);
assert_eq!( assert_eq!(
actual, expected, actual, expected,
"failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}" "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"

View File

@ -253,9 +253,9 @@ pub struct Plane {
pub value: PlaneType, pub value: PlaneType,
/// Origin of the plane. /// Origin of the plane.
pub origin: Point3d, pub origin: Point3d,
/// What should the planes X axis be? /// What should the plane's X axis be?
pub x_axis: Point3d, pub x_axis: Point3d,
/// What should the planes Y axis be? /// What should the plane's Y axis be?
pub y_axis: Point3d, pub y_axis: Point3d,
/// The z-axis (normal). /// The z-axis (normal).
pub z_axis: Point3d, pub z_axis: Point3d,
@ -376,9 +376,9 @@ pub struct Face {
pub artifact_id: ArtifactId, pub artifact_id: ArtifactId,
/// The tag of the face. /// The tag of the face.
pub value: String, pub value: String,
/// What should the faces X axis be? /// What should the face's X axis be?
pub x_axis: Point3d, pub x_axis: Point3d,
/// What should the faces Y axis be? /// What should the face's Y axis be?
pub y_axis: Point3d, pub y_axis: Point3d,
/// The z-axis (normal). /// The z-axis (normal).
pub z_axis: Point3d, pub z_axis: Point3d,
@ -764,6 +764,19 @@ pub enum Path {
/// This is used to compute the tangential angle. /// This is used to compute the tangential angle.
ccw: bool, ccw: bool,
}, },
CircleThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the circle
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the circle
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the circle
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal. /// A path that is horizontal.
Horizontal { Horizontal {
#[serde(flatten)] #[serde(flatten)]
@ -806,6 +819,7 @@ enum PathType {
TangentialArc, TangentialArc,
TangentialArcTo, TangentialArcTo,
Circle, Circle,
CircleThreePoint,
Horizontal, Horizontal,
AngledLineTo, AngledLineTo,
Arc, Arc,
@ -818,6 +832,7 @@ impl From<&Path> for PathType {
Path::TangentialArcTo { .. } => Self::TangentialArcTo, Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc, Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle, Path::Circle { .. } => Self::Circle,
Path::CircleThreePoint { .. } => Self::CircleThreePoint,
Path::Horizontal { .. } => Self::Horizontal, Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo, Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base, Path::Base { .. } => Self::Base,
@ -836,6 +851,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.geo_meta.id, Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id, Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id, Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id, Path::Arc { base, .. } => base.geo_meta.id,
} }
} }
@ -849,6 +865,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.tag.clone(), Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(), Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(), Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(), Path::Arc { base, .. } => base.tag.clone(),
} }
} }
@ -862,6 +879,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base, Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base, Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base, Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base, Path::Arc { base, .. } => base,
} }
} }
@ -899,6 +917,15 @@ impl Path {
linear_distance(self.get_from(), self.get_to()) linear_distance(self.get_from(), self.get_to())
} }
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius, Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::CircleThreePoint { .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([
self.get_base().from.into(),
self.get_base().to.into(),
self.get_base().to.into(),
]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], &self.get_base().from);
2.0 * std::f64::consts::PI * radius
}
Self::Arc { .. } => { Self::Arc { .. } => {
// TODO: Call engine utils to figure this out. // TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to()) linear_distance(self.get_from(), self.get_to())
@ -915,6 +942,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => Some(base), Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base), Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base), Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base), Path::Arc { base, .. } => Some(base),
} }
} }
@ -934,6 +962,17 @@ impl Path {
ccw: *ccw, ccw: *ccw,
radius: *radius, radius: *radius,
}, },
Path::CircleThreePoint { p1, p2, p3, .. } => {
let circle_center =
crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
let center_point = [circle_center.center.x, circle_center.center.y];
GetTangentialInfoFromPathsResult::Circle {
center: center_point,
ccw: true,
radius,
}
}
Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => { Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
let base = self.get_base(); let base = self.get_base();
GetTangentialInfoFromPathsResult::PreviousPoint(base.from) GetTangentialInfoFromPathsResult::PreviousPoint(base.from)

View File

@ -12,14 +12,16 @@ use crate::{
TagIdentifier, TagIdentifier,
}, },
parsing::{ parsing::{
ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode}, ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
},
token::NumericSuffix, token::NumericSuffix,
}, },
std::{args::Arg, FnAsArg}, std::{args::Arg, FnAsArg},
ExecutorContext, KclError, ModuleId, SourceRange, ExecutorContext, KclError, ModuleId, SourceRange,
}; };
use super::memory::EnvironmentRef; use super::{memory::EnvironmentRef, MetaSettings};
pub type KclObjectFields = HashMap<String, KclValue>; pub type KclObjectFields = HashMap<String, KclValue>;
@ -40,11 +42,7 @@ pub enum KclValue {
}, },
Number { Number {
value: f64, value: f64,
#[serde(rename = "__meta")] ty: NumericType,
meta: Vec<Metadata>,
},
Int {
value: i64,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
meta: Vec<Metadata>, meta: Vec<Metadata>,
}, },
@ -168,7 +166,6 @@ impl From<KclValue> for Vec<SourceRange> {
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(&meta), KclValue::Bool { meta, .. } => to_vec_sr(&meta),
KclValue::Number { meta, .. } => to_vec_sr(&meta), KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::Int { meta, .. } => to_vec_sr(&meta),
KclValue::String { meta, .. } => to_vec_sr(&meta), KclValue::String { meta, .. } => to_vec_sr(&meta),
KclValue::Array { meta, .. } => to_vec_sr(&meta), KclValue::Array { meta, .. } => to_vec_sr(&meta),
KclValue::Object { meta, .. } => to_vec_sr(&meta), KclValue::Object { meta, .. } => to_vec_sr(&meta),
@ -200,7 +197,6 @@ impl From<&KclValue> for Vec<SourceRange> {
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(meta), KclValue::Bool { meta, .. } => to_vec_sr(meta),
KclValue::Number { meta, .. } => to_vec_sr(meta), KclValue::Number { meta, .. } => to_vec_sr(meta),
KclValue::Int { meta, .. } => to_vec_sr(meta),
KclValue::String { meta, .. } => to_vec_sr(meta), KclValue::String { meta, .. } => to_vec_sr(meta),
KclValue::Uuid { meta, .. } => to_vec_sr(meta), KclValue::Uuid { meta, .. } => to_vec_sr(meta),
KclValue::Array { meta, .. } => to_vec_sr(meta), KclValue::Array { meta, .. } => to_vec_sr(meta),
@ -217,8 +213,7 @@ impl KclValue {
match self { match self {
KclValue::Uuid { value: _, meta } => meta.clone(), KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(), KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(), KclValue::Number { meta, .. } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(), KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(), KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(), KclValue::Object { value: _, meta } => meta.clone(),
@ -297,7 +292,6 @@ impl KclValue {
KclValue::Face { .. } => "Face", KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)", KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number", KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)", KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)", KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object", KclValue::Object { .. } => "object",
@ -307,14 +301,29 @@ impl KclValue {
} }
} }
pub(crate) fn from_literal(literal: LiteralValue, meta: Vec<Metadata>) -> Self { pub(crate) fn from_literal(literal: Node<Literal>, settings: &MetaSettings) -> Self {
match literal { let meta = vec![literal.metadata()];
LiteralValue::Number { value, .. } => KclValue::Number { value, meta }, match literal.inner.value {
LiteralValue::Number { value, suffix } => KclValue::Number {
value,
meta,
ty: NumericType::from_parsed(suffix, settings),
},
LiteralValue::String(value) => KclValue::String { value, meta }, LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta }, LiteralValue::Bool(value) => KclValue::Bool { value, meta },
} }
} }
pub(crate) fn from_default_param(param: DefaultParamVal, settings: &MetaSettings) -> Self {
match param {
DefaultParamVal::Literal(lit) => Self::from_literal(lit, settings),
DefaultParamVal::KclNone(none) => KclValue::KclNone {
value: none,
meta: Default::default(),
},
}
}
pub(crate) fn map_env_ref(&self, env_map: &HashMap<EnvironmentRef, EnvironmentRef>) -> Self { pub(crate) fn map_env_ref(&self, env_map: &HashMap<EnvironmentRef, EnvironmentRef>) -> Self {
let mut result = self.clone(); let mut result = self.clone();
if let KclValue::Function { ref mut memory, .. } = result { if let KclValue::Function { ref mut memory, .. } = result {
@ -327,20 +336,30 @@ impl KclValue {
/// Put the number into a KCL value. /// Put the number into a KCL value.
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self { pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta } Self::Number {
value: f,
meta,
ty: NumericType::Unknown,
}
}
pub const fn from_number_with_type(f: f64, ty: NumericType, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta, ty }
} }
/// Put the point into a KCL value. /// Put the point into a KCL value.
pub fn from_point2d(p: [f64; 2], meta: Vec<Metadata>) -> Self { pub fn from_point2d(p: [f64; 2], ty: NumericType, meta: Vec<Metadata>) -> Self {
Self::Array { Self::Array {
value: vec![ value: vec![
Self::Number { Self::Number {
value: p[0], value: p[0],
meta: meta.clone(), meta: meta.clone(),
ty: ty.clone(),
}, },
Self::Number { Self::Number {
value: p[1], value: p[1],
meta: meta.clone(), meta: meta.clone(),
ty,
}, },
], ],
meta, meta,
@ -349,7 +368,6 @@ impl KclValue {
pub(crate) fn as_usize(&self) -> Option<usize> { pub(crate) fn as_usize(&self) -> Option<usize> {
match self { match self {
KclValue::Int { value, .. } if *value > 0 => Some(*value as usize),
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value), KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),
_ => None, _ => None,
} }
@ -357,7 +375,6 @@ impl KclValue {
pub fn as_int(&self) -> Option<i64> { pub fn as_int(&self) -> Option<i64> {
match self { match self {
KclValue::Int { value, .. } => Some(*value),
KclValue::Number { value, .. } => crate::try_f64_to_i64(*value), KclValue::Number { value, .. } => crate::try_f64_to_i64(*value),
_ => None, _ => None,
} }
@ -438,10 +455,8 @@ impl KclValue {
} }
pub fn as_f64(&self) -> Option<f64> { pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, meta: _ } = &self { if let KclValue::Number { value, .. } = &self {
Some(*value) Some(*value)
} else if let KclValue::Int { value, meta: _ } = &self {
Some(*value as f64)
} else { } else {
None None
} }
@ -606,6 +621,73 @@ impl KclValue {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum NumericType {
// Specified by the user (directly or indirectly)
Known(UnitType),
// Unspecified, using defaults
Default { len: UnitLen, angle: UnitAngle },
// Exceeded the ability of the type system to track.
Unknown,
// Type info has been explicitly cast away.
Any,
}
impl NumericType {
pub fn count() -> Self {
NumericType::Known(UnitType::Count)
}
pub fn combine(self, other: &NumericType) -> NumericType {
if &self == other {
self
} else {
NumericType::Unknown
}
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix {
NumericSuffix::None => NumericType::Default {
len: settings.default_length_units,
angle: settings.default_angle_units,
},
NumericSuffix::Count => NumericType::Known(UnitType::Count),
NumericSuffix::Mm => NumericType::Known(UnitType::Length(UnitLen::Mm)),
NumericSuffix::Cm => NumericType::Known(UnitType::Length(UnitLen::Cm)),
NumericSuffix::M => NumericType::Known(UnitType::Length(UnitLen::M)),
NumericSuffix::Inch => NumericType::Known(UnitType::Length(UnitLen::Inches)),
NumericSuffix::Ft => NumericType::Known(UnitType::Length(UnitLen::Feet)),
NumericSuffix::Yd => NumericType::Known(UnitType::Length(UnitLen::Yards)),
NumericSuffix::Deg => NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
NumericSuffix::Rad => NumericType::Known(UnitType::Angle(UnitAngle::Radians)),
}
}
}
impl From<UnitLen> for NumericType {
fn from(value: UnitLen) -> Self {
NumericType::Known(UnitType::Length(value))
}
}
impl From<UnitAngle> for NumericType {
fn from(value: UnitAngle) -> Self {
NumericType::Known(UnitType::Angle(value))
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitType {
Count,
Length(UnitLen),
Angle(UnitAngle),
}
// TODO called UnitLen so as not to clash with UnitLength in settings) // TODO called UnitLen so as not to clash with UnitLength in settings)
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]

View File

@ -845,6 +845,8 @@ mod env {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::execution::kcl_value::NumericType;
use super::*; use super::*;
fn sr() -> SourceRange { fn sr() -> SourceRange {
@ -852,8 +854,9 @@ mod test {
} }
fn val(value: i64) -> KclValue { fn val(value: i64) -> KclValue {
KclValue::Int { KclValue::Number {
value, value: value as f64,
ty: NumericType::count(),
meta: Vec::new(), meta: Vec::new(),
} }
} }
@ -861,14 +864,14 @@ mod test {
#[track_caller] #[track_caller]
fn assert_get(mem: &ProgramMemory, key: &str, n: i64) { fn assert_get(mem: &ProgramMemory, key: &str, n: i64) {
match mem.get(key, sr()).unwrap() { match mem.get(key, sr()).unwrap() {
KclValue::Int { value, .. } => assert_eq!(*value, n), KclValue::Number { value, .. } => assert_eq!(*value as i64, n),
_ => unreachable!(), _ => unreachable!(),
} }
} }
fn expect_int(value: &KclValue) -> Option<i64> { fn expect_small_number(value: &KclValue) -> Option<i64> {
match value { match value {
KclValue::Int { value, .. } => Some(*value), KclValue::Number { value, .. } if value > &0.0 && value < &10.0 => Some(*value as i64),
_ => None, _ => None,
} }
} }
@ -876,7 +879,7 @@ mod test {
#[track_caller] #[track_caller]
fn assert_get_from(mem: &ProgramMemory, key: &str, n: i64, snapshot: EnvironmentRef) { fn assert_get_from(mem: &ProgramMemory, key: &str, n: i64, snapshot: EnvironmentRef) {
match mem.get_from(key, snapshot, sr()).unwrap() { match mem.get_from(key, snapshot, sr()).unwrap() {
KclValue::Int { value, .. } => assert_eq!(*value, n), KclValue::Number { value, .. } => assert_eq!(*value as i64, n),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -1127,7 +1130,7 @@ mod test {
assert_get_from(mem, "b", 3, sn3); assert_get_from(mem, "b", 3, sn3);
assert_get_from(mem, "b", 4, sn4); assert_get_from(mem, "b", 4, sn4);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_int).collect(); let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [6, 1, 3, 1, 7]; let expected = [6, 1, 3, 1, 7];
assert_eq!(vals, expected); assert_eq!(vals, expected);
@ -1136,7 +1139,7 @@ mod test {
mem.get_from("b", sn1, sr()).unwrap_err(); mem.get_from("b", sn1, sr()).unwrap_err();
assert_get_from(mem, "b", 3, sn2); assert_get_from(mem, "b", 3, sn2);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_int).collect(); let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [1, 7]; let expected = [1, 7];
assert_eq!(vals, expected); assert_eq!(vals, expected);

View File

@ -1581,7 +1581,7 @@ pub struct CallExpression {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
pub struct CallExpressionKw { pub struct CallExpressionKw {
pub callee: Node<Identifier>, pub callee: Node<Identifier>,
pub unlabeled: Option<Expr>, pub unlabeled: Option<Expr>,
@ -1591,6 +1591,9 @@ pub struct CallExpressionKw {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub digest: Option<Digest>, pub digest: Option<Digest>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1714,6 +1717,7 @@ impl CallExpressionKw {
unlabeled, unlabeled,
arguments, arguments,
digest: None, digest: None,
non_code_meta: Default::default(),
})) }))
} }
@ -2080,30 +2084,6 @@ impl Literal {
} }
} }
impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self {
let meta = vec![literal.metadata()];
match literal.inner.value {
LiteralValue::Number { value, .. } => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta },
}
}
}
impl From<&Node<Literal>> for KclValue {
fn from(literal: &Node<Literal>) -> Self {
Self::from(literal.to_owned())
}
}
impl From<&BoxNode<Literal>> for KclValue {
fn from(literal: &BoxNode<Literal>) -> Self {
let b: &Node<Literal> = literal;
Self::from(b)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -3088,20 +3068,7 @@ pub enum FnArgType {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum DefaultParamVal { pub enum DefaultParamVal {
KclNone(KclNone), KclNone(KclNone),
Literal(Literal), Literal(Node<Literal>),
}
// TODO: This should actually take metadata.
impl From<DefaultParamVal> for KclValue {
fn from(v: DefaultParamVal) -> Self {
match v {
DefaultParamVal::KclNone(kcl_none) => Self::KclNone {
value: kcl_none,
meta: Default::default(),
},
DefaultParamVal::Literal(literal) => Self::from_literal(literal.value, Vec::new()),
}
}
} }
impl DefaultParamVal { impl DefaultParamVal {

View File

@ -882,6 +882,17 @@ fn property_separator(i: &mut TokenSlice) -> PResult<()> {
.parse_next(i) .parse_next(i)
} }
/// Match something that separates the labeled arguments of a fn call.
fn labeled_arg_separator(i: &mut TokenSlice) -> PResult<()> {
alt((
// Normally you need a comma.
comma_sep,
// But, if the argument list is ending, no need for a comma.
peek(preceded(opt(whitespace), close_paren)).void(),
))
.parse_next(i)
}
/// Parse a KCL object value. /// Parse a KCL object value.
pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> { pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> {
let open = open_brace(i)?; let open = open_brace(i)?;
@ -2496,14 +2507,6 @@ fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
.parse_next(i) .parse_next(i)
} }
/// Arguments are passed into a function,
/// preceded by the name of the parameter (the label).
fn labeled_arguments(i: &mut TokenSlice) -> PResult<Vec<LabeledArg>> {
separated(0.., labeled_argument, comma_sep)
.context(expected("function arguments"))
.parse_next(i)
}
/// A type of a function argument. /// A type of a function argument.
/// This can be: /// This can be:
/// - a primitive type, e.g. 'number' or 'string' or 'bool' /// - a primitive type, e.g. 'number' or 'string' or 'bool'
@ -2579,7 +2582,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
arg_name, arg_name,
type_, type_,
default_value: match (question_mark.is_some(), default_literal) { default_value: match (question_mark.is_some(), default_literal) {
(true, Some(lit)) => Some(DefaultParamVal::Literal(lit.inner)), (true, Some(lit)) => Some(DefaultParamVal::Literal(*lit)),
(true, None) => Some(DefaultParamVal::none()), (true, None) => Some(DefaultParamVal::none()),
(false, None) => None, (false, None) => None,
(false, Some(lit)) => { (false, Some(lit)) => {
@ -2783,7 +2786,28 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
ignore_whitespace(i); ignore_whitespace(i);
let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?; let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?;
let args = labeled_arguments(i)?; let args: Vec<_> = repeat(
0..,
alt((
terminated(non_code_node.map(NonCodeOr::NonCode), whitespace),
terminated(labeled_argument, labeled_arg_separator).map(NonCodeOr::Code),
)),
)
.parse_next(i)?;
let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().fold(
(Vec::new(), BTreeMap::new()),
|(mut args, mut non_code_nodes), (i, e)| {
match e {
NonCodeOr::NonCode(x) => {
non_code_nodes.insert(i, vec![x]);
}
NonCodeOr::Code(x) => {
args.push(x);
}
}
(args, non_code_nodes)
},
);
if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) { if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) {
let just_args: Vec<_> = args.iter().collect(); let just_args: Vec<_> = args.iter().collect();
typecheck_all_kw(std_fn, &just_args)?; typecheck_all_kw(std_fn, &just_args)?;
@ -2792,6 +2816,10 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
opt(comma_sep).parse_next(i)?; opt(comma_sep).parse_next(i)?;
let end = close_paren.parse_next(i)?.end; let end = close_paren.parse_next(i)?.end;
let non_code_meta = NonCodeMeta {
non_code_nodes,
..Default::default()
};
Ok(Node { Ok(Node {
start: fn_name.start, start: fn_name.start,
end, end,
@ -2801,6 +2829,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
unlabeled: initial_unlabeled_arg, unlabeled: initial_unlabeled_arg,
arguments: args, arguments: args,
digest: None, digest: None,
non_code_meta,
}, },
outer_attrs: Vec::new(), outer_attrs: Vec::new(),
}) })
@ -4390,14 +4419,6 @@ let myBox = box([0,0], -3, -16, -10)
crate::parsing::top_level_parse(some_program_string).unwrap(); crate::parsing::top_level_parse(some_program_string).unwrap();
} }
#[test]
fn arg_labels() {
let input = r#"length: 3"#;
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(input, module_id).unwrap();
super::labeled_arguments(&mut tokens.as_slice()).unwrap();
}
#[test] #[test]
fn kw_fn() { fn kw_fn() {
for input in ["val = foo(x, y = z)", "val = foo(y = z)"] { for input in ["val = foo(x, y = z)", "val = foo(y = z)"] {
@ -4879,6 +4900,22 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
r#"fn foo(x?: number = 2) { return 1 }"# r#"fn foo(x?: number = 2) { return 1 }"#
); );
snapshot_test!(kw_function_call_in_pipe, r#"val = 1 |> f(arg = x)"#); snapshot_test!(kw_function_call_in_pipe, r#"val = 1 |> f(arg = x)"#);
snapshot_test!(
kw_function_call_multiline,
r#"val = f(
arg = x,
foo = x,
bar = x,
)"#
);
snapshot_test!(
kw_function_call_multiline_with_comments,
r#"val = f(
arg = x,
// foo = x,
bar = x,
)"#
);
} }
#[allow(unused)] #[allow(unused)]

View File

@ -0,0 +1,86 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 87,
"id": {
"end": 3,
"name": "val",
"start": 0,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "arg"
},
"arg": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "foo"
},
"arg": {
"end": 51,
"name": "x",
"start": 50,
"type": "Identifier",
"type": "Identifier"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "bar"
},
"arg": {
"end": 73,
"name": "x",
"start": 72,
"type": "Identifier",
"type": "Identifier"
}
}
],
"callee": {
"end": 7,
"name": "f",
"start": 6,
"type": "Identifier"
},
"end": 87,
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 87,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 87,
"start": 0
}

View File

@ -0,0 +1,89 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 90,
"id": {
"end": 3,
"name": "val",
"start": 0,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "arg"
},
"arg": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "bar"
},
"arg": {
"end": 76,
"name": "x",
"start": 75,
"type": "Identifier",
"type": "Identifier"
}
}
],
"callee": {
"end": 7,
"name": "f",
"start": 6,
"type": "Identifier"
},
"end": 90,
"nonCodeMeta": {
"nonCodeNodes": {
"1": [
{
"end": 55,
"start": 44,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "foo = x,",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 90,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 90,
"start": 0
}

View File

@ -48,13 +48,15 @@ expression: actual
"type": "Identifier" "type": "Identifier"
}, },
"default_value": { "default_value": {
"end": 21,
"raw": "2",
"start": 20,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": {
"value": 2.0, "value": 2.0,
"suffix": "None" "suffix": "None"
}, }
"raw": "2"
} }
} }
], ],

View File

@ -48,13 +48,15 @@ expression: actual
"type": "Identifier" "type": "Identifier"
}, },
"default_value": { "default_value": {
"end": 13,
"raw": "2",
"start": 12,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": {
"value": 2.0, "value": 2.0,
"suffix": "None" "suffix": "None"
}, }
"raw": "2"
} }
} }
], ],

View File

@ -1600,6 +1600,27 @@ mod parametric {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod ssi_pattern {
const TEST_NAME: &str = "ssi_pattern";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod angled_line { mod angled_line {
const TEST_NAME: &str = "angled_line"; const TEST_NAME: &str = "angled_line";

View File

@ -8,8 +8,8 @@ use super::shapes::PolygonType;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata, Sketch, SketchSet, kcl_value::NumericType, ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata,
SketchSurface, Solid, SolidSet, TagIdentifier, Sketch, SketchSet, SketchSurface, Solid, SolidSet, TagIdentifier,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
source_range::SourceRange, source_range::SourceRange,
@ -299,10 +299,12 @@ impl Args {
let x = KclValue::Number { let x = KclValue::Number {
value: p[0], value: p[0],
meta: vec![meta], meta: vec![meta],
ty: NumericType::Unknown,
}; };
let y = KclValue::Number { let y = KclValue::Number {
value: p[1], value: p[1],
meta: vec![meta], meta: vec![meta],
ty: NumericType::Unknown,
}; };
Ok(KclValue::Array { Ok(KclValue::Array {
value: vec![x, y], value: vec![x, y],
@ -319,6 +321,16 @@ impl Args {
) )
} }
pub(crate) fn make_user_val_from_f64_with_type(&self, f: f64, ty: NumericType) -> KclValue {
KclValue::from_number_with_type(
f,
ty,
vec![Metadata {
source_range: self.source_range,
}],
)
}
pub(crate) fn make_user_val_from_f64_array(&self, f: Vec<f64>) -> Result<KclValue, KclError> { pub(crate) fn make_user_val_from_f64_array(&self, f: Vec<f64>) -> Result<KclValue, KclError> {
let array = f let array = f
.into_iter() .into_iter()
@ -327,6 +339,7 @@ impl Args {
meta: vec![Metadata { meta: vec![Metadata {
source_range: self.source_range, source_range: self.source_range,
}], }],
ty: NumericType::Unknown,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(KclValue::Array { Ok(KclValue::Array {
@ -341,6 +354,10 @@ impl Args {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
pub(crate) fn get_number_with_type(&self) -> Result<(f64, NumericType), KclError> {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_number_array(&self) -> Result<Vec<f64>, KclError> { pub(crate) fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
let numbers = self let numbers = self
.args .args
@ -358,8 +375,25 @@ impl Args {
Ok(numbers) Ok(numbers)
} }
pub(crate) fn get_hypotenuse_leg(&self) -> Result<(f64, f64), KclError> { pub(crate) fn get_number_array_with_types(&self) -> Result<Vec<(f64, NumericType)>, KclError> {
let numbers = self.get_number_array()?; let numbers = self
.args
.iter()
.map(|arg| {
let Some(num) = <(f64, NumericType)>::from_kcl_val(&arg.value) else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
message: format!("Expected a number but found {}", arg.value.human_friendly_type()),
}));
};
Ok(num)
})
.collect::<Result<_, _>>()?;
Ok(numbers)
}
pub(crate) fn get_hypotenuse_leg(&self) -> Result<(f64, f64, NumericType), KclError> {
let numbers = self.get_number_array_with_types()?;
if numbers.len() != 2 { if numbers.len() != 2 {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
@ -368,7 +402,11 @@ impl Args {
})); }));
} }
Ok((numbers[0], numbers[1])) let mut numbers = numbers.into_iter();
let (a, ta) = numbers.next().unwrap();
let (b, tb) = numbers.next().unwrap();
let ty = ta.combine(&tb);
Ok((a, b, ty))
} }
pub(crate) fn get_circle_args( pub(crate) fn get_circle_args(
@ -456,13 +494,6 @@ impl Args {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) async fn get_adjacent_face_to_tag( pub(crate) async fn get_adjacent_face_to_tag(
&self, &self,
exec_state: &mut ExecState, exec_state: &mut ExecState,
@ -1390,8 +1421,7 @@ impl<'a> FromKclValue<'a> for super::sketch::AngledLineData {
impl<'a> FromKclValue<'a> for i64 { impl<'a> FromKclValue<'a> for i64 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg { match arg {
KclValue::Number { value, meta: _ } => crate::try_f64_to_i64(*value), KclValue::Number { value, .. } => crate::try_f64_to_i64(*value),
KclValue::Int { value, meta: _ } => Some(*value),
_ => None, _ => None,
} }
} }
@ -1427,8 +1457,7 @@ impl<'a> FromKclValue<'a> for uuid::Uuid {
impl<'a> FromKclValue<'a> for u32 { impl<'a> FromKclValue<'a> for u32 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg { match arg {
KclValue::Number { value, meta: _ } => crate::try_f64_to_u32(*value), KclValue::Number { value, .. } => crate::try_f64_to_u32(*value),
KclValue::Int { value, meta: _ } => Some(*value as u32),
_ => None, _ => None,
} }
} }
@ -1443,8 +1472,7 @@ impl<'a> FromKclValue<'a> for NonZeroU32 {
impl<'a> FromKclValue<'a> for u64 { impl<'a> FromKclValue<'a> for u64 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg { match arg {
KclValue::Number { value, meta: _ } => crate::try_f64_to_u64(*value), KclValue::Number { value, .. } => crate::try_f64_to_u64(*value),
KclValue::Int { value, meta: _ } => Some(*value as u64),
_ => None, _ => None,
} }
} }
@ -1452,8 +1480,15 @@ impl<'a> FromKclValue<'a> for u64 {
impl<'a> FromKclValue<'a> for f64 { impl<'a> FromKclValue<'a> for f64 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg { match arg {
KclValue::Number { value, meta: _ } => Some(*value), KclValue::Number { value, .. } => Some(*value),
KclValue::Int { value, meta: _ } => Some(*value as f64), _ => None,
}
}
}
impl<'a> FromKclValue<'a> for (f64, NumericType) {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Number { value, ty, .. } => Some((*value, ty.clone())),
_ => None, _ => None,
} }
} }

View File

@ -5,7 +5,7 @@ use derive_docs::stdlib;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue}, execution::{kcl_value::NumericType, ExecState, KclValue},
std::Args, std::Args,
}; };
@ -24,7 +24,7 @@ async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, description): (bool, String) = args.get_data()?; let (data, description): (bool, String) = args.get_data()?;
inner_assert(data, &description, &args).await?; inner_assert(data, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check a value at runtime, and raise an error if the argument provided /// Check a value at runtime, and raise an error if the argument provided
@ -44,7 +44,7 @@ async fn inner_assert(data: bool, message: &str, args: &Args) -> Result<(), KclE
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lt(left, right, &description, &args).await?; inner_assert_lt(left, right, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is less than to another at runtime, /// Check that a numerical value is less than to another at runtime,
@ -63,7 +63,7 @@ async fn inner_assert_lt(left: f64, right: f64, message: &str, args: &Args) -> R
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gt(left, right, &description, &args).await?; inner_assert_gt(left, right, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value equals another at runtime, /// Check that a numerical value equals another at runtime,
@ -96,7 +96,7 @@ async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str,
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?; let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
inner_assert_equal(left, right, epsilon, &description, &args).await?; inner_assert_equal(left, right, epsilon, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is greater than another at runtime, /// Check that a numerical value is greater than another at runtime,
@ -115,7 +115,7 @@ async fn inner_assert_gt(left: f64, right: f64, message: &str, args: &Args) -> R
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_lte(left, right, &description, &args).await?; inner_assert_lte(left, right, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is less than or equal to another at runtime, /// Check that a numerical value is less than or equal to another at runtime,
@ -135,7 +135,7 @@ async fn inner_assert_lte(left: f64, right: f64, message: &str, args: &Args) ->
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (left, right, description): (f64, f64, String) = args.get_data()?; let (left, right, description): (f64, f64, String) = args.get_data()?;
inner_assert_gte(left, right, &description, &args).await?; inner_assert_gte(left, right, &description, &args).await?;
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything. Ok(args.make_user_val_from_f64_with_type(0.0, NumericType::count())) // TODO: Add a new Void enum for fns that don't return anything.
} }
/// Check that a numerical value is greater than or equal to another at runtime, /// Check that a numerical value is greater than or equal to another at runtime,

View File

@ -10,10 +10,10 @@ use crate::{
/// Converts a number to integer. /// Converts a number to integer.
pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let num = args.get_number()?; let (num, ty) = args.get_number_with_type()?;
let converted = inner_int(num)?; let converted = inner_int(num)?;
Ok(args.make_user_val_from_f64(converted)) Ok(args.make_user_val_from_f64_with_type(converted, ty))
} }
/// Convert a number to an integer. /// Convert a number to an integer.

View File

@ -243,7 +243,8 @@ pub(crate) async fn do_post_extrude(
Path::Arc { .. } Path::Arc { .. }
| Path::TangentialArc { .. } | Path::TangentialArc { .. }
| Path::TangentialArcTo { .. } | Path::TangentialArcTo { .. }
| Path::Circle { .. } => { | Path::Circle { .. }
| Path::CircleThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc { let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id, face_id: *actual_face_id,
tag: path.get_base().tag.clone(), tag: path.get_base().tag.clone(),

View File

@ -57,7 +57,7 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close() /// |> close()
/// ///
/// triangleSketch = startSketchOn(offsetPlane('XY', 75)) /// triangleSketch = startSketchOn(offsetPlane('XY', offset = 75))
/// |> startProfileAt([0, 125], %) /// |> startProfileAt([0, 125], %)
/// |> line(end = [-15, -30]) /// |> line(end = [-15, -30])
/// |> line(end = [30, 0]) /// |> line(end = [30, 0])
@ -77,10 +77,10 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close() /// |> close()
/// ///
/// circleSketch0 = startSketchOn(offsetPlane('XY', 75)) /// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
/// |> circle({ center = [0, 100], radius = 50 }, %) /// |> circle({ center = [0, 100], radius = 50 }, %)
/// ///
/// circleSketch1 = startSketchOn(offsetPlane('XY', 150)) /// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle({ center = [0, 100], radius = 20 }, %) /// |> circle({ center = [0, 100], radius = 20 }, %)
/// ///
/// loft([squareSketch, circleSketch0, circleSketch1]) /// loft([squareSketch, circleSketch0, circleSketch1])
@ -96,10 +96,10 @@ pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) /// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close() /// |> close()
/// ///
/// circleSketch0 = startSketchOn(offsetPlane('XY', 75)) /// circleSketch0 = startSketchOn(offsetPlane('XY', offset = 75))
/// |> circle({ center = [0, 100], radius = 50 }, %) /// |> circle({ center = [0, 100], radius = 50 }, %)
/// ///
/// circleSketch1 = startSketchOn(offsetPlane('XY', 150)) /// circleSketch1 = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle({ center = [0, 100], radius = 20 }, %) /// |> circle({ center = [0, 100], radius = 20 }, %)
/// ///
/// loft([squareSketch, circleSketch0, circleSketch1], /// loft([squareSketch, circleSketch0, circleSketch1],

View File

@ -6,7 +6,7 @@ use derive_docs::stdlib;
use super::args::FromArgs; use super::args::FromArgs;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue}, execution::{kcl_value::NumericType, ExecState, KclValue},
std::Args, std::Args,
}; };
@ -50,7 +50,7 @@ pub async fn cos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_cos(num)?; let result = inner_cos(num)?;
Ok(args.make_user_val_from_f64(result)) Ok(args.make_user_val_from_f64_with_type(result, NumericType::count()))
} }
/// Compute the cosine of a number (in radians). /// Compute the cosine of a number (in radians).
@ -80,7 +80,7 @@ pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_sin(num)?; let result = inner_sin(num)?;
Ok(args.make_user_val_from_f64(result)) Ok(args.make_user_val_from_f64_with_type(result, NumericType::count()))
} }
/// Compute the sine of a number (in radians). /// Compute the sine of a number (in radians).
@ -110,7 +110,7 @@ pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
let num = args.get_number()?; let num = args.get_number()?;
let result = inner_tan(num)?; let result = inner_tan(num)?;
Ok(args.make_user_val_from_f64(result)) Ok(args.make_user_val_from_f64_with_type(result, NumericType::count()))
} }
/// Compute the tangent of a number (in radians). /// Compute the tangent of a number (in radians).

View File

@ -228,9 +228,9 @@ pub enum FunctionKind {
/// Compute the length of the given leg. /// Compute the length of the given leg.
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_length(hypotenuse, leg); let result = inner_leg_length(hypotenuse, leg);
Ok(KclValue::from_number(result, vec![args.into()])) Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
} }
/// Compute the length of the given leg. /// Compute the length of the given leg.
@ -248,9 +248,9 @@ fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
/// Compute the angle of the given leg for x. /// Compute the angle of the given leg for x.
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_x(hypotenuse, leg); let result = inner_leg_angle_x(hypotenuse, leg);
Ok(KclValue::from_number(result, vec![args.into()])) Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
} }
/// Compute the angle of the given leg for x. /// Compute the angle of the given leg for x.
@ -268,9 +268,9 @@ fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
/// Compute the angle of the given leg for y. /// Compute the angle of the given leg for y.
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg) = args.get_hypotenuse_leg()?; let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
let result = inner_leg_angle_y(hypotenuse, leg); let result = inner_leg_angle_y(hypotenuse, leg);
Ok(KclValue::from_number(result, vec![args.into()])) Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
} }
/// Compute the angle of the given leg for y. /// Compute the angle of the given leg for y.

View File

@ -20,8 +20,8 @@ use super::{args::Arg, FnAsArg};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
ExecState, FunctionParam, Geometries, Geometry, KclObjectFields, KclValue, Point2d, Point3d, Sketch, SketchSet, kcl_value::NumericType, ExecState, FunctionParam, Geometries, Geometry, KclObjectFields, KclValue, Point2d,
Solid, SolidSet, Point3d, Sketch, SketchSet, Solid, SolidSet,
}, },
std::Args, std::Args,
SourceRange, SourceRange,
@ -446,8 +446,9 @@ async fn make_transform<T: GeometryTrait>(
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> Result<Vec<Transform>, KclError> { ) -> Result<Vec<Transform>, KclError> {
// Call the transform fn for this repetition. // Call the transform fn for this repetition.
let repetition_num = KclValue::Int { let repetition_num = KclValue::Number {
value: i.into(), value: i.into(),
ty: NumericType::count(),
meta: vec![source_range.into()], meta: vec![source_range.into()],
}; };
let transform_fn_args = vec![Arg::synthetic(repetition_num)]; let transform_fn_args = vec![Arg::synthetic(repetition_num)];
@ -530,7 +531,7 @@ fn transform_from_obj_fields<T: GeometryTrait>(
} }
if let Some(angle) = rot.get("angle") { if let Some(angle) = rot.get("angle") {
match angle { match angle {
KclValue::Number { value: number, meta: _ } => { KclValue::Number { value: number, .. } => {
rotation.angle = Angle::from_degrees(*number); rotation.angle = Angle::from_degrees(*number);
} }
_ => { _ => {
@ -679,6 +680,8 @@ impl GeometryTrait for Box<Solid> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::execution::kcl_value::NumericType;
use super::*; use super::*;
#[test] #[test]
@ -688,14 +691,17 @@ mod tests {
KclValue::Number { KclValue::Number {
value: 1.1, value: 1.1,
meta: Default::default(), meta: Default::default(),
ty: NumericType::Unknown,
}, },
KclValue::Number { KclValue::Number {
value: 2.2, value: 2.2,
meta: Default::default(), meta: Default::default(),
ty: NumericType::Unknown,
}, },
KclValue::Number { KclValue::Number {
value: 3.3, value: 3.3,
meta: Default::default(), meta: Default::default(),
ty: NumericType::Unknown,
}, },
], ],
meta: Default::default(), meta: Default::default(),

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