Compare commits

...

38 Commits

Author SHA1 Message Date
96652a0c48 Fix onboarding rendering (#4789)
* fix onboarding rendering

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

* updates

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

* updates

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

* empty string

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

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

* updates

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

* updates

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

* updates

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

* updates

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

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

* empty

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

* empty

* can be off by 20

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

* can be off by 20

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-14 01:35:34 +00:00
04e586d07b multi profile (#4532)
* multi-profile work

* another test

* clean up

* cover a quirk with a test

* last of tests

* fix typos

* Fix source range in snap test

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 17:57:33 -05:00
fe5f574a77 Revert "Fix so that tag declarators can be used as parameters (#4692)" (#4788)
This reverts commit e27840219b.
2024-12-13 21:39:40 +00:00
e787495ad0 add a test to make sure we cant shebang in a fn (#4781)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-13 20:10:33 +00:00
8bb9be7a5e Bump kittycad-modeling-cmds (#4777) 2024-12-13 19:42:41 +00:00
00892464e8 Always run cargo test in CI so that it can be required (#4786) 2024-12-13 14:34:23 -05:00
05ed2a3367 Loft uses kw arguments (#4757)
Part of #4600
2024-12-13 13:07:52 -06:00
10cc5bce59 Fix SourceRange values in OrderedCommands to match the TS type (#4785)
* Fix SourceRange type to match WASM commands

* Update artifact graph test snap

* Update artifact graph test
2024-12-13 19:03:24 +00:00
a32f150fc1 KCL tests: Update engine API snapshot (#4784)
When engine merged their big extrude ID bugfix,
they changed the response for extrudes on face.

Basically there's no longer a start face for
something you extrude from a face, because there
just isn't. There's a hole in a previous face,
it's just a gap.

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-13 11:24:29 -06:00
ac60082e67 Fix ids for kurt so front end re-uses same ones on executions (#4780)
* updates

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

* updates

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

* working test;

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

* fix tests

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

* Update src/wasm-lib/tests/executor/main.rs

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

* Update src/wasm-lib/tests/executor/main.rs

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

* fix race condition

* fix whoopsie

* fix tsc

* for some dumb ass reason the model executes twice on load

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-12-13 02:06:26 +00:00
d44dc1b21a Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from… (#4732)
* Bump wasm-bindgen from 0.2.91 to 0.2.99 and wasm-bindgen-futures from 0.4.44 to 0.4.49

* Upgrade in the kcl crate also

* Update web-sys version constraint to match lock
2024-12-12 15:37:50 -06:00
813962ea4c Revert "warn on unneccessary brackets (#4769)" (#4776)
This reverts commit 4b6bbbe2c5 (PR #4769) because these tests are failing:

        FAIL [   0.013s] kcl-lib parsing::parser::tests::assign_brackets
        FAIL [   0.012s] kcl-lib parsing::parser::tests::test_arg
2024-12-12 15:37:37 -06:00
738443a6ab add test that ensures we use the cache, from playwright (#4721)
add test that ensures we use the cache;

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-12 15:37:23 -06:00
4b6bbbe2c5 warn on unneccessary brackets (#4769)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-13 08:20:57 +13:00
6ff8addc8b Remove non code from Digests (#4772)
Remove non code from Digests

@jessfraz and I talked it over; for the time being we're going to remove
comments from the AST digest. We already exclude source position, so
this is just increasing the degree to which we're going to ignore things
that are not germane to execution.

Before, we'd digest *some* but not all of the comments in the AST.
Silly, I know, right?

So, this code:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %) // my thing
  |> close(%)
  |> extrude(6, %)
```

Would digest differently than:

```
firstSketch = startSketchOn('XY')
  |> startProfileAt([-12, 12], %)
  |> line([-24, 0], %)
  |> close(%)
  |> extrude(6, %)
```

Which is wrong. We've fully divested of hashing code comments, so this
will now hash to be the same. Hooray.
2024-12-12 18:55:09 +00:00
da05c38b9e KCL stdlib: Add atan2 function (#4771)
At Lee's request
2024-12-12 18:11:07 +00:00
191b9b71fd KCL: Keyword fn args like "x = 1" not like "x: 1" (#4770)
Aligns with how we're doing objects.
2024-12-12 17:53:35 +00:00
05163fdded Fix KCL warnings in doc comments from let, const, and new fn syntax (#4756)
* Fix KCL warnings in doc comments from let, const, and new fn syntax

* Update docs
2024-12-12 11:33:37 -05:00
7ed26e21c6 More Walk cleanup (#4738)
* More Walk cleanup

 - The `Node` type contained two enums by mistake. Those have been
   removed.

 - Export the `Visitor` and `Visitable` traits, as I start to migrate
   stuff to them.

 - Add a wrapper to pull the `digest` off the node without doing a
   `match` elsewhere.
2024-12-12 01:49:18 +00:00
c668d40efc make pipe have a hole (#4766)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-12 01:07:14 +00:00
f38c6b90b7 Color picker in the code pane (#4761)
* add color plugin

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

* fixes

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

* fmt

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

* snapshot test goober

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

* updates

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

* updates

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

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

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-12 00:45:39 +00:00
7bc8bae0ec Update Camera Controls to Zoo (#4755)
* update camera controls to Zoo

* update e2e and initial settings

* update types and camera controls ts

* update mod.rs test

* update test, test locally
2024-12-11 15:03:51 -08:00
3804aca27e Bump codecov/codecov-action from 4 to 5 (#4498)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 14:19:43 -08:00
b127680f2f Remove type coercion (#4759)
remove type coercion
2024-12-11 22:04:36 +00:00
b7de8e60cf Sweep in kcl (#4754)
* updates

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

* updates

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

* updates

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

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

* updates

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

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

* empty

* Update src/wasm-lib/kcl/src/docs/mod.rs

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

* updates

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-11 20:59:02 +00:00
058fccb5e1 Add a right-click menu to the stream, but only when not dragging (#4745)
* Refactor ContextMenu to be able to take a guard and other event types

* refactor: break out ViewControlMenu into its own component

* Add ViewControlMenu to Stream, but only on right-click non-drag mouseup

* Fix lints

* Don't use `useCallback` for contextmenu guard

* Update context menu position on subsequent right-clicks
2024-12-11 17:57:38 +00:00
00e97257ae Set disableDifferentialDownload to true for the auto updater (#4742)
Fixes #4120
2024-12-11 09:04:02 -06:00
aeb656d176 Remove flags we had for code sign on Cut Release PRs (#4728) 2024-12-11 09:03:12 -06:00
ac49ebd6e0 Revert "chore: implemented multiple instances instead of multiple appications?" (#4750)
Revert "chore: implemented multiple instances instead of multiple appications…"

This reverts commit 548c664db0.
2024-12-11 08:58:42 -06:00
b40f03ad25 Fix wasm init deprecation warning (#4747)
See `__wbg_init()` in `src/wasm-lib/pkg/wasm_lib.js`.
2024-12-11 04:54:20 -05:00
a8ad86e645 Add some comments to new API in wasm.ts (#4712)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:37:03 +13:00
87f50cd5e9 Implement as aliases for sub-expressions (#4723)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-11 21:26:42 +13:00
0400e6228e Add a "current" marker to UnitsMenu (#4744) 2024-12-11 06:00:19 +00:00
26f150fd6c Change diagnostic action button to primary color (#4737) 2024-12-11 00:25:43 -05:00
3049f405f5 Reapply "More aggressive using of cache on engine settings changes" (#4736)
* Reapply "More aggressive using of cache on engine settings changes (#4691)" (#4729)

This reverts commit 3f1f40eeba.

* Add a utility to get all the current values from the settings object

* Use an XState selector to get the latest settings snapshot for WASM

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-12-11 02:50:22 +00:00
53d40301dc start of Appearance function (#4743)
* initial commit

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

* updates

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

* updates

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

* updates

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

* fix docs

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

* updates

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

* updates

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

* add more samples

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

* updates

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

* updatres

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

* regenerate docs

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

* updates

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

* patterns and appearance samples

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

* updates

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

* updates

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

* fmt

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-11 01:51:51 +00:00
671c01e36f More consistent GitHub links (#4741)
Fixes #4726
Tested locally with regular and nightly (`yarn files:flip-to-nightly`) configs.
2024-12-10 15:52:57 -06:00
e80151979b Pin electron versions and remove unused packages (#4708)
* Remove forge dependencies for packaging
Fixes #4628

* More clean up and pin current electron versions
2024-12-10 16:37:08 -05:00
236 changed files with 18425 additions and 5046 deletions

View File

@ -165,7 +165,6 @@ jobs:
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env: env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -173,7 +172,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -229,7 +227,6 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always

View File

@ -2,28 +2,8 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
pull_request: pull_request:
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
workflow_dispatch: workflow_dispatch:
permissions: read-all permissions: read-all
concurrency: concurrency:
@ -71,7 +51,7 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000 RUST_MIN_STACK: 10485760000
- name: Upload to codecov.io - name: Upload to codecov.io
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
token: ${{secrets.CODECOV_TOKEN}} token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

239
docs/kcl/appearance.md Normal file

File diff suppressed because one or more lines are too long

49
docs/kcl/atan2.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects) * [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo) * [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)
@ -29,6 +30,7 @@ layout: manual
* [`assertLessThan`](kcl/assertLessThan) * [`assertLessThan`](kcl/assertLessThan)
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq) * [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
* [`atan`](kcl/atan) * [`atan`](kcl/atan)
* [`atan2`](kcl/atan2)
* [`bezierCurve`](kcl/bezierCurve) * [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil) * [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer) * [`chamfer`](kcl/chamfer)
@ -101,6 +103,7 @@ layout: manual
* [`startProfileAt`](kcl/startProfileAt) * [`startProfileAt`](kcl/startProfileAt)
* [`startSketchAt`](kcl/startSketchAt) * [`startSketchAt`](kcl/startSketchAt)
* [`startSketchOn`](kcl/startSketchOn) * [`startSketchOn`](kcl/startSketchOn)
* [`sweep`](kcl/sweep)
* [`tan`](kcl/tan) * [`tan`](kcl/tan)
* [`tangentToEnd`](kcl/tangentToEnd) * [`tangentToEnd`](kcl/tangentToEnd)
* [`tangentialArc`](kcl/tangentialArc) * [`tangentialArc`](kcl/tangentialArc)

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@ fn sum(arr) {
/* The above is basically like this pseudo-code: /* The above is basically like this pseudo-code:
fn sum(arr): fn sum(arr):
let sumSoFar = 0 sumSoFar = 0
for i in arr: for i in arr:
sumSoFar = add(sumSoFar, i) sumSoFar = add(sumSoFar, i)
return sumSoFar */ return sumSoFar */
@ -96,14 +96,14 @@ fn decagon(radius) {
/* The `decagon` above is basically like this pseudo-code: /* The `decagon` above is basically like this pseudo-code:
fn decagon(radius): fn decagon(radius):
let stepAngle = (1/10) * tau() stepAngle = (1/10) * tau()
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)]) startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
// Here's the reduce part. // Here's the reduce part.
let partialDecagon = startOfDecagonSketch partialDecagon = startOfDecagonSketch
for i in [1..10]: for i in [1..10]:
let x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
let y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius
partialDecagon = lineTo([x, y], partialDecagon) partialDecagon = lineTo([x, y], partialDecagon)
fullDecagon = partialDecagon // it's now full fullDecagon = partialDecagon // it's now full
return fullDecagon */ return fullDecagon */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

55
docs/kcl/sweep.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "SweepData"
excerpt: "Data for a sweep."
layout: manual
---
Data for a sweep.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No |

View File

@ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
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 {
@ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
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}, %)`)
@ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
// 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

@ -44,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
}, },
} }
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

@ -94,6 +94,51 @@ test.describe('Editor tests', () => {
|> close(%)`) |> close(%)`)
}) })
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// Ensure we execute the first time.
await u.openDebugPanel()
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
// Add whitespace to the end of the code.
await u.codeLocator.click()
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type(' ')
await page.keyboard.press('Enter')
await page.keyboard.type(' ')
// Ensure we don't execute the second time.
await u.openDebugPanel()
// Make sure we didn't clear the scene.
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(3)
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({ test('if you click the format button it formats your code and executes so lints are still there', async ({
page, page,
}) => { }) => {

View File

@ -11,6 +11,7 @@ import {
type mouseParams = { type mouseParams = {
pixelDiff?: number pixelDiff?: number
shouldDbClick?: boolean
} }
type mouseDragToParams = mouseParams & { type mouseDragToParams = mouseParams & {
fromPoint: { x: number; y: number } fromPoint: { x: number; y: number }
@ -75,11 +76,16 @@ export class SceneFixture {
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)
: this.page.mouse.click(x, y),
clickParams.pixelDiff clickParams.pixelDiff
) )
} }
return this.page.mouse.click(x, y) return clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y)
: this.page.mouse.click(x, y)
}, },
(moveParams?: mouseParams) => { (moveParams?: mouseParams) => {
if (moveParams?.pixelDiff) { if (moveParams?.pixelDiff) {
@ -214,23 +220,7 @@ export class SceneFixture {
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) => { ) => {
let finalValue = colour await expectPixelColor(this.page, colour, coords, diff)
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
} }
get gizmo() { get gizmo() {
@ -246,3 +236,28 @@ export class SceneFixture {
await buttonToTest.click() await buttonToTest.click()
} }
} }
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
}

View File

@ -11,7 +11,10 @@ export class ToolbarFixture {
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator exitSketchBtn!: Locator
editSketchBtn!: Locator editSketchBtn!: Locator
fileTreeBtn!: Locator fileTreeBtn!: Locator
@ -33,7 +36,10 @@ export class ToolbarFixture {
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit') this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch') this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]') this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -91,4 +97,13 @@ 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()
}
} }

View File

@ -19,6 +19,7 @@ import {
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
import { expectPixelColor } from './fixtures/sceneFixture'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }, testInfo) => {
if (testInfo.tags.includes('@electron')) { if (testInfo.tags.includes('@electron')) {
@ -45,7 +46,7 @@ test.describe('Onboarding tests', () => {
{ settingsKey: TEST_SETTINGS_KEY } { settingsKey: TEST_SETTINGS_KEY }
) )
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -54,6 +55,12 @@ test.describe('Onboarding tests', () => {
// *and* that the code is shown in the editor // *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
}) })
test( test(
@ -72,7 +79,7 @@ test.describe('Onboarding tests', () => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 1000 }
await page.setViewportSize(viewportSize) await page.setViewportSize(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => { await test.step(`Create a project and open to the onboarding`, async () => {
@ -92,6 +99,14 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket' '// Shelf Bracket'
) )
// TODO: jess make less shit
// Make sure the model loaded
//const XYPlanePoint = { x: 986, y: 522 } as const
//const modelColor: [number, number, number] = [76, 76, 76]
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
await electronApp.close() await electronApp.close()
@ -108,7 +123,7 @@ test.describe('Onboarding tests', () => {
}, initialCode) }, initialCode)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Replay the onboarding // Replay the onboarding
@ -140,6 +155,12 @@ test.describe('Onboarding tests', () => {
return localStorage.getItem('persistCode') return localStorage.getItem('persistCode')
}) })
).toContain('// Shelf Bracket') ).toContain('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 986, y: 522 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
test('Click through each onboarding step', async ({ page }) => { test('Click through each onboarding step', async ({ page }) => {
@ -179,6 +200,17 @@ test.describe('Onboarding tests', () => {
// Test that the onboarding pane is gone // Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding') await expect(page.url()).not.toContain('onboarding')
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// TODO: jess to fix
// Make sure the model loaded
//const XYPlanePoint = { x: 774, y: 516 } as const
// const modelColor: [number, number, number] = [129, 129, 129]
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
}) })
test('Onboarding redirects and code updating', async ({ page }) => { test('Onboarding redirects and code updating', async ({ page }) => {
@ -439,7 +471,7 @@ test(
}) })
await test.step('Navigate into project', async () => { await test.step('Navigate into project', async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 1000 })
page.on('console', console.log) page.on('console', console.log)
@ -462,7 +494,15 @@ test(
await test.step('Confirm that the onboarding has restarted', async () => { await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible() await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click() await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
await test.step('Clear code and restart onboarding from settings', async () => { await test.step('Clear code and restart onboarding from settings', async () => {

View File

@ -135,7 +135,9 @@ test.describe('verify sketch on chamfer works', () => {
pixelDiff: 50, pixelDiff: 50,
}) })
await rectangle2ndClick() await rectangle2ndClick()
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet) await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, {
shouldNormalise: true,
})
}) })
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
@ -177,18 +179,13 @@ test.describe('verify sketch on chamfer works', () => {
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) |>lineTo([profileStartX(%),profileStartY(%)],%)
|> angledLine([ |>close(%)`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => {
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) |>lineTo([profileStartX(%),profileStartY(%)],%)
|> angledLine([ |>close(%)`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -234,19 +227,14 @@ test.describe('verify sketch on chamfer works', () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch004 = startSketchOn(extrude001, seg05)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', 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) |>lineTo([profileStartX(%),profileStartY(%)],%)|
|> angledLine([ >close(%)`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -259,104 +247,97 @@ test.describe('verify sketch on chamfer works', () => {
}, %)`, }, %)`,
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 |>lineTo([profileStartX(%),profileStartY(%)],%)
], %, $rectangleSegmentB004) |>close(%)`,
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] |> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([
|> angledLine([ segAng(rectangleSegmentA001) - 90,
segAng(rectangleSegmentA001) - 90, 217.26
217.26 ], %, $seg01)
], %, $seg01) |> angledLine([
|> angledLine([ segAng(rectangleSegmentA001),
segAng(rectangleSegmentA001), -segLen(rectangleSegmentA001)
-segLen(rectangleSegmentA001) ], %, $yo)
], %, $yo) |> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02) |> close(%)
|> close(%) extrude001 = extrude(100, sketch001)
extrude001 = extrude(100, sketch001) |> chamfer({
|> chamfer({ length = 30,
length = 30, tags = [getOppositeEdge(seg01)]
tags = [getOppositeEdge(seg01)] }, %, $seg03)
}, %, $seg03) |> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04) |> chamfer({
|> chamfer({ length = 30,
length = 30, tags = [getNextAdjacentEdge(seg02)]
tags = [getNextAdjacentEdge(seg02)] }, %, $seg05)
}, %, $seg05) |> chamfer({
|> chamfer({ length = 30,
length = 30, tags = [getNextAdjacentEdge(yo)]
tags = [getNextAdjacentEdge(yo)] }, %, $seg06)
}, %, $seg06) sketch005 = startSketchOn(extrude001, seg06)
sketch005 = startSketchOn(extrude001, seg06) profile004 = startProfileAt([-23.43, 19.69], sketch005)
|> startProfileAt([-23.43, 19.69], %) |> 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) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)
|> close(%) sketch004 = startSketchOn(extrude001, seg05)
sketch004 = startSketchOn(extrude001, seg05) profile003 = startProfileAt([82.57, 322.96], sketch004)
|> startProfileAt([82.57, 322.96], %) |> 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) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)
|> close(%) sketch003 = startSketchOn(extrude001, seg04)
sketch003 = startSketchOn(extrude001, seg04) profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> startProfileAt([-209.64, 255.28], %) |> 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) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)
|> close(%) sketch002 = startSketchOn(extrude001, seg03)
sketch002 = startSketchOn(extrude001, seg03) profile001 = startProfileAt([205.96, 254.59], sketch002)
|> startProfileAt([205.96, 254.59], %) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([
|> angledLine([ segAng(rectangleSegmentA002) - 90,
segAng(rectangleSegmentA002) - 90, 105.26
105.26 ], %)
], %, $rectangleSegmentB001) |> angledLine([
|> angledLine([ segAng(rectangleSegmentA002),
segAng(rectangleSegmentA002), -segLen(rectangleSegmentA002)
-segLen(rectangleSegmentA002) ], %)
], %, $rectangleSegmentC001) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)
|> close(%) `,
`,
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) })
@ -392,18 +373,13 @@ test.describe('verify sketch on chamfer works', () => {
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) |>lineTo([profileStartX(%),profileStartY(%)],%)
|> angledLine([ |>close(%)`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
@ -433,16 +409,16 @@ 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) ], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -504,10 +480,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, 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 app.initialise() await app.initialise()

View File

@ -114,9 +114,9 @@ test.describe('Sketch tests', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([2.61, -4.01], %)
|> xLine(12.73, %) |> xLine(8.73, %)
|> tangentialArcTo([24.95, -5.38], %)` |> tangentialArcTo([8.33, -1.31], %)`
) )
}) })
@ -126,7 +126,7 @@ test.describe('Sketch tests', () => {
await expect(async () => { await expect(async () => {
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 1000 }) ).toBeEnabled({ timeout: 1000 })
@ -135,7 +135,7 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(600) // wait for animation await page.waitForTimeout(600) // wait for animation
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
@ -149,17 +149,21 @@ test.describe('Sketch tests', () => {
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click start profileAt handle to continue profile
await page.mouse.click(702, 407)
await page.waitForTimeout(100)
await expect(async () => { await expect(async () => {
// click to add segment
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch002 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> yLine(12.34, %) |> yLine(12.34, %)
`) `)
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 5_000, intervals: [1_000] })
}) })
test('Can exit selection of face', async ({ page }) => { test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes // Load the app with the code panes
@ -669,7 +673,7 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await click00r(0, 0) await click00r(0, 0)
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)` codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0) await click00r(50, 0)
@ -705,7 +709,7 @@ test.describe('Sketch tests', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
await click00r(30, 0) await click00r(30, 0)
codeStr += ` |> startProfileAt([2.03, 0], %)` codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
// TODO: I couldn't use `toSU` here because of some rounding error causing // TODO: I couldn't use `toSU` here because of some rounding error causing
@ -742,7 +746,9 @@ test.describe('Sketch tests', () => {
await u.openDebugPanel() await u.openDebugPanel()
const code = `sketch001 = startSketchOn('-XZ') const code = `sketch001 = startSketchOn('-XZ')
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %) profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
scale * 34.8
)}], sketch001)
|> xLine(${roundOff(scale * 139.19)}, %) |> xLine(${roundOff(scale * 139.19)}, %)
|> yLine(-${roundOff(scale * 139.2)}, %) |> yLine(-${roundOff(scale * 139.2)}, %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
@ -808,11 +814,17 @@ test.describe('Sketch tests', () => {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')).toHaveText(code) await expect
// Assert the tool was unequipped .poll(async () => {
const text = await page.locator('.cm-content').innerText()
return text.replace(/\s/g, '')
})
.toBe(code.replace(/\s/g, ''))
// Assert the tool stays equipped after a profile is closed (ready for the next one)
await expect( await expect(
page.getByRole('button', { name: 'line Line', exact: true }) page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true') ).toHaveAttribute('aria-pressed', 'true')
// exit sketch // exit sketch
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -1130,11 +1142,17 @@ sketch002 = startSketchOn(extrude001, 'END')
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50) await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect
|> startProfileAt([11.8, 9.09], %) .poll(async () => {
|> line([3.39, -3.39], %) const text = await u.codeLocator.innerText()
`) return text.replace(/\s/g, '')
})
.toBe(
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([11.8, 9.09], sketch001)
|> line([3.39, -3.39], %)
`.replace(/\s/g, '')
)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
@ -1419,3 +1437,560 @@ test2.describe(`Sketching with offset planes`, () => {
} }
) )
}) })
test2.describe('multi-profile sketching', () => {
test2(
'Can add multiple profiles to a sketch (all tool types)',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(``)
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
const [startProfile1] = scene.makeMouseHelpers(568, 70)
const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78)
const [endArcStartLine] = scene.makeMouseHelpers(745, 189)
const [startProfile2] = scene.makeMouseHelpers(782, 80)
const [profile2Point2] = scene.makeMouseHelpers(921, 90)
const [profile2Point3] = scene.makeMouseHelpers(953, 178)
const [circle1Center] = scene.makeMouseHelpers(842, 147)
const [circle1Radius] = scene.makeMouseHelpers(870, 171)
const [circle2Center] = scene.makeMouseHelpers(850, 222)
const [circle2Radius] = scene.makeMouseHelpers(843, 230)
const [crnRect1point1] = scene.makeMouseHelpers(583, 205)
const [crnRect1point2] = scene.makeMouseHelpers(618, 320)
const [crnRect2point1] = scene.makeMouseHelpers(663, 215)
const [crnRect2point2] = scene.makeMouseHelpers(744, 276)
const [cntrRect1point1] = scene.makeMouseHelpers(624, 387)
const [cntrRect1point2] = scene.makeMouseHelpers(676, 355)
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
)
await endLineStartTanArc()
await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`)
await toolbar.tangentialArcBtn.click()
await app.page.waitForTimeout(100)
await endLineStartTanArc()
await endArcStartLine()
await editor.expectEditor.toContain(
`|> tangentialArcTo([16.61, 4.14], %)`
)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
await endArcStartLine()
await app.page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`)
await startProfile1()
await editor.expectEditor
.toContain(`|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
})
await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => {
await startProfile2()
await editor.expectEditor.toContain(
`profile002 = startProfileAt([19.12, 11.53], sketch001)`
)
await profile2Point2()
await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`)
await profile2Point3()
await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`)
})
await test.step('create two circles in a row without unequip', async () => {
await toolbar.circleBtn.click()
await circle1Center()
await app.page.waitForTimeout(100)
await circle1Radius()
await editor.expectEditor.toContain(
`profile003 = circle({ center = [23.19, 6.98], radius = 2.5 }, sketch001)`
)
await test.step('hover in empty space to wait for overlays to get out of the way', async () => {
await app.page.mouse.move(951, 223)
await app.page.waitForTimeout(1000)
})
await circle2Center()
await app.page.waitForTimeout(100)
await circle2Radius()
await editor.expectEditor.toContain(
`profile004 = circle({ center = [23.74, 1.9], radius = 0.72 }, sketch001)`
)
})
await test.step('create two corner rectangles in a row without unequip', async () => {
await toolbar.rectangleBtn.click()
await crnRect1point1()
await editor.expectEditor.toContain(
`profile005 = startProfileAt([5.63, 3.05], sketch001)`
)
await crnRect1point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 2.37], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 7.8], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
await crnRect2point1()
await editor.expectEditor.toContain(
`profile006 = startProfileAt([11.05, 2.37], sketch001)`
)
await crnRect2point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 5.49], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
await test.step('create two center rectangles in a row without unequip', async () => {
await toolbar.selectCenterRectangle()
await cntrRect1point1()
await editor.expectEditor.toContain(
`profile007 = startProfileAt([8.41, -9.29], sketch001)`
)
await cntrRect1point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 7.06], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) + 90,
4.34
], %)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
await cntrRect2point1()
await editor.expectEditor.toContain(
`profile008 = startProfileAt([19.33, -5.56], sketch001)`
)
await cntrRect2point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 3.12], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) + 90,
6.24
], %)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
}
)
test2(
'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`)
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [profileEnd] = scene.makeMouseHelpers(970, 105)
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
const [newProfileEnd] = scene.makeMouseHelpers(764, 104)
const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1]
const rectHandle = scene.makeMouseHelpers(901, 150)[1]
const rectDragTo = scene.makeMouseHelpers(901, 180)[1]
const circleEdge = scene.makeMouseHelpers(691, 331)[1]
const dragCircleTo = scene.makeMouseHelpers(720, 331)[1]
const [rectStart] = scene.makeMouseHelpers(794, 322)
const [rectEnd] = scene.makeMouseHelpers(757, 395)
await test2.step('enter sketch and setup', async () => {
await pointOnSegment({ shouldDbClick: true })
await app.page.waitForTimeout(600)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('extend existing profile', async () => {
await profileEnd()
await app.page.waitForTimeout(100)
await newProfileEnd()
await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('edit existing profile', async () => {
await profileEndMv()
await app.page.mouse.down()
await dragSegmentTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(`line([4.16, -4.51], %)`)
})
await test2.step('edit existing rect', async () => {
await rectHandle()
await app.page.mouse.down()
await rectDragTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(
`angledLine([-7, 10.2], %, $rectangleSegmentA001)`
)
})
await test2.step('edit existing circl', async () => {
await circleEdge()
await app.page.mouse.down()
await dragCircleTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(
`profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)`
)
})
await test2.step('add new profile', async () => {
await toolbar.rectangleBtn.click()
await app.page.waitForTimeout(100)
await rectStart()
await editor.expectEditor.toContain(
`profile004 = startProfileAt([15.62, -3.83], sketch001)`
)
await app.page.waitForTimeout(100)
await rectEnd()
await editor.expectEditor
.toContain(`|> angledLine([180, 1.97], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) + 90,
3.88
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
}
)
test2(
'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added',
async ({ app, scene, toolbar, editor, cmdBar }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`)
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [segment1Click] = scene.makeMouseHelpers(616, 131)
const sketchIsDrawnProperly = async () => {
await test2.step(
'check the sketch is still drawn properly',
async () => {
await app.page.waitForTimeout(200)
await scene.expectPixelColor(
[255, 255, 255],
{ x: 617, y: 163 },
15
)
await scene.expectPixelColor(
[255, 255, 255],
{ x: 629, y: 331 },
15
)
}
)
}
await test2.step('enter sketch and setup', async () => {
await pointOnSegment({ shouldDbClick: true })
await app.page.waitForTimeout(600)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('select and delete code for a profile', async () => {})
await app.page.getByText('close(%)').click()
await app.page.keyboard.down('Shift')
for (let i = 0; i < 11; i++) {
await app.page.keyboard.press('ArrowUp')
}
await app.page.keyboard.press('Home')
await app.page.keyboard.up('Shift')
await app.page.keyboard.press('Backspace')
await sketchIsDrawnProperly()
await test2.step('add random new var between profiles', async () => {
await app.page.keyboard.type('myVar = 5')
await app.page.keyboard.press('Enter')
await app.page.waitForTimeout(600)
})
await sketchIsDrawnProperly()
await test2.step(
'Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode',
async () => {
await expect(async () => {
await segment1Click()
await editor.expectState({
diagnostics: [],
activeLines: ['|>line([-0.41,6.99],%)'],
highlightedCode: 'line([-0.41,6.99],%)',
})
}).toPass({ timeout: 5_000, intervals: [500] })
await toolbar.lengthConstraintBtn.click()
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain('length001 = 7')
// wait for execute defer
await app.page.waitForTimeout(600)
await sketchIsDrawnProperly()
await app.page.keyboard.down('Meta')
await app.page.keyboard.press('KeyZ')
await app.page.keyboard.up('Meta')
await editor.expectEditor.not.toContain('length001 = 7')
await sketchIsDrawnProperly()
}
)
}
)
test2(
'can enter sketch when there is an extrude',
async ({ app, scene, toolbar }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([-63.43, 193.08], sketch001)
|> line([168.52, 149.87], %)
|> line([190.29, -39.18], %)
|> tangentialArcTo([319.63, 129.65], %)
|> line([-217.65, -21.76], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([16.79, 38.24], sketch001)
|> angledLine([0, 182.82], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
105.71
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile004 = circle({
center = [280.45, 47.57],
radius = 55.26
}, sketch001)
extrude002 = extrude(50, profile001)
extrude001 = extrude(5, profile003)
`)
const [pointOnSegment] = scene.makeMouseHelpers(574, 207)
await pointOnSegment()
await toolbar.editSketch()
// wait for engine animation
await app.page.waitForTimeout(600)
await test2.step('check the sketch is still drawn properly', async () => {
await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15)
})
}
)
test2(
'exit new sketch without drawing anything should not be a problem',
async ({ app, scene, toolbar, editor, cmdBar }) => {
await app.initialise(`myVar = 5`)
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
await test2.step(
"still renders code, hasn't got into a weird state",
async () => {
await editor.replaceCode(
'myVar = 5',
`myVar = 5
sketch001 = startSketchOn('XZ')
profile001 = circle({
center = [12.41, 3.87],
radius = myVar
}, sketch001)`
)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
}
)
}
)
test2(
'A sketch with only "startProfileAt" and no segments should still be able to be continued ',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([85.19, 338.59], sketch001)
|> line([213.3, -94.52], %)
|> line([-230.09, -55.34], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch002 = startSketchOn('XY')
profile002 = startProfileAt([85.81, 52.55], sketch002)
`)
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
const [nextPoint] = scene.makeMouseHelpers(763, 130)
await app.page
.getByText('startProfileAt([85.81, 52.55], sketch002)')
.click()
await toolbar.editSketch()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
// equip line tool
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
await startProfileAt()
await app.page.waitForTimeout(100)
await nextPoint()
await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`)
}
)
test2(
'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`thePart = startSketchOn('XZ')
|> startProfileAt([7.53, 10.51], %)
|> line([12.54, 1.83], %)
|> line([6.65, -6.91], %)
|> line([-6.31, -8.69], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(75, thePart)
`)
const [objClick] = scene.makeMouseHelpers(565, 343)
const [profilePoint1] = scene.makeMouseHelpers(609, 289)
const [profilePoint2] = scene.makeMouseHelpers(714, 389)
await test2.step('enter sketch and setup', async () => {
await objClick()
await toolbar.editSketch()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
})
await test2.step(
'expect code to match initial conditions still',
async () => {
await editor.expectEditor.toContain(`thePart = startSketchOn('XZ')
|> startProfileAt([7.53, 10.51], %)`)
}
)
await test2.step(
'equiping the line tool should break up the pipe expression',
async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')thePart = startProfileAt([7.53, 10.51], sketch001)`
)
}
)
await test2.step(
'can continue on to add a new profile to this sketch',
async () => {
await profilePoint1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([19.77, -7.08], sketch001)`
)
await profilePoint2()
await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`)
}
)
}
)
})

View File

@ -446,8 +446,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)
@ -469,6 +468,10 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -591,8 +594,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 }, %)`
) )
} }
) )
@ -636,8 +638,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)
@ -655,6 +656,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 += `
@ -741,8 +746,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)
@ -760,6 +764,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 += `
@ -950,7 +958,75 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => { test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird. // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos') // test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned off', async ({ page }) => { test('Grid turned off', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -1096,3 +1172,109 @@ test.fixme('theme persists', async ({ page, context }) => {
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('code color goober opening window', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
// Click the color widget
await page.locator('.cm-css-color-picker-wrapper input').click()
await expect(
page,
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 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: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
}, },
modeling: { modeling: {
defaultUnit: 'in', defaultUnit: 'in',
mouseControls: 'KittyCAD', mouseControls: 'Zoo',
cameraProjection: 'perspective', cameraProjection: 'perspective',
showDebugPanel: true, showDebugPanel: true,
}, },

View File

@ -1,6 +1,8 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { commonPoints, getUtils, setup, tearDown } from './test-utils' import { commonPoints, getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { EngineCommand } from 'lang/std/artifactGraph'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo) await setup(context, page, testInfo)
@ -130,17 +132,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
@ -188,7 +189,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(
@ -202,11 +205,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([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
@ -216,7 +244,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([-12.34, 12.34], %) |> line([-12.34, 12.34], %)
|> xLine(-12.34, %) |> xLine(-12.34, %)

View File

@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
}) })
} }
}) })
test('Right-click opens context menu when not dragged', async ({ page }) => {
const u = await getUtils(page)
await u.waitForAuthSkipAppStart()
await test.step(`The menu should not show if we drag the mouse`, async () => {
await page.mouse.move(900, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(900, 300)
await page.mouse.up({ button: 'right' })
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
})
await test.step(`The menu should show if we don't drag the mouse`, async () => {
await page.mouse.move(900, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.up({ button: 'right' })
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
})
})
}) })

View File

@ -17,11 +17,11 @@ test.describe('Testing constraints', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> xLine(-20, %) |> xLine(-20, %)
` `
) )
}) })

View File

@ -77,30 +77,31 @@ test.describe('Testing selections', () => {
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) 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}, %)`)
@ -330,6 +331,28 @@ part009 = startSketchOn('XY')
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close(%) |> close(%)
rev = revolve({ axis = 'y' }, part009) rev = revolve({ axis = 'y' }, part009)
sketch006 = startSketchOn('XY')
profile001 = circle({
center = [42.91, -70.42],
radius = 17.96
}, sketch006)
profile002 = startProfileAt([86.92, -63.81], sketch006)
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
17.05
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line([26.95, 24.21], %)
|> line([20.91, -28.61], %)
|> line([32.46, 18.71], %)
` `
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
@ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009)
}) })
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)
@ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009)
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
// Delete a single profile
await page.mouse.click(individualProfile.x, individualProfile.y)
await page.waitForTimeout(100)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line([20.91, -28.61], %)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
}) })
test("Deleting solid that the AST mod can't handle results in a toast message", async ({ test("Deleting solid that the AST mod can't handle results in a toast message", async ({
page, page,
@ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001)
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()
@ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001)
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

@ -224,8 +224,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.
@ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Equip arc tool // Equip arc tool
await page.keyboard.press('a') await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100) await page.mouse.click(1000, 100)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await page.keyboard.press('l') await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
@ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => {
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.88, 6.66], sketch002)
|> line([2.45, -0.2], %) |> line([2.71, -0.22], %)
|> line([-2.6, -1.25], %) |> line([-2.87, -1.38], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`) |> close(%)`)
) )
@ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => {
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.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166]) await u.updateCamPosition([452, -152, 1166])

View File

@ -1,20 +1,9 @@
import type { ForgeConfig } from '@electron-forge/shared-types' import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite' import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses' import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses' import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path' import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd() const rootDir = process.cwd()
const config: ForgeConfig = { const config: ForgeConfig = {
@ -39,26 +28,7 @@ const config: ForgeConfig = {
extendInfo: 'Info.plist', // Information for file associations. extendInfo: 'Info.plist', // Information for file associations.
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [ makers: [],
new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']),
new MakerRpm({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
new MakerDeb({
options: {
icon: path.resolve(rootDir, 'assets', 'icon.png'),
},
}),
],
plugins: [ plugins: [
new VitePlugin({ new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

View File

@ -39,7 +39,6 @@
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "6.3.0", "electron-updater": "6.3.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
@ -69,7 +68,7 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite --port=3000 --host=0.0.0.0",
"start:prod": "vite preview --port=3000", "start:prod": "vite preview --port=3000",
"serve": "vite serve --port=3000", "serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build", "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
@ -104,8 +103,6 @@
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "tron:start": "electron-forge start",
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make",
"tron:publish": "electron-forge publish",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron", "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts", "tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml", "tronb:package": "electron-builder --config electron-builder.yml",
@ -148,17 +145,10 @@
"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.4.0",
"@electron-forge/maker-deb": "^7.4.0", "@electron-forge/plugin-fuses": "7.4.0",
"@electron-forge/maker-rpm": "^7.4.0", "@electron-forge/plugin-vite": "7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0", "@electron/fuses": "1.8.0",
"@electron-forge/maker-wix": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5", "@nabla/vite-plugin-eslint": "^2.0.5",
@ -188,9 +178,9 @@
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "^32.1.2", "electron": "32.1.2",
"electron-builder": "^24.13.3", "electron-builder": "24.13.3",
"electron-notarize": "^1.2.2", "electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",

View File

@ -119,6 +119,11 @@
"title": "Pipe and Flange Assembly", "title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint." "description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
}, },
{
"file": "pipe-with-bend.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{ {
"file": "poopy-shoe.kcl", "file": "poopy-shoe.kcl",
"title": "Poopy Shoe", "title": "Poopy Shoe",

View File

@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -22,6 +21,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'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -38,7 +38,12 @@ export function Toolbar({
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!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

@ -105,7 +105,7 @@ export class CameraControls {
pendingZoom: number | null = null pendingZoom: number | null = null
pendingRotation: Vector2 | null = null pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
isFovAnimationInProgress = false isFovAnimationInProgress = false
perspectiveFovBeforeOrtho = 45 perspectiveFovBeforeOrtho = 45
get isPerspective() { get isPerspective() {

View File

@ -433,6 +433,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

@ -691,19 +691,21 @@ export function createProfileStartHandle({
scale = 1, scale = 1,
theme, theme,
isSelected, isSelected,
size = 12,
...rest ...rest
}: { }: {
from: Coords2d from: Coords2d
scale?: number scale?: number
theme: Themes theme: Themes
isSelected?: boolean isSelected?: boolean
size?: number
} & ( } & (
| { isDraft: true } | { isDraft: true }
| { isDraft: false; id: string; pathToNode: PathToNode } | { isDraft: false; id: string; pathToNode: PathToNode }
)) { )) {
const group = new Group() const group = new Group()
const geometry = new BoxGeometry(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 })

View File

@ -1,13 +1,23 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import { RefObject, useEffect, useMemo, useRef, useState } from 'react' import {
MouseEvent,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
interface ContextMenuProps export interface ContextMenuProps
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> { extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
items?: React.ReactElement[] items?: React.ReactElement[]
menuTargetElement?: RefObject<HTMLElement> menuTargetElement?: RefObject<HTMLElement>
guard?: (e: globalThis.MouseEvent) => boolean
event?: 'contextmenu' | 'mouseup'
} }
const DefaultContextMenuItems = [ const DefaultContextMenuItems = [
@ -20,6 +30,8 @@ export function ContextMenu({
items = DefaultContextMenuItems, items = DefaultContextMenuItems,
menuTargetElement, menuTargetElement,
className, className,
guard,
event = 'contextmenu',
...props ...props
}: ContextMenuProps) { }: ContextMenuProps) {
const dialogRef = useRef<HTMLDivElement>(null) const dialogRef = useRef<HTMLDivElement>(null)
@ -32,6 +44,15 @@ export function ContextMenu({
useHotkeys('esc', () => setOpen(false), { useHotkeys('esc', () => setOpen(false), {
enabled: open, enabled: open,
}) })
const handleContextMenu = useCallback(
(e: globalThis.MouseEvent) => {
if (guard && !guard(e)) return
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
setOpen(true)
},
[guard, setPosition, setOpen]
)
const dialogPositionStyle = useMemo(() => { const dialogPositionStyle = useMemo(() => {
if (!dialogRef.current) if (!dialogRef.current)
@ -78,21 +99,9 @@ export function ContextMenu({
// Add context menu listener to target once mounted // Add context menu listener to target once mounted
useEffect(() => { useEffect(() => {
const handleContextMenu = (e: MouseEvent) => { menuTargetElement?.current?.addEventListener(event, handleContextMenu)
console.log('context menu', e)
e.preventDefault()
setPosition({ x: e.x, y: e.y })
setOpen(true)
}
menuTargetElement?.current?.addEventListener(
'contextmenu',
handleContextMenu
)
return () => { return () => {
menuTargetElement?.current?.removeEventListener( menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
'contextmenu',
handleContextMenu
)
} }
}, [menuTargetElement?.current]) }, [menuTargetElement?.current])
@ -100,7 +109,10 @@ export function ContextMenu({
<Dialog open={open} onClose={() => setOpen(false)}> <Dialog open={open} onClose={() => setOpen(false)}>
<div <div
className="fixed inset-0 z-50 w-screen h-screen" className="fixed inset-0 z-50 w-screen h-screen"
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => {
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
}}
> >
<Dialog.Backdrop className="fixed z-10 inset-0" /> <Dialog.Backdrop className="fixed z-10 inset-0" />
<Dialog.Panel <Dialog.Panel

View File

@ -1,6 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra' import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons' import { sceneInfra } from 'lib/singletons'
import { MutableRefObject, useEffect, useMemo, useRef } from 'react' import { MutableRefObject, useEffect, useRef } from 'react'
import { import {
WebGLRenderer, WebGLRenderer,
Scene, Scene,
@ -19,16 +19,14 @@ import {
Intersection, Intersection,
Object3D, Object3D,
} from 'three' } from 'three'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
} from './ContextMenu'
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { useModelingContext } from 'hooks/useModelingContext' import {
useViewControlMenuItems,
ViewControlContextMenu,
} from './ViewControlMenu'
import { AxisNames } from 'lib/constants'
const CANVAS_SIZE = 80 const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5 const FRUSTUM_SIZE = 0.5
@ -40,64 +38,14 @@ enum AxisColors {
Z = '#6689ef', Z = '#6689ef',
Gray = '#c6c7c2', Gray = '#c6c7c2',
} }
enum AxisNames {
X = 'x',
Y = 'y',
Z = 'z',
NEG_X = '-x',
NEG_Y = '-y',
NEG_Z = '-z',
}
const axisNamesSemantic: Record<AxisNames, string> = {
[AxisNames.X]: 'Right',
[AxisNames.Y]: 'Back',
[AxisNames.Z]: 'Top',
[AxisNames.NEG_X]: 'Left',
[AxisNames.NEG_Y]: 'Front',
[AxisNames.NEG_Z]: 'Bottom',
}
export default function Gizmo() { export default function Gizmo() {
const menuItems = useViewControlMenuItems()
const wrapperRef = useRef<HTMLDivElement | null>(null) const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null) const canvasRef = useRef<HTMLCanvasElement | null>(null)
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null) const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0) const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0) const raycasterPassiveUpdateTimer = useRef(0)
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo(
() => [
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}}
>
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],
[axisNamesSemantic]
)
useEffect(() => { useEffect(() => {
if (!canvasRef.current) return if (!canvasRef.current) return
@ -161,7 +109,7 @@ export default function Gizmo() {
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm" className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
> >
<canvas ref={canvasRef} /> <canvas ref={canvasRef} />
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} /> <ViewControlContextMenu menuTargetElement={wrapperRef} />
</div> </div>
<GizmoDropdown items={menuItems} /> <GizmoDropdown items={menuItems} />
</div> </div>

View File

@ -1,4 +1,4 @@
import { APP_VERSION, RELEASE_URL } from 'routes/Settings' import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
@ -72,8 +72,8 @@ export function LowerRightControls({
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />} {!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a <a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)} onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={RELEASE_URL} href={getReleaseUrl()}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName} className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { const { auth } = useSettingsAuthContext()
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const token = auth?.context.token const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = { const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({

View File

@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { import {
isCursorInSketchCommandRange, isCursorInSketchCommandRange,
updatePathToNodeFromMap, updateSketchDetailsNodePaths,
} from 'lang/util' } from 'lang/util'
import { import {
kclManager, kclManager,
@ -71,14 +71,24 @@ 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 {
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import { import {
doesSceneHaveExtrudedSketch, doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
getNodePathFromSourceRange, doesSketchPipeNeedSplitting,
isSingleCursorInPipe, getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst' } from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
@ -86,7 +96,7 @@ import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
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 { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment' import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
@ -100,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -290,7 +304,7 @@ export const ModelingMachineProvider = ({
return { return {
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: event.data, sketchEntryNodePath: event.data,
}, },
} }
}), }),
@ -413,9 +427,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 ||
[], [],
}, },
} }
@ -625,7 +647,6 @@ export const ModelingMachineProvider = ({
} }
const canShell = canShellSelection(selectionRanges) const canShell = canShellSelection(selectionRanges)
console.log('canShellSelection', canShellSelection(selectionRanges))
if (err(canShell)) return false if (err(canShell)) return false
return canShell return canShell
}, },
@ -648,7 +669,12 @@ export const ModelingMachineProvider = ({
'Selection is on face': ({ context: { selectionRanges }, event }) => { 'Selection is on face': ({ context: { selectionRanges }, event }) => {
if (event.type !== 'Enter sketch') return false if (event.type !== 'Enter sketch') return false
if (event.data?.forceNewSketch) return false if (event.data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
)
return false return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
@ -679,10 +705,32 @@ export const ModelingMachineProvider = ({
// this assumes no changes have been made to the sketch besides what we did when entering the sketch // this assumes no changes have been made to the sketch besides what we did when entering the sketch
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode? // i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
const newAst = structuredClone(kclManager.ast) const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.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: () => {},
@ -692,7 +740,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'
@ -719,7 +767,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,
@ -739,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],
@ -747,12 +799,14 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sourceRange = const sketchPathToNode =
selectionRanges.graphSelections[0]?.codeRef?.range selectionRanges.graphSelections[0]?.codeRef?.pathToNode
const sketchPathToNode = getNodePathFromSourceRange( const plane = getPlaneFromArtifact(
kclManager.ast, selectionRanges.graphSelections[0].artifact,
sourceRange engineCommandManager.artifactGraph
) )
if (err(plane)) return Promise.reject(plane)
const info = await getSketchOrientationDetails( const info = await getSketchOrientationDetails(
sketchPathToNode || [] sketchPathToNode || []
) )
@ -760,8 +814,17 @@ export const ModelingMachineProvider = ({
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
return { const sketchPaths = getPathsFromArtifact({
artifact: selectionRanges.graphSelections[0].artifact,
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
if (!plane.codeRef)
return Promise.reject(new Error('No plane codeRef'))
return {
sketchEntryNodePath: sketchPathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath: plane.codeRef.pathToNode,
zAxis: info.sketchDetails.zAxis || null, zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null, yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map( origin: info.sketchDetails.origin.map(
@ -773,7 +836,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,
@ -785,13 +848,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,
@ -812,13 +885,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,
@ -829,13 +904,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,
@ -856,7 +941,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -866,14 +953,15 @@ 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 } =
? applyConstraintAngleBetween({ await (info.enabled
selectionRanges, ? applyConstraintAngleBetween({
}) selectionRanges,
: applyConstraintAngleLength({ })
selectionRanges, : applyConstraintAngleLength({
angleOrLength: 'setAngle', selectionRanges,
})) angleOrLength: 'setAngle',
}))
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
@ -882,13 +970,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,
@ -909,7 +1007,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -924,20 +1024,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,
@ -958,13 +1068,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,
}) })
@ -974,13 +1086,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,
@ -1001,13 +1122,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,
@ -1018,13 +1141,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,
@ -1045,13 +1177,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,
@ -1062,13 +1196,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,
@ -1089,7 +1232,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -1109,9 +1254,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
@ -1141,6 +1288,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,
@ -1171,10 +1319,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,
@ -1195,7 +1355,140 @@ 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')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-center-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
})
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')
const existingSketchInfoNoOp = {
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
} as const
if (
!sketchDetails.sketchNodePaths.length &&
sketchDetails.planeNodePath.length
) {
// new sketch, no profiles yet
return existingSketchInfoNoOp
}
const doesNeedSplitting = doesSketchPipeNeedSplitting(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
if (!doesNeedSplitting) return existingSketchInfoNoOp
const splitResult = splitPipedProfile(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
await kclManager.executeAstMock(splitResult.modifiedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
splitResult.modifiedAst
)
return {
updatedEntryNodePath: splitResult.pathToProfile,
updatedSketchNodePaths: [splitResult.pathToProfile],
updatedPlaneNodePath: sketchDetails.planeNodePath,
} }
} }
), ),

View File

@ -6,6 +6,7 @@ import Tooltip from 'components/Tooltip'
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import { onboardingPaths } from 'routes/Onboarding/paths'
export interface ModelingPaneProps { export interface ModelingPaneProps {
id: string id: string
@ -70,7 +71,7 @@ export const ModelingPane = ({
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const pointerEventsCssClass = const pointerEventsCssClass =
onboardingStatus.current === 'camera' onboardingStatus.current === onboardingPaths.CAMERA
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '
return ( return (

View File

@ -19,6 +19,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
import { onboardingPaths } from 'routes/Onboarding/paths'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -41,7 +42,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext() const { send, context } = useModelingContext()
const pointerEventsCssClass = const pointerEventsCssClass =
onboardingStatus.current === 'camera' || onboardingStatus.current === onboardingPaths.CAMERA ||
context.store?.openPanes.length === 0 context.store?.openPanes.length === 0
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput' import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings' import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createAndOpenNewTutorialProject, createAndOpenNewTutorialProject,
@ -246,8 +246,8 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */} to inject the version from package.json */}
App version {APP_VERSION}.{' '} App version {APP_VERSION}.{' '}
<a <a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)} onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={RELEASE_URL} href={getReleaseUrl()}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -1,5 +1,5 @@
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react' import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
@ -23,7 +23,6 @@ import {
engineCommandManager, engineCommandManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings' import { settings } from 'lib/settings/initialSettings'
import { import {
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
// a little hacky for sure, open to changing it /**
// this implies that we should only even have one instance of this provider mounted at any one time * This variable is used to store the last snapshot of the settings context
// but I think that's a safe assumption * for use outside of React, such as in `wasm.ts`. It is updated every time
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined * the settings machine changes with `useSelector`.
export const getSettingsState = () => settingsStateRef * TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
*/
export let lastSettingsContextSnapshot:
| ContextFrom<typeof settingsMachine>
| undefined
export const SettingsAuthContext = createContext({} as SettingsAuthContextType) export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current) .setTheme(context.app.theme.current)
.catch(reportRejection) .catch(reportRejection)
}, },
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => { setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
}, },
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
hidden: !context.modeling.highlightEdges.current,
},
})
},
toastSuccess: ({ event }) => { toastSuccess: ({ event }) => {
if (!('data' in event)) return if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [ const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': ({ context, event }) => { 'Execute AST': ({ context, event }) => {
try { try {
const relevantSetting = (s: typeof settings) => {
return (
s.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current ||
s.modeling.showScaleGrid.current !==
context.modeling.showScaleGrid.current ||
s.modeling?.highlightEdges.current !==
context.modeling.highlightEdges.current
)
}
const allSettingsIncludesUnitChange = const allSettingsIncludesUnitChange =
event.type === 'Set all settings' && event.type === 'Set all settings' &&
event.settings?.modeling?.defaultUnit?.current !== relevantSetting(event.settings)
context.modeling.defaultUnit.current
const resetSettingsIncludesUnitChange = const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' && event.type === 'Reset settings' && relevantSetting(settings)
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
if ( if (
event.type === 'set.modeling.defaultUnit' || event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange || allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
}), }),
{ input: loadedSettings } { input: loadedSettings }
) )
settingsStateRef = settingsState.context // Any time the actor changes, update the settings state for external use
useSelector(settingsActor, (s) => {
lastSettingsContextSnapshot = s.context
})
useEffect(() => { useEffect(() => {
if (!isDesktop()) return if (!isDesktop()) return

View File

@ -20,6 +20,7 @@ import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { getArtifactOfTypes } from 'lang/std/artifactGraph' import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { ViewControlContextMenu } from './ViewControlMenu'
enum StreamState { enum StreamState {
Playing = 'playing', Playing = 'playing',
@ -30,6 +31,7 @@ enum StreamState {
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const videoWrapperRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext() const { state, send } = useModelingContext()
@ -258,7 +260,7 @@ export const Stream = () => {
setIsLoading(false) setIsLoading(false)
}, [mediaStream]) }, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => { const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything // If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return if (!isNetworkOkay) return
if (!videoRef.current) return if (!videoRef.current) return
@ -320,10 +322,11 @@ export const Stream = () => {
return ( return (
<div <div
ref={videoWrapperRef}
className="absolute inset-0 z-0" className="absolute inset-0 z-0"
id="stream" id="stream"
data-testid="stream" data-testid="stream"
onClick={handleMouseUp} onClick={handleClick}
onDoubleClick={enterSketchModeIfSelectingSketch} onDoubleClick={enterSketchModeIfSelectingSketch}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()} onContextMenuCapture={(e) => e.preventDefault()}
@ -384,6 +387,14 @@ export const Stream = () => {
</Loading> </Loading>
</div> </div>
)} )}
<ViewControlContextMenu
event="mouseup"
guard={(e) =>
sceneInfra.camControls.wasDragging === false &&
btnName(e).right === true
}
menuTargetElement={videoWrapperRef}
/>
</div> </div>
) )
} }

View File

@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
export function ToastUpdate({ export function ToastUpdate({
version, version,
@ -32,10 +33,8 @@ export function ToastUpdate({
A new update has downloaded and will be available next time you A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '} start the app. You can view the release notes{' '}
<a <a
onClick={openExternalBrowserIfDesktop( onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}` href={getReleaseUrl(version)}
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >

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

@ -93,6 +93,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,
@ -132,6 +133,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(
@ -144,8 +146,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

@ -70,10 +70,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
@ -94,7 +98,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({
@ -112,6 +116,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,
} }
} }
@ -124,6 +134,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)
@ -208,5 +219,6 @@ export async function applyConstraintAngleLength({
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
} }
} }

View File

@ -41,7 +41,10 @@ export function UnitsMenu() {
close() close()
}} }}
> >
{baseUnitLabels[unit]} <span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && (
<span className="text-chalkboard-60">current</span>
)}
</button> </button>
</li> </li>
))} ))}

View File

@ -0,0 +1,66 @@
import { reportRejection } from 'lib/trap'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
ContextMenuProps,
} from './ContextMenu'
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
import { useModelingContext } from 'hooks/useModelingContext'
import { useMemo } from 'react'
import { sceneInfra } from 'lib/singletons'
export function useViewControlMenuItems() {
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo(
() => [
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}}
>
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuDivider />,
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],
[VIEW_NAMES_SEMANTIC]
)
return menuItems
}
export function ViewControlContextMenu({
menuTargetElement: wrapperRef,
...props
}: ContextMenuProps) {
const menuItems = useViewControlMenuItems()
return (
<ContextMenu
data-testid="view-controls-menu"
menuTargetElement={wrapperRef}
items={menuItems}
{...props}
/>
)
}

View File

@ -0,0 +1,327 @@
import {
EditorView,
WidgetType,
ViewUpdate,
ViewPlugin,
DecorationSet,
Decoration,
} from '@codemirror/view'
import { Range, Extension, Text } from '@codemirror/state'
import { NodeProp, Tree } from '@lezer/common'
import { language, syntaxTree } from '@codemirror/language'
interface PickerState {
from: number
to: number
alpha: string
colorType: ColorType
}
export interface WidgetOptions extends PickerState {
color: string
}
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
export enum ColorType {
hex = 'HEX',
}
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
function discoverColorsInKCL(
syntaxTree: Tree,
from: number,
to: number,
typeName: string,
doc: Text,
language?: string
): WidgetOptions | Array<WidgetOptions> | null {
switch (typeName) {
case 'Program':
case 'VariableDeclaration':
case 'CallExpression':
case 'ObjectExpression':
case 'ObjectProperty':
case 'ArgumentList':
case 'PipeExpression': {
let innerTree = syntaxTree.resolveInner(from, 0).tree
if (!innerTree) {
innerTree = syntaxTree.resolveInner(from, 1).tree
if (!innerTree) {
return null
}
}
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
if (overlayTree?.type.name !== 'Styles') {
return null
}
const ret: Array<WidgetOptions> = []
overlayTree.iterate({
from: 0,
to: overlayTree.length,
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
const maybeWidgetOptions = discoverColorsInKCL(
syntaxTree,
// We add one because the tree doesn't include the
// quotation mark from the style tag
from + 1 + overlayFrom,
from + 1 + overlayTo,
type.name,
doc,
language
)
if (maybeWidgetOptions) {
if (Array.isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions)
} else {
ret.push(maybeWidgetOptions)
}
}
},
})
return ret
}
case 'String': {
const result = parseColorLiteral(doc.sliceString(from, to))
if (!result) {
return null
}
return {
...result,
from,
to,
}
}
default:
return null
}
}
export function parseColorLiteral(colorLiteral: string): ColorData | null {
const literal = colorLiteral.replace(/"/g, '')
const match = hexRegex.exec(literal)
if (!match) {
return null
}
const [color, alpha] = toFullHex(literal)
return {
colorType: ColorType.hex,
color,
alpha,
}
}
function colorPickersDecorations(
view: EditorView,
discoverColors: typeof discoverColorsInKCL
) {
const widgets: Array<Range<Decoration>> = []
const st = syntaxTree(view.state)
for (const range of view.visibleRanges) {
st.iterate({
from: range.from,
to: range.to,
enter: ({ type, from, to }) => {
const maybeWidgetOptions = discoverColors(
st,
from,
to,
type.name,
view.state.doc,
view.state.facet(language)?.name
)
if (!maybeWidgetOptions) {
return
}
if (!Array.isArray(maybeWidgetOptions)) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions),
side: 1,
}).range(maybeWidgetOptions.from)
)
return
}
for (const wo of maybeWidgetOptions) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(wo),
side: 1,
}).range(wo.from)
)
}
},
})
}
return Decoration.set(widgets)
}
function toFullHex(color: string): string[] {
if (color.length === 4) {
// 3-char hex
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
'',
]
}
if (color.length === 5) {
// 4-char hex (alpha)
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
color[4].repeat(2),
]
}
if (color.length === 9) {
// 8-char hex (alpha)
return [`#${color.slice(1, -2)}`, color.slice(-2)]
}
return [color, '']
}
export const wrapperClassName = 'cm-css-color-picker-wrapper'
class ColorPickerWidget extends WidgetType {
private readonly state: PickerState
private readonly color: string
constructor({ color, ...state }: WidgetOptions) {
super()
this.state = state
this.color = color
}
eq(other: ColorPickerWidget) {
return (
other.state.colorType === this.state.colorType &&
other.color === this.color &&
other.state.from === this.state.from &&
other.state.to === this.state.to &&
other.state.alpha === this.state.alpha
)
}
toDOM() {
const picker = document.createElement('input')
pickerState.set(picker, this.state)
picker.type = 'color'
picker.value = this.color
const wrapper = document.createElement('span')
wrapper.appendChild(picker)
wrapper.className = wrapperClassName
return wrapper
}
ignoreEvent() {
return false
}
}
export const colorPickerTheme = EditorView.baseTheme({
[`.${wrapperClassName}`]: {
display: 'inline-block',
outline: '1px solid #eee',
marginRight: '0.6ch',
height: '1em',
width: '1em',
transform: 'translateY(1px)',
},
[`.${wrapperClassName} input[type="color"]`]: {
cursor: 'pointer',
height: '100%',
width: '100%',
padding: 0,
border: 'none',
'&::-webkit-color-swatch-wrapper': {
padding: 0,
},
'&::-webkit-color-swatch': {
border: 'none',
},
'&::-moz-color-swatch': {
border: 'none',
},
},
})
interface IFactoryOptions {
discoverColors: typeof discoverColorsInKCL
}
export const makeColorPicker = (options: IFactoryOptions) =>
ViewPlugin.fromClass(
class ColorPickerViewPlugin {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = colorPickersDecorations(view, options.discoverColors)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = colorPickersDecorations(
update.view,
options.discoverColors
)
}
}
},
{
decorations: (v) => v.decorations,
eventHandlers: {
change: (e, view) => {
const target = e.target as HTMLInputElement
if (
target.nodeName !== 'INPUT' ||
!target.parentElement ||
!target.parentElement.classList.contains(wrapperClassName)
) {
return false
}
const data = pickerState.get(target)!
let converted = '"' + target.value + data.alpha + '"'
view.dispatch({
changes: {
from: data.from,
to: data.to,
insert: converted,
},
})
return true
},
},
}
)
export const colorPicker: Extension = [
makeColorPicker({ discoverColors: discoverColorsInKCL }),
colorPickerTheme,
]

View File

@ -17,6 +17,7 @@ import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
// @ts-ignore: No types available // @ts-ignore: No types available
import { parser } from './kcl.grammar' import { parser } from './kcl.grammar'
import { colorPicker } from './colors'
export interface LanguageOptions { export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] workspaceFolders: LSP.WorkspaceFolder[]
@ -54,14 +55,14 @@ export const KclLanguage = LRLanguage.define({
}) })
export function kcl(options: LanguageOptions) { export function kcl(options: LanguageOptions) {
return new LanguageSupport( return new LanguageSupport(KclLanguage, [
KclLanguage, colorPicker,
kclPlugin({ kclPlugin({
documentUri: options.documentUri, documentUri: options.documentUri,
workspaceFolders: options.workspaceFolders, workspaceFolders: options.workspaceFolders,
allowHTMLContent: true, allowHTMLContent: true,
client: options.client, client: options.client,
processLspNotification: options.processLspNotification, processLspNotification: options.processLspNotification,
}) }),
) ])
} }

View File

@ -1,7 +1,5 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client' import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker { export enum LspWorker {
Kcl = 'kcl', Kcl = 'kcl',
Copilot = 'copilot', Copilot = 'copilot',
@ -9,7 +7,6 @@ export enum LspWorker {
export interface KclWorkerOptions { export interface KclWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
baseUnit: UnitLength
apiBaseUrl: string apiBaseUrl: string
} }

View File

@ -17,7 +17,6 @@ import {
KclWorkerOptions, KclWorkerOptions,
CopilotWorkerOptions, CopilotWorkerOptions,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
@ -46,14 +45,12 @@ export async function copilotLspRun(
export async function kclLspRun( export async function kclLspRun(
config: ServerConfig, config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string, token: string,
baseUnit: string,
baseUrl: string baseUrl: string
) { ) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl) await kcl_lsp_run(config, null, undefined, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
switch (worker) { switch (worker) {
case LspWorker.Kcl: case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions const kclData = eventData as KclWorkerOptions
await kclLspRun( await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState' import { useAppState, useAppStream } from 'AppState'
@ -56,9 +56,6 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager) return makeDefaultPlanes(kclManager.engineCommandManager)
}, },
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
}) })
hasSetNonZeroDimensions.current = true hasSetNonZeroDimensions.current = true
} }

View File

@ -317,3 +317,8 @@ code {
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
height: 100% !important; height: 100% !important;
} }
/* Can't use #code-mirror-override here as we're outside of this div */
.body-bg .cm-diagnosticAction {
@apply bg-primary;
}

View File

@ -311,8 +311,6 @@ export class KclManager {
// Do not send send scene commands if the program was interrupted, go to clean up // Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) { if (!isInterrupted) {
this.addDiagnostics(await lintAst({ ast: ast })) this.addDiagnostics(await lintAst({ ast: ast }))
sceneInfra.modelingSend({ type: 'code edit during sketch' })
setSelectionFilterToDefault(execState.memory, this.engineCommandManager) setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
if (args.zoomToFit) { if (args.zoomToFit) {
@ -358,7 +356,11 @@ export class KclManager {
this.lastSuccessfulProgramMemory = execState.memory this.lastSuccessfulProgramMemory = execState.memory
} }
this.ast = { ...ast } this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
this._executeCallback() this._executeCallback()
if (!isInterrupted)
sceneInfra.modelingSend({ type: 'code edit during sketch' })
this.engineCommandManager.addCommandLog({ this.engineCommandManager.addCommandLog({
type: 'execution-done', type: 'execution-done',
data: null, data: null,
@ -400,6 +402,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._programMemory = execState.memory this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
@ -411,7 +414,7 @@ export class KclManager {
// problem this solves, but either way we should strive to remove it. // problem this solves, but either way we should strive to remove it.
Array.from(this.engineCommandManager.artifactGraph).forEach( Array.from(this.engineCommandManager.artifactGraph).forEach(
([commandId, artifact]) => { ([commandId, artifact]) => {
if (!('codeRef' in artifact)) return if (!('codeRef' in artifact && artifact.codeRef)) return
const _node1 = getNodeFromPath<Node<CallExpression>>( const _node1 = getNodeFromPath<Node<CallExpression>>(
this.ast, this.ast,
artifact.codeRef.pathToNode, artifact.codeRef.pathToNode,

View File

@ -1,6 +1,6 @@
import { import {
Program, Program,
_executor, executor,
ProgramMemory, ProgramMemory,
kclLint, kclLint,
emptyExecState, emptyExecState,
@ -64,12 +64,9 @@ export async function executeAst({
try { try {
const execState = await (programMemoryOverride const execState = await (programMemoryOverride
? enginelessExecutor(ast, programMemoryOverride) ? enginelessExecutor(ast, programMemoryOverride)
: _executor(ast, engineCommandManager)) : executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands(
programMemoryOverride !== undefined
)
await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],

View File

@ -16,6 +16,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, getNodePathFromSourceRange } from './queryAst' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
@ -918,3 +919,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, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
if (err(result)) throw result
const newCode = recast(result.modifiedAst)
if (err(newCode)) throw newCode
expect(newCode.trim()).toBe(expectedCodeAfter.trim())
})
it('should return error for already split pipe', () => {
const codeBefore = `sketch001 = startSketchOn('XZ')
part001 = startProfileAt([1, 2], sketch001)
|> line([3, 4], %)
|> line([5, 6], %)
|> close(%)
extrude001 = extrude(5, part001)
`
const ast = assertParse(codeBefore)
const codeOfInterest = `startProfileAt([1, 2], sketch001)`
const range: [number, number, boolean] = [
codeBefore.indexOf(codeOfInterest),
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
true,
]
const pathToPipe = getNodePathFromSourceRange(ast, range)
const result = splitPipedProfile(ast, pathToPipe)
expect(result instanceof Error).toBe(true)
})
})

View File

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

View File

@ -40,7 +40,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => { callbackOnEngineLiteConnect: () => {
resolve(true) resolve(true)
}, },
@ -248,7 +247,7 @@ extrude003 = extrude(-15, sketch003)`
selectedSegmentSnippet, selectedSegmentSnippet,
expectedExtrudeSnippet expectedExtrudeSnippet
) )
}) }, 5_000)
}) })
const runModifyAstCloneWithEdgeTreatmentAndTag = async ( const runModifyAstCloneWithEdgeTreatmentAndTag = async (
@ -276,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
const selection: Selections = { const selection: Selections = {
graphSelections: segmentRanges.map((segmentRange) => { graphSelections: segmentRanges.map((segmentRange) => {
const maybeArtifact = [...artifactGraph].find(([, a]) => { const maybeArtifact = [...artifactGraph].find(([, a]) => {
if (!('codeRef' in a)) return false if (!('codeRef' in a && a.codeRef)) return false
return isOverlap(a.codeRef.range, segmentRange) return isOverlap(a.codeRef.range, segmentRange)
}) })
return { return {
@ -478,7 +477,7 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %)` |> chamfer({ length = 5, tags = [seg01] }, %)`
const segmentSnippets = ['line([-20, 0], %)'] const segmentSnippets = ['line([-20, 0], %)']
const expectedCode = `sketch001 = startSketchOn('XY') const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %) |> startProfileAt([-10, 10], %)
@ -488,8 +487,8 @@ extrude001 = extrude(-15, sketch001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-15, sketch001) extrude001 = extrude(-15, sketch001)
|> chamfer({ length: 5, tags: [seg01] }, %) |> chamfer({ length = 5, tags = [seg01] }, %)
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)` |> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
await runModifyAstCloneWithEdgeTreatmentAndTag( await runModifyAstCloneWithEdgeTreatmentAndTag(
code, code,

View File

@ -33,6 +33,7 @@ import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from 'wasm-lib/kcl/bindings/FunctionExpression'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -596,7 +597,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,
@ -648,7 +655,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')
@ -767,8 +774,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(programMemory?.get(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
@ -1101,3 +1115,57 @@ export function getObjExprProperty(
if (index === -1) return null if (index === -1) return null
return { expr: node.properties[index].value, index } return { expr: node.properties[index].value, index }
} }
export function isCursorInFunctionDefinition(
ast: Node<Program>,
selectionRanges: Selection
): boolean {
if (!selectionRanges?.codeRef?.pathToNode) return false
const node = getNodeFromPath<FunctionExpression>(
ast,
selectionRanges.codeRef.pathToNode,
'FunctionExpression'
)
if (err(node)) return false
if (node.node.type === 'FunctionExpression') return true
return false
}
export function getBodyIndex(pathToNode: PathToNode): number | Error {
const index = Number(pathToNode[1][0])
if (Number.isInteger(index)) return index
return new Error('Expected number index')
}
export function isCallExprWithName(
expr: Expr | CallExpression,
name: string
): expr is CallExpression {
if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
return expr.callee.name === name
}
return false
}
export function doesSketchPipeNeedSplitting(
ast: Node<Program>,
pathToPipe: PathToNode
): boolean | Error {
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
pathToPipe,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclarator') return new Error('Not a var')
const pipeExpression = varDec.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const [firstPipe, secondPipe] = pipeExpression.body
if (!firstPipe || !secondPipe) return false
if (
isCallExprWithName(firstPipe, 'startSketchOn') &&
isCallExprWithName(secondPipe, 'startProfileAt')
)
return true
return false
}

View File

@ -13,7 +13,7 @@ Map {
"range": [ "range": [
12, 12,
31, 31,
0, true,
], ],
}, },
"id": "UUID", "id": "UUID",
@ -33,7 +33,7 @@ Map {
"range": [ "range": [
37, 37,
64, 64,
0, true,
], ],
}, },
"id": "UUID", "id": "UUID",
@ -60,7 +60,7 @@ Map {
"range": [ "range": [
70, 70,
86, 86,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -83,7 +83,7 @@ Map {
"range": [ "range": [
92, 92,
119, 119,
0, true,
], ],
}, },
"edgeCutId": "UUID", "edgeCutId": "UUID",
@ -107,7 +107,7 @@ Map {
"range": [ "range": [
125, 125,
150, 150,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -130,7 +130,7 @@ Map {
"range": [ "range": [
156, 156,
203, 203,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -153,7 +153,7 @@ Map {
"range": [ "range": [
209, 209,
217, 217,
0, true,
], ],
}, },
"edgeIds": [], "edgeIds": [],
@ -177,7 +177,7 @@ Map {
"range": [ "range": [
231, 231,
254, 254,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -212,6 +212,19 @@ Map {
"type": "wall", "type": "wall",
}, },
"UUID-10" => { "UUID-10" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
501,
522,
true,
],
},
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"id": "UUID", "id": "UUID",
"pathIds": [ "pathIds": [
@ -320,7 +333,7 @@ Map {
"range": [ "range": [
260, 260,
299, 299,
0, true,
], ],
}, },
"consumedEdgeId": "UUID", "consumedEdgeId": "UUID",
@ -340,7 +353,7 @@ Map {
"range": [ "range": [
350, 350,
377, 377,
0, true,
], ],
}, },
"id": "UUID", "id": "UUID",
@ -366,7 +379,7 @@ Map {
"range": [ "range": [
383, 383,
398, 398,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -389,7 +402,7 @@ Map {
"range": [ "range": [
404, 404,
420, 420,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -412,7 +425,7 @@ Map {
"range": [ "range": [
426, 426,
473, 473,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -435,7 +448,7 @@ Map {
"range": [ "range": [
479, 479,
487, 487,
0, true,
], ],
}, },
"edgeIds": [], "edgeIds": [],
@ -459,7 +472,7 @@ Map {
"range": [ "range": [
501, 501,
522, 522,
0, true,
], ],
}, },
"edgeIds": [ "edgeIds": [
@ -478,7 +491,6 @@ Map {
"UUID", "UUID",
"UUID", "UUID",
"UUID", "UUID",
"UUID",
], ],
"type": "sweep", "type": "sweep",
}, },
@ -507,14 +519,6 @@ Map {
"type": "wall", "type": "wall",
}, },
"UUID-34" => { "UUID-34" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "start",
"sweepId": "UUID",
"type": "cap",
},
"UUID-35" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"id": "UUID", "id": "UUID",
"pathIds": [], "pathIds": [],
@ -522,42 +526,42 @@ Map {
"sweepId": "UUID", "sweepId": "UUID",
"type": "cap", "type": "cap",
}, },
"UUID-36" => { "UUID-35" => {
"id": "UUID", "id": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"sweepId": "UUID", "sweepId": "UUID",
"type": "sweepEdge", "type": "sweepEdge",
}, },
"UUID-36" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-37" => { "UUID-37" => {
"id": "UUID", "id": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "opposite",
"sweepId": "UUID", "sweepId": "UUID",
"type": "sweepEdge", "type": "sweepEdge",
}, },
"UUID-38" => { "UUID-38" => {
"id": "UUID", "id": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "adjacent",
"sweepId": "UUID", "sweepId": "UUID",
"type": "sweepEdge", "type": "sweepEdge",
}, },
"UUID-39" => { "UUID-39" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-40" => {
"id": "UUID", "id": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"sweepId": "UUID", "sweepId": "UUID",
"type": "sweepEdge", "type": "sweepEdge",
}, },
"UUID-41" => { "UUID-40" => {
"id": "UUID", "id": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",

View File

@ -139,7 +139,6 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [
@ -260,11 +259,13 @@ describe('testing createArtifactGraph', () => {
if (err(extrusion)) throw extrusion if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('sweep') expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6 const firstExtrusionIsACubeIE6Sides = 6
const secondExtrusionIsATriangularPrismIE5Sides = 5 // Each face of the triangular prism (5), but without the bottom cap.
// The engine doesn't generate that.
const secondExtrusionIsATriangularPrism = 4
expect(extrusion.surfaces.length).toBe( expect(extrusion.surfaces.length).toBe(
!index !index
? firstExtrusionIsACubeIE6Sides ? firstExtrusionIsACubeIE6Sides
: secondExtrusionIsATriangularPrismIE5Sides : secondExtrusionIsATriangularPrism
) )
}) })
}) })
@ -660,7 +661,7 @@ describe('testing getArtifactsToUpdate', () => {
sweepId: '', sweepId: '',
codeRef: { codeRef: {
pathToNode: [['body', '']], pathToNode: [['body', '']],
range: [37, 64, 0], range: [37, 64, true],
}, },
}, },
]) ])
@ -673,7 +674,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: [], surfaceIds: [],
edgeIds: [], edgeIds: [],
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -684,7 +685,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String), planeId: expect.any(String),
sweepId: expect.any(String), sweepId: expect.any(String),
codeRef: { codeRef: {
range: [37, 64, 0], range: [37, 64, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
solid2dId: expect.any(String), solid2dId: expect.any(String),
@ -698,7 +699,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: '', surfaceId: '',
edgeIds: [], edgeIds: [],
codeRef: { codeRef: {
range: [70, 86, 0], range: [70, 86, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -709,7 +710,7 @@ describe('testing getArtifactsToUpdate', () => {
planeId: expect.any(String), planeId: expect.any(String),
sweepId: expect.any(String), sweepId: expect.any(String),
codeRef: { codeRef: {
range: [37, 64, 0], range: [37, 64, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
solid2dId: expect.any(String), solid2dId: expect.any(String),
@ -724,7 +725,7 @@ describe('testing getArtifactsToUpdate', () => {
edgeIds: [], edgeIds: [],
surfaceId: '', surfaceId: '',
codeRef: { codeRef: {
range: [260, 299, 0], range: [260, 299, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -735,7 +736,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String), surfaceId: expect.any(String),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [92, 119, 0], range: [92, 119, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
edgeCutId: expect.any(String), edgeCutId: expect.any(String),
@ -757,7 +758,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String), surfaceId: expect.any(String),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [156, 203, 0], range: [156, 203, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -769,7 +770,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -788,7 +789,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String), surfaceId: expect.any(String),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [125, 150, 0], range: [125, 150, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -800,7 +801,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -819,7 +820,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String), surfaceId: expect.any(String),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [92, 119, 0], range: [92, 119, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
edgeCutId: expect.any(String), edgeCutId: expect.any(String),
@ -832,7 +833,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -851,7 +852,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceId: expect.any(String), surfaceId: expect.any(String),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [70, 86, 0], range: [70, 86, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -863,7 +864,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -883,7 +884,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },
@ -903,7 +904,7 @@ describe('testing getArtifactsToUpdate', () => {
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
codeRef: { codeRef: {
range: [231, 254, 0], range: [231, 254, true],
pathToNode: [['body', '']], pathToNode: [['body', '']],
}, },
}, },

View File

@ -1,7 +1,8 @@
import { PathToNode, Program, SourceRange } from 'lang/wasm' import { Expr, PathToNode, Program, SourceRange } from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
export type ArtifactId = string export type ArtifactId = string
@ -34,7 +35,7 @@ export interface PathArtifact extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
interface solid2D extends BaseArtifact { interface Solid2DArtifact extends BaseArtifact {
type: 'solid2D' type: 'solid2D'
pathId: ArtifactId pathId: ArtifactId
} }
@ -61,7 +62,7 @@ interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf: WallArtifact surf: WallArtifact
edges: Array<SweepEdge> edges: Array<SweepEdgeArtifact>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CodeRef codeRef: CodeRef
} }
@ -80,7 +81,7 @@ interface SweepArtifactRich extends BaseArtifact {
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge> edges: Array<SweepEdgeArtifact>
codeRef: CodeRef codeRef: CodeRef
} }
@ -90,6 +91,9 @@ interface WallArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
} }
interface CapArtifact extends BaseArtifact { interface CapArtifact extends BaseArtifact {
type: 'cap' type: 'cap'
@ -99,7 +103,7 @@ interface CapArtifact extends BaseArtifact {
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
} }
interface SweepEdge extends BaseArtifact { interface SweepEdgeArtifact extends BaseArtifact {
type: 'sweepEdge' type: 'sweepEdge'
segId: ArtifactId segId: ArtifactId
sweepId: ArtifactId sweepId: ArtifactId
@ -129,10 +133,10 @@ export type Artifact =
| SweepArtifact | SweepArtifact
| WallArtifact | WallArtifact
| CapArtifact | CapArtifact
| SweepEdge | SweepEdgeArtifact
| EdgeCut | EdgeCut
| EdgeCutEdge | EdgeCutEdge
| solid2D | Solid2DArtifact
export type ArtifactGraph = Map<ArtifactId, Artifact> export type ArtifactGraph = Map<ArtifactId, Artifact>
@ -284,6 +288,7 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId, sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds, pathIds: existingPlane.pathIds,
codeRef,
}, },
}, },
] ]
@ -733,7 +738,7 @@ export function getCapCodeRef(
} }
export function getSolid2dCodeRef( export function getSolid2dCodeRef(
solid2D: solid2D, solid2D: Solid2DArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
@ -757,7 +762,7 @@ export function getWallCodeRef(
} }
export function getSweepEdgeCodeRef( export function getSweepEdgeCodeRef(
edge: SweepEdge, edge: SweepEdgeArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
@ -872,14 +877,201 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
} }
} }
export function isSolid2D(artifact: Artifact): artifact is solid2D { function getPlaneFromPath(
return (artifact as solid2D).pathId !== undefined path: PathArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall'] },
graph
)
if (err(plane)) return plane
return plane
} }
export function isSegment(artifact: Artifact): artifact is SegmentArtifact { function getPlaneFromSegment(
return (artifact as SegmentArtifact).pathId !== undefined segment: SegmentArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
const path = getArtifactOfTypes(
{ key: segment.pathId, types: ['path'] },
graph
)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSolid2D(
solid2D: Solid2DArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | 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 | 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 | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) {
const sweep = getArtifactOfTypes(
{ key: edge.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
} }
export function isSweep(artifact: Artifact): artifact is SweepArtifact { export function getPlaneFromArtifact(
return (artifact as SweepArtifact).pathId !== undefined artifact: Artifact | undefined,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
if (!artifact) return new Error(`Artifact is undefined`)
if (artifact.type === 'plane') return artifact
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
if (artifact.type === 'solid2D') return getPlaneFromSolid2D(artifact, graph)
if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
if (artifact.type === 'sweepEdge')
return getPlaneFromSweepEdge(artifact, graph)
return new Error(`Artifact type ${artifact.type} does not have a plane`)
}
const isExprSafe = (index: number): boolean => {
const expr = kclManager.ast.body?.[index]
if (!expr) {
return false
}
if (expr.type === 'ImportStatement' || expr.type === 'ReturnStatement') {
return false
}
if (expr.type === 'VariableDeclaration') {
const init = expr.declaration?.init
if (!init) return false
if (init.type === 'CallExpression') {
return false
}
if (init.type === 'BinaryExpression' && isNodeSafe(init)) {
return true
}
if (init.type === 'Literal' || init.type === 'MemberExpression') {
return true
}
}
return false
}
const onlyConsecutivePaths = (
orderedNodePaths: PathToNode[],
originalPath: PathToNode
): PathToNode[] => {
const originalIndex = Number(
orderedNodePaths.find(
(path) => path[1][0] === originalPath[1][0]
)?.[1]?.[0] || 0
)
const minIndex = Number(orderedNodePaths[0][1][0])
const maxIndex = Number(orderedNodePaths[orderedNodePaths.length - 1][1][0])
const pathIndexMap: any = {}
orderedNodePaths.forEach((path) => {
const bodyIndex = Number(path[1][0])
pathIndexMap[bodyIndex] = path
})
const safePaths: PathToNode[] = []
// traverse expressions in either direction from the profile selected
// when the user entered sketch mode
for (let i = originalIndex; i <= maxIndex; i++) {
if (pathIndexMap[i]) {
safePaths.push(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
for (let i = originalIndex - 1; i >= minIndex; i--) {
if (pathIndexMap[i]) {
safePaths.unshift(pathIndexMap[i])
} else if (!isExprSafe(i)) {
break
}
}
return safePaths
}
export function getPathsFromPlaneArtifact(planeArtifact: PlaneArtifact) {
const nodePaths: PathToNode[] = []
for (const pathId of planeArtifact.pathIds) {
const path = engineCommandManager.artifactGraph.get(pathId)
if (!path) continue
if ('codeRef' in path && path.codeRef) {
// TODO should figure out why upstream the path is bad
const isNodePathBad = path.codeRef.pathToNode.length < 2
nodePaths.push(
isNodePathBad
? getNodePathFromSourceRange(kclManager.ast, path.codeRef.range)
: path.codeRef.pathToNode
)
}
}
return onlyConsecutivePaths(nodePaths, nodePaths[0])
}
export function getPathsFromArtifact({
sketchPathToNode,
artifact,
}: {
sketchPathToNode: PathToNode
artifact?: Artifact
}): PathToNode[] | Error {
const plane = getPlaneFromArtifact(
artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return plane
const paths = getArtifactsOfTypes(
{ keys: plane.pathIds, types: ['path'] },
engineCommandManager.artifactGraph
)
let nodePaths = [...paths.values()]
.map((path) => path.codeRef.pathToNode)
.sort((a, b) => Number(a[1][0]) - Number(b[1][0]))
return onlyConsecutivePaths(nodePaths, sketchPathToNode)
}
function isNodeSafe(node: Expr): boolean {
if (node.type === 'Literal' || node.type === 'MemberExpression') {
return true
}
if (node.type === 'BinaryExpression') {
return isNodeSafe(node.left) && isNodeSafe(node.right)
}
return false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 568 KiB

View File

@ -1,4 +1,11 @@
import { defaultSourceRange, SourceRange } from 'lang/wasm' import {
defaultRustSourceRange,
defaultSourceRange,
Program,
RustSourceRange,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
@ -1302,8 +1309,8 @@ export enum EngineCommandManagerEvents {
interface PendingMessage { interface PendingMessage {
command: EngineCommand command: EngineCommand
range: SourceRange range: RustSourceRange
idToRangeMap: { [key: string]: SourceRange } idToRangeMap: { [key: string]: RustSourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]> promise: Promise<[Models['WebSocketResponse_type']]>
@ -1399,7 +1406,6 @@ export class EngineCommandManager extends EventTarget {
} }
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {} private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {} private onEngineConnectionClosed = () => {}
@ -1432,7 +1438,6 @@ export class EngineCommandManager extends EventTarget {
height, height,
token, token,
makeDefaultPlanes, makeDefaultPlanes,
modifyGrid,
settings = { settings = {
pool: null, pool: null,
theme: Themes.Dark, theme: Themes.Dark,
@ -1452,14 +1457,12 @@ export class EngineCommandManager extends EventTarget {
height: number height: number
token?: string token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes> makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString settings?: SettingsViaQueryString
}) { }) {
if (settings) { if (settings) {
this.settings = settings this.settings = settings
} }
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return return
} }
@ -1539,21 +1542,15 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
// We want modify the grid first because we don't want it to flash. await this.initPlanes()
// Ideally these would already be default hidden in engine (TODO do setIsStreamReady(true)
// that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
setIsStreamReady(true)
// Other parts of the application should use this to react on scene ready. // Other parts of the application should use this to react on scene ready.
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, { new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection, detail: this.engineConnection,
}) })
) )
})
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
@ -2003,7 +2000,7 @@ export class EngineCommandManager extends EventTarget {
{ {
command, command,
idToRangeMap: {}, idToRangeMap: {},
range: defaultSourceRange(), range: defaultRustSourceRange(),
}, },
true // isSceneCommand true // isSceneCommand
) )
@ -2034,9 +2031,9 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('rangeStr is undefined')) return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined) if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined')) return Promise.reject(new Error('commandStr is undefined'))
const range: SourceRange = JSON.parse(rangeStr) const range: RustSourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr) const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } = const idToRangeMap: { [key: string]: RustSourceRange } =
JSON.parse(idToRangeStr) JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger // Current executeAst is stale, going to interrupt, a new executeAst will trigger
@ -2079,10 +2076,14 @@ export class EngineCommandManager extends EventTarget {
if (message.command.type === 'modeling_cmd_req') { if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({ this.orderedCommands.push({
command: message.command, command: message.command,
range: message.range, range: sourceRangeFromRust(message.range),
}) })
} else if (message.command.type === 'modeling_cmd_batch_req') { } else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => { message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = { const cmd: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: req.cmd_id, cmd_id: req.cmd_id,
@ -2090,7 +2091,7 @@ export class EngineCommandManager extends EventTarget {
} }
this.orderedCommands.push({ this.orderedCommands.push({
command: cmd, command: cmd,
range: message.idToRangeMap[req.cmd_id || ''], range,
}) })
}) })
} }
@ -2109,30 +2110,23 @@ export class EngineCommandManager extends EventTarget {
* When an execution takes place we want to wait until we've got replies for all of the commands * When an execution takes place we want to wait until we've got replies for all of the commands
* When this is done when we build the artifact map synchronously. * When this is done when we build the artifact map synchronously.
*/ */
async waitForAllCommands(useFakeExecutor = false) { waitForAllCommands() {
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise)) return Promise.all(
setTimeout(() => { Object.values(this.pendingCommands).map((a) => a.promise)
// the ast is wrong without this one tick timeout. )
// an example is `Solids should be select and deletable` e2e test will fail }
// because the out of date ast messes with selections updateArtifactGraph(ast: Program) {
// TODO: race condition this.artifactGraph = createArtifactGraph({
if (!this?.kclManager) return orderedCommands: this.orderedCommands,
this.artifactGraph = createArtifactGraph({ responseMap: this.responseMap,
orderedCommands: this.orderedCommands, ast,
responseMap: this.responseMap,
ast: this.kclManager.ast,
})
if (useFakeExecutor) {
// mock executions don't produce an artifactGraph, so this will always be empty
// skipping the below logic to wait for the next real execution
return
}
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
}) })
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
} }
/** /**
@ -2212,15 +2206,6 @@ export class EngineCommandManager extends EventTarget {
}).catch(reportRejection) }).catch(reportRejection)
} }
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible)
}
// Some "objects" have the same source range, such as sketch_mode_start and start_path. // Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type // So when passing a range, we need to also specify the command type
mapRangeToObjectId( mapRangeToObjectId(
@ -2228,7 +2213,11 @@ export class EngineCommandManager extends EventTarget {
commandTypeToTarget: string commandTypeToTarget: string
): string | undefined { ): string | undefined {
for (const [artifactId, artifact] of this.artifactGraph) { for (const [artifactId, artifact] of this.artifactGraph) {
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) { if (
'codeRef' in artifact &&
artifact.codeRef &&
isOverlap(range, artifact.codeRef.range)
) {
if (commandTypeToTarget === artifact.type) return artifactId if (commandTypeToTarget === artifact.type) return artifactId
} }
} }

View File

@ -297,14 +297,20 @@ export const lineTo: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const to = segmentInput.to const to = segmentInput.to
const _node = { ...node } const _node = structuredClone(node)
const nodeMeta = getNodeFromPath<PipeExpression>( const nodeMeta = getNodeFromPath<PipeExpression>(
_node, _node,
pathToNode, pathToNode,
'PipeExpression' 'PipeExpression'
) )
if (err(nodeMeta)) return nodeMeta if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta const varDec = getNodeFromPath<VariableDeclaration>(
_node,
pathToNode,
'VariableDeclaration'
)
if (err(varDec)) return varDec
const dec = varDec.node.declaration
const newVals: [Expr, Expr] = [ const newVals: [Expr, Expr] = [
createLiteral(roundOff(to[0], 2)), createLiteral(roundOff(to[0], 2)),
@ -333,14 +339,20 @@ export const lineTo: 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,
valueUsedInTransform: valueUsedInTransform, valueUsedInTransform: valueUsedInTransform,
} }
} else if (dec.init.type === 'PipeExpression') {
dec.init.body = [...dec.init.body, newLine]
} else { } else {
pipe.body = [...pipe.body, newLine] dec.init = createPipeExpression([dec.init, newLine])
} }
return { return {
modifiedAst: _node, modifiedAst: _node,
@ -663,11 +675,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))
@ -682,7 +694,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,
@ -694,7 +710,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 }) => {
@ -731,11 +751,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)
@ -748,7 +768,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,
@ -760,7 +784,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 }) => {
@ -2145,8 +2173,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
@ -2227,7 +2253,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,

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