Compare commits

...

44 Commits

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

* notarize: true

* Remove signingHashAlgorithms from win

* Fix signtoolOptions props after migration

* Disable branch build

* Bump more

* Add back CSC_FOR_PULL_REQUEST

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

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

* Another CSC_FOR_PULL_REQUEST

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

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

* Revert force prod changes

---------

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

* Add !IS_PLAYWRIGHT

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

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

---------

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

* fix: trying to unflake this test

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

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

---------

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

This reverts commit efe8089b08.

* fix poor 1000ms wait UX

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

* trigger CI

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

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

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

* lint

* lint

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

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

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

* trigger CI

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

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

* partial fixes

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

* Trigger CI

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

* Trigger CI

* Fix up all the tests

* Fix partial execution

* wip

* WIP

* wip

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

* most of the fix for 3 point circle

* get overlays working for circle three point

* fmt

* fix types

* cargo fmt

* add face codef ref for walls and caps

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

* some things needed for multi-profile tests

* bad attempts at fixing rust

* more

* more

* fix rust

* more rust fixes

* overlay fix

* remove duplicate test

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

* lint and typing

* maybe fix a unit test

* small thing

* fix circ dep

* fix unit test

* fix some tests

* fix sweep point-and-click test

* fix more tests and add a fix me

* fix more tests

* fix electron specific test

* tsc

* more test tweaks

* update docs

* commint snaps?

* is clippy happy now?

* clippy again

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

* small bug

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

* fix up state diagram

* fmt

* add draft point for first click of three point circ

* 1 test for three point circle

* 2 test for three point circle

* clean up

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

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

* remove bad doc comment

* remove test skip

* remove onboarding test changes

* Update src/lang/modifyAst.ts

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

* Update output from simulation tests

* Fix to use correct source ranges

This also reduces cloning.

* Change back to skipping face cap none and both

* Update output after changing back to skipping none and both

* Fix clippy warning

* fix profile start snap bug

* add path ids to cap

* fix going into edit sketch

* make other startSketchOn's work

* fix snapshot test

* explain function name

* Update src/lib/rectangleTool.ts

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

* rename error

* remove file tree from diff

* Update src/clientSideScene/segments.ts

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

* nit

* Prevent double write to KCL code on revolve

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

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

* clean up for face codeRef

* fix changing tools part way through circle/rect tools

* fix delete of circle profile

* fix close profiles

* fix closing profile bug (tangentArcTo being ignored)

* remove stale comment

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

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

* get delet working for walls

* make delet of extrusions work for multi profile

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

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

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

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

* docs

* fix circ dep

* tsc

* fix selection enter sketch weirdness

* test fixes

* comment out and fixme for delete related tests

* add skip wins

* try and get last test to pass

---------

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

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

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

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

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

* Make AnnotationName a token that includes the @

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

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
2025-02-13 19:28:19 +00:00
49d52ce94b Remove KclValue::Int (#5369)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-14 08:28:00 +13:00
a572d7b0db Fix to not send engine commands when only importing functions or constants (#5371)
* Change to not send engine commands when in isolated mode

* Update output since not sending engine commands
2025-02-12 22:37:34 -05:00
1f405b9327 Remove annotations from non-code-meta and store them in nodes (#5360)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-13 16:17:09 +13:00
0874891dd0 pass thru the kcl version (#5366)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-02-12 18:55:34 -05:00
59d0e079a1 Make ProgramMemory and the internals of ExecState private (#5364)
* Make ProgramMemory and the internals of ExecState private

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* snapshot test changes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-13 11:59:57 +13:00
b9862baed0 bump kittycad.ts & pull thru project_name (#5359)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-02-12 16:44:47 -06:00
592c5259c6 KCL: Update patternTransform and 2d to use kwargs (#5348)
* KCL: Update patternTransform and 2d to use kwargs

* Update docs

* Convert segment functions to use keyword args

* Regenerate docs, change branch of kcl-samples
2025-02-12 21:47:02 +00:00
950f5cebfd Options and docs for foreign imports (#5351)
* Annotations for imports of foreign files

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Document foreign imports

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-13 06:24:27 +13:00
4c0ea136e0 Remove artifact graph mind maps (#5349)
* Remove artifact graph mind maps

* Delete mind map snap files
2025-02-11 19:28:00 -05:00
81c0035adc Remove unneeded AST test in TypeScript (#5343) 2025-02-11 23:55:05 +00:00
c68e5d7774 KCL: Linear/circular pattern in stdlib should use kwargs (#5315)
Part of https://github.com/KittyCAD/modeling-app/issues/4600
2025-02-11 16:06:47 -06:00
ed8a0e4aaa Fix formatting to preserve annotations when program is otherwise empty (#5310)
* Fix formatting to preserve annotations when program is otherwise empty

* Change to not allocate extra String
2025-02-11 21:58:48 +00:00
322f398049 ProgramMemory refactor - eliminate copies of memory (#5273)
* refactor program memory for encapsulation and remove special treatment of return values

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor ProgramMemory to isolate mutation

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor ProgramMemory

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Generated output

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Cache the result of executing modules for their items

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Compress envs when popped

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Remove the last traces of geometry from the memory module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* docs

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fixups

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Frontend

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Improve Environment GC

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fixup mock execution frontend and interpreter, misc bug fixes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 21:22:56 +00:00
bc4d254297 Release the Share Link feature (#5228)
* WIP: Release the Share Link feature
Fixes #5227

* Clean up
2025-02-11 16:06:24 -05:00
10b7a772bf Release kcl-test-server 0.1.21 (#5345) 2025-02-11 14:05:31 -06:00
86ba586318 Release new kcl-lib and derive-docs (#5344) 2025-02-11 14:56:23 -05:00
cc313afb89 Remove DEV=false from .env.production (#5342)
* Remove DEV=false from .env.production

* And process.env.DEV too

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

* Bad bot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 13:19:23 -05:00
d0c8311e41 Fix deep links on Linux .desktop files (#5337)
* Fix deep links on Linux .desktop files
Fixes #5325

* Fix lint
2025-02-11 17:25:46 +00:00
28311d160a Fix: stable E2E tests on ubuntu localhost (#5269)
* fix: Needed to increase timeout for this test. waitForExecutionDone does not work since there are errors in the KCL code

* fix: again another wait for execution does not work

* fix: bug that desyncs codeManager, executeCode, lspPlugin

* fix: fixing react race condition on parsing numeric literals in command bar on open

* fix: adding comment to clarify the gotcha

* fix: saving off debugging...

* fix: added wait for execution done

* fix: removing testing code

* fix: adding wait for execution done

* fix: adding execution done wait

* fix: only fixes the chamfer point and click delete

* fix: add 1250ms before every progresscmdbar, removed model loaded scene state that is flaky, added toast if someone presses the command bar too quickly

* fix: adding a wait for execution

* fix: updating wait for execution

* fix: wait for execution done

* fix: wait for execution done

* fix: not waiting for scene, not waiting for command bar

* fix: restoring name

* fix: auto fixes

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

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

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

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

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

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

* fix: bad prompt fix

* fix: Fixed testing selections with wait

* fix: last wait fix

* fix: trying to resolve more flakes when running with more workers

* chore: adding a skipLocalEngine tag

* fix: fixing test when using local engine

* fix: codespell

* fix: big if true

* chore: skipping one more local engine tests that fails

* fix: auto fix:

* fix: restoring this testing code

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 12:13:25 -05:00
162856064b Dedicated installation path for nightly builds on Windows (#5331)
* WIP: Fix deep links on Windows for the nightly app
Fixes #5326

* Add nsis.include flip

* Clean up for PR
2025-02-11 11:18:33 -05:00
max
652f82e8c3 Fix Shift-Click to Deselect Edges/Faces (#5289)
* fix selections and support deselections

* test deselections

* rever commit

* undo sloppy merge

* re-commit the test

* make ubuntu happy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* support initial sketch

* test sketch

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 08:16:48 -05:00
d4d9bf6c7f Add trivia to AST Node (#5330)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 05:30:14 +00:00
0e9d37c0a0 Factor out a modules module and add main.rs (#5329)
* Factor out a modules module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Very simple cargo run

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 13:52:46 +13:00
9c18060d73 UoM types in AST, unnamed annotations, and syntax for importing from std (#5324)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 11:26:14 +13:00
e2be66b024 Fix: inserting revolve command accounts for axis dependency (#5231)
fix: inserting revolve with axis dependency
2025-02-10 14:01:06 -06:00
1bb372b642 Add helix to the artifact graph (#5317)
* Add helix simulation test

* Update output for new test

* Add helix to the artifact graph

* Update output since adding helix to the graph

* Fix helix selection-based deletion (#5323)

* Fix helix selection-based deletion

* Run e2e tests on all PRs

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

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

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-10 19:24:00 +00:00
627fbda671 Bump @types/node from 22.10.6 to 22.13.1 in /packages/codemirror-lsp-client (#5285) 2025-02-10 09:29:26 -05:00
74bdb72edc Bump serde_json from 1.0.137 to 1.0.138 in /src/wasm-lib in the serde-dependencies group (#5222) 2025-02-10 05:00:17 +00:00
a9182bf357 Bump typescript-eslint from 8.19.1 to 8.23.0 (#5322) 2025-02-10 04:28:21 +00:00
5f14023404 Split up creating a new simulation test and running it (#5316) 2025-02-08 00:28:39 -05:00
9a3ac64603 KCL: Appearance stdlib fn is now kwargs (#5308) 2025-02-07 16:36:51 -06:00
67e60cb832 fix: remove noisy log line in linux (#5306)
fix: remove noisy log line

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2025-02-07 19:23:06 +00:00
110037df79 Fix: update sweep snapshots code after kwargs merge (#5307)
* Fix: update sweep snapshots code after kwargs merge

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 13:51:28 -05:00
30b1dae38a KCL: Sweep stdlib fn now uses keyword args (#5300)
Before:
```
|> sweep({ path = myPath }, %)
```

After:
```
|> sweep(path = myPath)
```
2025-02-07 18:35:04 +00:00
f20fc5b467 Remove units from share link flow, enable it on nightly (#5304)
* Remove units from the share link flow

They should rely on inline settings annotations

* @pierremtb's fix to turn on the menu item in nightly

* fmt

* Don't show web banner if the create file query param is present

* Change copy to 'Share current part (via Zoo link)'

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-02-07 18:19:56 +00:00
551 changed files with 257199 additions and 187850 deletions

View File

@ -1,5 +1,4 @@
NODE_ENV=production NODE_ENV=production
DEV=false
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev

View File

@ -3,7 +3,6 @@ on:
push: push:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ lastSegX(sketch: Sketch) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | A sketch is a collection of paths. | Yes | | `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | The sketch whose line segment is being queried | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ lastSegY(sketch: Sketch) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | A sketch is a collection of paths. | Yes | | `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | The sketch whose line segment is being queried | Yes |
### Returns ### Returns

File diff suppressed because one or more lines are too long

View File

@ -57,3 +57,55 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
``` ```
import increment as inc, decrement as dec from "util.kcl" import increment as inc, decrement as dec from "util.kcl"
``` ```
## Importing files from other CAD systems
`import` can also be used to import files from other CAD systems. The format of the statement is the
same as for KCL files. You can only import the whole file, not items from it. E.g.,
```
import "tests/inputs/cube.obj"
// Use `cube` just like a KCL object.
```
```
import "tests/inputs/cube-2.sldprt" as cube
// Use `cube` just like a KCL object.
```
You can make the file format explicit using a format attribute (useful if using a different
extension), e.g.,
```
@(format = obj)
import "tests/inputs/cube"
```
For formats lacking unit data (such as STL, OBJ, or PLY files), the default
unit of measurement is millimeters. Alternatively you may specify the unit
by using an attirbute. Likewise, you can also specify a coordinate system. E.g.,
```
@(unitLength = ft, coords = opengl)
import "tests/inputs/cube.obj"
```
When importing a GLTF file, the bin file will be imported as well.
Import paths are relative to the current project directory. Imports currently only work when
using the native Modeling App, not in the browser.
### Supported values
File formats: `fbx`, `gltf`/`glb`, `obj`+, `ply`+, `sldprt`, `step`/`stp`, `stl`+. (Those marked with a
'+' support customising the length unit and coordinate system).
Length units: `mm` (the default), `cm`, `m`, `inch`, `ft`, `yd`.
Coordinate systems:
- `zoo` (the default), forward: -Y, up: +Z, handedness: right
- `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left

View File

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

View File

@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch some number of times along a partial or
complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained. complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.
```js ```js
patternCircular2d(data: CircularPattern2dData, sketch_set: SketchSet) -> [Sketch] patternCircular2d(sketch_set: SketchSet, instances: integer, center: [number], arc_degrees: number, rotate_duplicates: bool, use_original?: bool) -> [Sketch]
``` ```
@ -17,8 +17,12 @@ patternCircular2d(data: CircularPattern2dData, sketch_set: SketchSet) -> [Sketch
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `data` | [`CircularPattern2dData`](/docs/kcl/types/CircularPattern2dData) | Data for a circular pattern on a 2D sketch. | Yes | | `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch(es) to pattern | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `center` | `[number]` | The center about which to make the pattern. This is a 2D vector. | Yes |
| `arc_degrees` | `number` | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
| `rotate_duplicates` | `bool` | Whether or not to rotate the duplicates as they are copied. | Yes |
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
### Returns ### Returns
@ -34,12 +38,12 @@ exampleSketch = startSketchOn('XZ')
|> line(end = [-1, 0]) |> line(end = [-1, 0])
|> line(end = [0, -5]) |> line(end = [0, -5])
|> close() |> close()
|> patternCircular2d({ |> patternCircular2d(
center = [0, 0], center = [0, 0],
instances = 13, instances = 13,
arcDegrees = 360, arcDegrees = 360,
rotateDuplicates = true rotateDuplicates = true,
}, %) )
example = extrude(exampleSketch, length = 1) example = extrude(exampleSketch, length = 1)
``` ```

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
of distance between each repetition, some specified number of times. of distance between each repetition, some specified number of times.
```js ```js
patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet, use_original?: bool) -> [Sketch] patternLinear2d(sketch_set: SketchSet, instances: integer, distance: number, axis: [number], use_original?: bool) -> [Sketch]
``` ```
@ -17,9 +17,11 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet, use_original?:
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `data` | [`LinearPattern2dData`](/docs/kcl/types/LinearPattern2dData) | Data for a linear pattern on a 2D sketch. | Yes | | `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `use_original` | `bool` | | No | | `distance` | `number` | Distance between each repetition. Also known as 'spacing'. | Yes |
| `axis` | `[number]` | The axis of the pattern. A 2D vector. | Yes |
| `use_original` | `bool` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
### Returns ### Returns
@ -31,11 +33,7 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet, use_original?:
```js ```js
exampleSketch = startSketchOn('XZ') exampleSketch = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
|> patternLinear2d({ |> patternLinear2d(axis = [1, 0], instances = 7, distance = 4)
axis = [1, 0],
instances = 7,
distance = 4
}, %)
example = extrude(exampleSketch, length = 1) example = extrude(exampleSketch, length = 1)
``` ```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ segAng(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segEnd(tag: TagIdentifier) -> [number]
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segEndX(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segEndY(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segLen(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segStart(tag: TagIdentifier) -> [number]
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segStartX(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -17,7 +17,7 @@ segStartY(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ tangentToEnd(tag: TagIdentifier) -> number
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes | | `tag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The line segment being queried by its tag | Yes |
### Returns ### Returns

View File

@ -0,0 +1,16 @@
---
title: "EnvironmentRef"
excerpt: ""
layout: manual
---
[`SnapshotRef`](/docs/kcl/types/SnapshotRef)

View File

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

View File

@ -59,23 +59,7 @@ Any KCL value.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Number`| | No | | `type` |enum: `Number`| | No |
| `value` |`number`| | No | | `value` |`number`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `ty` |[`NumericType`](/docs/kcl/types/NumericType)| Any KCL value. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Int`| | No |
| `value` |`integer`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -311,7 +295,7 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `memory` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
@ -351,6 +335,23 @@ Data for an imported geometry.
---- ----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Tombstone`| | No |
| `value` |`null`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

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

View File

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

View File

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

View File

@ -17,6 +17,5 @@ layout: manual
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `environments` |`[` [`Environment`](/docs/kcl/types/Environment) `]`| | No | | `environments` |`[` [`Environment`](/docs/kcl/types/Environment) `]`| | No |
| `currentEnv` |`integer`| | No | | `currentEnv` |`integer`| | No |
| `return` |[`KclValue`](/docs/kcl/types/KclValue)| | No |

View File

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

View File

@ -0,0 +1,16 @@
---
title: "SnapshotRef"
excerpt: "An index pointing to a snapshot within a specific (unspecified) environment."
layout: manual
---
An index pointing to a snapshot within a specific (unspecified) environment.
**Type:** `integer` (`uint`)

View File

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

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

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

View File

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

View File

@ -19,6 +19,8 @@ test.describe(
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
// FIXME: Cannot use scene.waitForExecutionDone() since there is no KCL code
await page.waitForTimeout(10000)
await u.openDebugPanel() await u.openDebugPanel()
const coord = const coord =
@ -41,8 +43,7 @@ test.describe(
}, },
} }
const code = `sketch001 = startSketchOn('${plane}') const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel() await u.openDebugPanel()

View File

@ -10,6 +10,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
test('Typing KCL errors induces a badge on the code pane button', async ({ test('Typing KCL errors induces a badge on the code pane button', async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -30,11 +31,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Ensure no badge is present // Ensure no badge is present
const codePaneButtonHolder = page.locator('#code-button-holder') const codePaneButtonHolder = page.locator('#code-button-holder')
@ -175,7 +172,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await page.waitForTimeout(1000) // FIXME: await scene.waitForExecutionDone() does not work. It still fails.
// I needed to increase this timeout to get this to pass.
await page.waitForTimeout(10000)
// Ensure badge is present // Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder') const codePaneButtonHolder = page.locator('#code-button-holder')
@ -187,7 +186,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
// click in the editor to focus it // click in the editor to focus it
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.waitForTimeout(500) await page.waitForTimeout(2000)
// go to the start of the editor and enter more text which will trigger // go to the start of the editor and enter more text which will trigger
// a lint error. // a lint error.
@ -204,8 +203,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home') await page.keyboard.press('Home')
await page.keyboard.type('foo_bar = 1') await page.keyboard.type('foo_bar = 1')
await page.waitForTimeout(500) await page.waitForTimeout(2000)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.waitForTimeout(2000)
// ensure we have a lint error // ensure we have a lint error
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()

View File

@ -174,6 +174,9 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
// FIXME: No KCL code, unable to wait for engine execution
await page.waitForTimeout(10000)
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()

View File

@ -9,8 +9,8 @@ import fsp from 'fs/promises'
test( test(
'export works on the first try', 'export works on the first try',
{ tag: '@electron' }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context }, testInfo) => { async ({ page, context, scene }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -118,8 +118,9 @@ test(
// Close the file pane // Close the file pane
await u.closeFilePanel() await u.closeFilePanel()
// wait for it to finish executing (todo: make this more robust) // FIXME: await scene.waitForExecutionDone() does not work. The modeling indicator stays in -receive-reliable and not execution done
await page.waitForTimeout(1000) await page.waitForTimeout(10000)
// expect zero errors in guter // expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()

View File

@ -490,6 +490,11 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
// FIXME: lsp errors do not propagate to the frontend until engine is connected and code is executed
// This timeout is to wait for engine connection. LSP and code execution errors should be handled differently
// LSP can emit errors as fast as it waits and show them in the editor
await page.waitForTimeout(10000)
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -641,7 +646,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
width = 0.500 width = 0.500
height = 0.500 height = 0.500
dia = 4 dia = 4
fn squareHole = (l, w) => { fn squareHole = (l, w) => {
squareHoleSketch = startSketchOn('XY') squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %) |> startProfileAt([-width / 2, -length / 2], %)
@ -714,7 +719,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> line(end = [0, -10], tag = $revolveAxis) |> line(end = [0, -10], tag = $revolveAxis)
|> close() |> close()
|> extrude(length = 10) |> extrude(length = 10)
sketch001 = startSketchOn(box, revolveAxis) sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line(end = [0, -10]) |> line(end = [0, -10])

View File

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

View File

@ -112,6 +112,9 @@ export class CmdBarFixture {
* and assumes we are past the `pickCommand` step. * and assumes we are past the `pickCommand` step.
*/ */
progressCmdBar = async (shouldFuzzProgressMethod = true) => { progressCmdBar = async (shouldFuzzProgressMethod = true) => {
// FIXME: Progressing the command bar is a race condition. We have an async useEffect that reports the final state via useCalculateKclExpression. If this does not run quickly enough, it will not "fail" the continue because you can press continue if the state is not ready. E2E tests do not know this.
// Wait 1250ms to assume the await executeAst of the KCL input field is finished
await this.page.waitForTimeout(1250)
if (shouldFuzzProgressMethod || Math.random() > 0.5) { if (shouldFuzzProgressMethod || Math.random() > 0.5) {
const arrowButton = this.page.getByRole('button', { const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue', name: 'arrow right Continue',
@ -128,6 +131,23 @@ export class CmdBarFixture {
} }
} }
// Added data-testid to the command bar buttons
// command-bar-continue are the buttons to go to the next step
// does not include the submit which is the final button press
// aka the right arrow button
continue = async () => {
const continueButton = this.page.getByTestId('command-bar-continue')
await continueButton.click()
}
// Added data-testid to the command bar buttons
// command-bar-submit is the button for the final step to submit
// the command bar flow aka the checkmark button.
submit = async () => {
const submitButton = this.page.getByTestId('command-bar-submit')
await submitButton.click()
}
openCmdBar = async (selectCmd?: 'promptToEdit') => { openCmdBar = async (selectCmd?: 'promptToEdit') => {
// TODO why does this button not work in electron tests? // TODO why does this button not work in electron tests?
// await this.cmdBarOpenBtn.click() // await this.cmdBarOpenBtn.click()

View File

@ -1,6 +1,6 @@
import type { Page, Locator } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { uuidv4 } from 'lib/utils' import { isArray, uuidv4 } from 'lib/utils'
import { import {
closeDebugPanel, closeDebugPanel,
doAndWaitForImageDiff, doAndWaitForImageDiff,
@ -9,13 +9,15 @@ import {
sendCustomCmd, sendCustomCmd,
} from '../test-utils' } from '../test-utils'
type mouseParams = { type MouseParams = {
pixelDiff?: number pixelDiff?: number
shouldDbClick?: boolean
delay?: number
} }
type mouseDragToParams = mouseParams & { type MouseDragToParams = MouseParams & {
fromPoint: { x: number; y: number } fromPoint: { x: number; y: number }
} }
type mouseDragFromParams = mouseParams & { type MouseDragFromParams = MouseParams & {
toPoint: { x: number; y: number } toPoint: { x: number; y: number }
} }
@ -26,12 +28,12 @@ type SceneSerialised = {
} }
} }
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: MouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: mouseDragFromParams dragParams: MouseDragFromParams
) => Promise<void | boolean> ) => Promise<void | boolean>
export class SceneFixture { export class SceneFixture {
@ -77,17 +79,26 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler, DblClickHandler] => ): [ClickHandler, MoveHandler, DblClickHandler] =>
[ [
(clickParams?: mouseParams) => { (clickParams?: MouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
() => this.page.mouse.click(x, y), () =>
clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y, {
delay: clickParams?.delay || 0,
})
: this.page.mouse.click(x, y, {
delay: clickParams?.delay || 0,
}),
clickParams.pixelDiff clickParams.pixelDiff
) )
} }
return this.page.mouse.click(x, y) return clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0 })
: this.page.mouse.click(x, y, { delay: clickParams?.delay || 0 })
}, },
(moveParams?: mouseParams) => { (moveParams?: MouseParams) => {
if (moveParams?.pixelDiff) { if (moveParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -97,7 +108,7 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: mouseParams) => { (clickParams?: MouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -114,7 +125,7 @@ export class SceneFixture {
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [DragToHandler, DragFromHandler] => ): [DragToHandler, DragFromHandler] =>
[ [
(dragToParams: mouseDragToParams) => { (dragToParams: MouseDragToParams) => {
if (dragToParams?.pixelDiff) { if (dragToParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -131,7 +142,7 @@ export class SceneFixture {
targetPosition: { x, y }, targetPosition: { x, y },
}) })
}, },
(dragFromParams: mouseDragFromParams) => { (dragFromParams: MouseDragFromParams) => {
if (dragFromParams?.pixelDiff) { if (dragFromParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
@ -219,7 +230,7 @@ export class SceneFixture {
} }
expectPixelColor = async ( expectPixelColor = async (
colour: [number, number, number], colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) => { ) => {
@ -241,22 +252,36 @@ export class SceneFixture {
} }
} }
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return isArray(colour[0])
}
export async function expectPixelColor( export async function expectPixelColor(
page: Page, page: Page,
colour: [number, number, number], colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) { ) {
let finalValue = colour let finalValue = colour
await expect await expect
.poll(async () => { .poll(
const pixel = (await getPixelRGBs(page)(coords, 1))[0] async () => {
if (!pixel) return null const pixel = (await getPixelRGBs(page)(coords, 1))[0]
finalValue = pixel if (!pixel) return null
return pixel.every( finalValue = pixel
(channel, index) => Math.abs(channel - colour[index]) < diff if (!isColourArray(colour)) {
) return pixel.every(
}) (channel, index) => Math.abs(channel - colour[index]) < diff
)
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
},
{ timeout: 10_000 }
)
.toBeTruthy() .toBeTruthy()
.catch((cause) => { .catch((cause) => {
throw new Error( throw new Error(

View File

@ -23,7 +23,10 @@ export class ToolbarFixture {
helixButton!: Locator helixButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator exitSketchBtn!: Locator
editSketchBtn!: Locator editSketchBtn!: Locator
fileTreeBtn!: Locator fileTreeBtn!: Locator
@ -53,7 +56,10 @@ export class ToolbarFixture {
this.helixButton = page.getByTestId('helix') this.helixButton = page.getByTestId('helix')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit') this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch') this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]') this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -119,6 +125,25 @@ export class ToolbarFixture {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
} }
} }
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down Corner rectangle:' })
.click()
await expect(
this.page.getByTestId('dropdown-center-rectangle')
).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click()
}
selectCircleThreePoint = async () => {
await this.page
.getByRole('button', { name: 'caret down Center circle:' })
.click()
await expect(
this.page.getByTestId('dropdown-circle-three-points')
).toBeVisible()
await this.page.getByTestId('dropdown-circle-three-points').click()
}
async closePane(paneId: SidebarType) { async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -29,11 +29,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => { await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
await scene.clickNoWhere() // FIXME: Do not click, clicking removes the activeLines in future checks
// await scene.clickNoWhere()
await expect(toolbar.extrudeButton).toBeEnabled() await expect(toolbar.extrudeButton).toBeEnabled()
}) })
@ -199,6 +201,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file) }, file)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -216,18 +219,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) 'startProfileAt([205.96, 254.59], sketch002)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
105.26 |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
], %, $rectangleSegmentB001) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -248,19 +246,15 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) 'startProfileAt([-209.64, 255.28], sketch003)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
segAng(rectangleSegmentA003) - 90, |>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
106.84 |>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
], %, $rectangleSegmentB002) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -273,19 +267,14 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch004 = startSketchOn(extrude001, seg05)',
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) 'startProfileAt([82.57, 322.96], sketch004)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
segAng(rectangleSegmentA003) - 90, |>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
106.84 |>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
], %, $rectangleSegmentB002) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -298,104 +287,98 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch005 = startSketchOn(extrude001, seg06)', 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) 'startProfileAt([-23.43, 19.69], sketch005)',
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|> angledLine([ |>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
segAng(rectangleSegmentA005) - 90, |>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
84.07 |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
], %, $rectangleSegmentB004) |>close()`,
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(sketch001, length = 100)
|> chamfer({
length = 30,
tags = [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|> chamfer({
length = 30,
tags = [getNextAdjacentEdge(seg02)]
}, %, $seg05)
|> chamfer({
length = 30,
tags = [getNextAdjacentEdge(yo)]
}, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06)
profile004 = startProfileAt([-23.43, 19.69], sketch005)
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch004 = startSketchOn(extrude001, seg05)
profile003 = startProfileAt([82.57, 322.96], sketch004)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) - 90,
103.07
], %)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch003 = startSketchOn(extrude001, seg04)
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(extrude001, seg03)
profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] `,
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(sketch001, length = 100)
|> chamfer({
length = 30,
tags = [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|> chamfer({
length = 30,
tags = [getNextAdjacentEdge(seg02)]
}, %, $seg05)
|> chamfer({
length = 30,
tags = [getNextAdjacentEdge(yo)]
}, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43,19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57,322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) - 90,
103.07
], %, $rectangleSegmentB003)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64,255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96,254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`,
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) })
@ -422,6 +405,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file) }, file)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -439,18 +423,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet:
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) 'startProfileAt([205.96, 254.59], sketch002)',
|> angledLine([ afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
segAng(rectangleSegmentA002) - 90, |>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
105.26 |>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
], %, $rectangleSegmentB001) |>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|> angledLine([ |>close()`,
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
}) })
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
@ -480,17 +459,17 @@ chamf = chamfer({
] ]
}, %) }, %)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %) profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
], %, $rectangleSegmentB001) ], %)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002), segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002) -segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001) ], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute=[profileStartX(%), profileStartY(%)])
|> close() |> close()
`, `,
{ shouldNormalise: true } { shouldNormalise: true }
@ -557,10 +536,10 @@ sketch002 = startSketchOn(extrude001, seg03)
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`, pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`, afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
} }
await test.step(`Start a sketch on the XZ plane`, async () => { await test.step(`Start a sketch on the XZ plane`, async () => {
@ -601,6 +580,7 @@ sketch002 = startSketchOn(extrude001, seg03)
expectedCodeSnippets.afterSegmentDraggedOnYAxis expectedCodeSnippets.afterSegmentDraggedOnYAxis
) )
}) })
await editor.page.waitForTimeout(1000)
}) })
test(`Verify user can double-click to edit a sketch`, async ({ test(`Verify user can double-click to edit a sketch`, async ({
@ -712,6 +692,330 @@ openSketch = startSketchOn('XY')
}) })
}) })
test(`Shift-click to select and deselect edges and faces`, async ({
context,
page,
homePage,
scene,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0])
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(%, length = -12)`
// Locators
const upperEdgeLocation = { x: 600, y: 192 }
const lowerEdgeLocation = { x: 600, y: 383 }
const faceLocation = { x: 630, y: 290 }
// Click helpers
const [clickOnUpperEdge] = scene.makeMouseHelpers(
upperEdgeLocation.x,
upperEdgeLocation.y
)
const [clickOnLowerEdge] = scene.makeMouseHelpers(
lowerEdgeLocation.x,
lowerEdgeLocation.y
)
const [clickOnFace] = scene.makeMouseHelpers(faceLocation.x, faceLocation.y)
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220] // varies from 192 to 255
const edgeColorYellow: [number, number, number] = [251, 251, 40] // vaies from 12 to 67
const faceColorGray: [number, number, number] = [168, 168, 168]
const faceColorYellow: [number, number, number] = [155, 155, 155]
const tolerance = 40
const timeout = 150
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Wait for the scene and stream to load
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
})
await test.step('Select and deselect a single edge', async () => {
await test.step('Click the edge', async () => {
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
await clickOnUpperEdge()
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
})
await test.step('Shift-click the same edge to deselect', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnUpperEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
})
})
await test.step('Select and deselect multiple objects', async () => {
await test.step('Select both edges and the face', async () => {
await test.step('Select the upper edge', async () => {
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
await clickOnUpperEdge()
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
})
await test.step('Select the lower edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
lowerEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnLowerEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorYellow,
lowerEdgeLocation,
tolerance
)
})
await test.step('Select the face (Shift-click)', async () => {
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnFace()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
})
})
await test.step('Deselect them one by one', async () => {
await test.step('Deselect the face (Shift-click)', async () => {
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnFace()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
})
await test.step('Deselect the lower edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorYellow,
lowerEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnLowerEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
lowerEdgeLocation,
tolerance
)
})
await test.step('Deselect the upper edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnUpperEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
})
})
})
})
test(`Shift-click to select and deselect sketch segments`, async ({
page,
homePage,
scene,
editor,
}) => {
// Locators
const firstPointLocation = { x: 200, y: 100 }
const secondPointLocation = { x: 800, y: 100 }
const thirdPointLocation = { x: 800, y: 400 }
const fristSegmentLocation = { x: 750, y: 100 }
const secondSegmentLocation = { x: 800, y: 150 }
const planeLocation = { x: 700, y: 200 }
// Click helpers
const [clickFirstPoint] = scene.makeMouseHelpers(
firstPointLocation.x,
firstPointLocation.y
)
const [clickSecondPoint] = scene.makeMouseHelpers(
secondPointLocation.x,
secondPointLocation.y
)
const [clickThirdPoint] = scene.makeMouseHelpers(
thirdPointLocation.x,
thirdPointLocation.y
)
const [clickFirstSegment] = scene.makeMouseHelpers(
fristSegmentLocation.x,
fristSegmentLocation.y
)
const [clickSecondSegment] = scene.makeMouseHelpers(
secondSegmentLocation.x,
secondSegmentLocation.y
)
const [clickPlane] = scene.makeMouseHelpers(
planeLocation.x,
planeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220]
const edgeColorBlue: [number, number, number] = [20, 20, 200]
const backgroundColor: [number, number, number] = [30, 30, 30]
const tolerance = 40
const timeout = 150
// Setup
await test.step(`Initial test setup`, async () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Wait for the scene and stream to load
await scene.expectPixelColor(
backgroundColor,
secondPointLocation,
tolerance
)
})
await test.step('Select and deselect a single sketch segment', async () => {
await test.step('Get into sketch mode', async () => {
await editor.closePane()
await page.waitForTimeout(timeout)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(timeout)
await clickPlane()
await page.waitForTimeout(1000)
})
await test.step('Draw sketch', async () => {
await clickFirstPoint()
await page.waitForTimeout(timeout)
await clickSecondPoint()
await page.waitForTimeout(timeout)
await clickThirdPoint()
await page.waitForTimeout(timeout)
})
await test.step('Deselect line tool', async () => {
const btnLine = page.getByTestId('line')
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
if (btnLineAriaPressed === 'true') {
await btnLine.click()
}
await page.waitForTimeout(timeout)
})
await test.step('Select the first segment', async () => {
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
await test.step('Select the second segment (Shift-click)', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the first segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the second segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
})
})
test(`Offset plane point-and-click`, async ({ test(`Offset plane point-and-click`, async ({
context, context,
page, page,
@ -724,9 +1028,12 @@ openSketch = startSketchOn('XY')
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 } const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)` const expectedOutput = `plane001 = offsetPlane('XZ', offset = 5)`
await homePage.goToModelingScene() await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
// The engine may not be connected
await page.waitForTimeout(15000)
await test.step(`Look for the blue of the XZ plane`, async () => { await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15) await scene.expectPixelColor([50, 51, 96], testPoint, 15)
@ -786,13 +1093,14 @@ openSketch = startSketchOn('XY')
}) => { }) => {
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 } const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix(revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5)` const expectedOutput = `helix001 = helix( revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5,)`
const expectedLine = `revolutions=1,`
await homePage.goToModelingScene() await homePage.goToModelingScene()
await test.step(`Look for the red of the default plane`, async () => { // await test.step(`Look for the red of the default plane`, async () => {
await scene.expectPixelColor([96, 52, 52], testPoint, 15) // await scene.expectPixelColor([96, 52, 52], testPoint, 15)
}) // })
await test.step(`Go through the command bar flow`, async () => { await test.step(`Go through the command bar flow`, async () => {
await toolbar.helixButton.click() await toolbar.helixButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
@ -823,7 +1131,7 @@ openSketch = startSketchOn('XY')
await editor.expectEditor.toContain(expectedOutput) await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({ await editor.expectState({
diagnostics: [], diagnostics: [],
activeLines: [expectedOutput], activeLines: [expectedLine],
highlightedCode: '', highlightedCode: '',
}) })
// Red plane is now gone, white helix is there // Red plane is now gone, white helix is there
@ -856,7 +1164,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
` `
@ -942,7 +1250,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002]) loft001 = loft([sketch001, sketch002])
@ -952,6 +1260,7 @@ loft001 = loft([sketch001, sketch002])
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 } const testPoint = { x: 575, y: 200 }
@ -988,7 +1297,7 @@ loft001 = loft([sketch001, sketch002])
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await clickOnSketch2() await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(` await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
`) `)
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Check for sketch 1 // Check for sketch 1
@ -1029,7 +1338,7 @@ sketch002 = startSketchOn('XZ')
testPoint.x - 50, testPoint.x - 50,
testPoint.y testPoint.y
) )
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)' const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
await test.step(`Look for sketch001`, async () => { await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code') await toolbar.closePane('code')
@ -1064,12 +1373,12 @@ sketch002 = startSketchOn('XZ')
await clickOnSketch2() await clickOnSketch2()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await toolbar.openPane('code')
await page.waitForTimeout(500) await page.waitForTimeout(500)
}) })
await test.step(`Confirm code is added to the editor, scene has changed`, async () => { await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15) await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration) await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({ await editor.expectState({
diagnostics: [], diagnostics: [],
@ -1594,16 +1903,7 @@ extrude001 = extrude(sketch001, length = -12)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
}) })
// Test 1: Command bar flow with preselected edges // Test 1: Command bar flow with preselected edges
@ -1828,6 +2128,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// verify modeling scene is loaded // verify modeling scene is loaded
await scene.expectPixelColor( await scene.expectPixelColor(
@ -1950,6 +2251,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 } const testPoint = { x: 575, y: 200 }
@ -2048,6 +2350,7 @@ extrude001 = extrude(sketch001, length = 40)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 } const testPoint = { x: 580, y: 180 }
@ -2145,19 +2448,18 @@ extrude002 = extrude(sketch002, length = 50)
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone() await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 } const testPoint = { x: 580, y: 320 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002' const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)` const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code') await scene.expectPixelColor([113, 113, 113], testPoint, 15)
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
}) })
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => { await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
@ -2221,7 +2523,7 @@ extrude002 = extrude(sketch002, length = 50)
sketch002 = startSketchOn('XZ') sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> xLine(-2000, %) |> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001) sweep001 = sweep(sketch001, path = sketch002)
` `
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)

View File

@ -455,7 +455,7 @@ test.describe('Can export from electron app', () => {
for (const method of exportMethods) { for (const method of exportMethods) {
test( test(
`Can export using ${method}`, `Can export using ${method}`,
{ tag: '@electron' }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')

View File

@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
` `
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => { test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Check the happy path, for basic changing color', () => { test.fixme('Check the happy path, for basic changing color', () => {
const cases = [ const cases = [
{ {
desc: 'User accepts change', desc: 'User accepts change',
@ -60,6 +60,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 } const body1CapCoords = { x: 571, y: 351 }
const greenCheckCoords = { x: 565, y: 345 } const greenCheckCoords = { x: 565, y: 345 }
@ -76,7 +77,9 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
'Submitting to Text-to-CAD API...' 'Submitting to Text-to-CAD API...'
) )
const successToast = page.getByText('Prompt to edit successful') const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' }) const acceptBtn = page.getByRole('button', {
name: 'checkmark Accept',
})
const rejectBtn = page.getByRole('button', { name: 'close Reject' }) const rejectBtn = page.getByRole('button', { name: 'close Reject' })
await test.step('wait for scene to load select body and check selection came through', async () => { await test.step('wait for scene to load select body and check selection came through', async () => {
@ -99,14 +102,16 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible() await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
}) // can take a while
await expect(successToast).toBeVisible() await expect(successToast).toBeVisible()
}) })
await test.step('verify initial change', async () => { await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15) await scene.expectPixelColor(green, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15) await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance({') await editor.expectEditor.toContain('appearance(')
}) })
if (!shouldReject) { if (!shouldReject) {
@ -115,13 +120,13 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible() await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(green, greenCheckCoords, 15) await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance({') await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting // ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta') await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta') await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance({') await editor.expectEditor.not.toContain('appearance(')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15) await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
}) })
} else { } else {
@ -130,7 +135,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible() await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15) await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance({') await editor.expectEditor.not.toContain('appearance(')
}) })
} }
}) })
@ -150,6 +155,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 } const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers( const [clickBody1Cap] = scene.makeMouseHelpers(

View File

@ -192,11 +192,11 @@ extrude001 = extrude(sketch001, length = 50)
|> line(end = [0, -1]) |> line(end = [0, -1])
|> close() |> close()
|> extrude(length = 1) |> extrude(length = 1)
|> patternLinear3d({ |> patternLinear3d(
axis: [1, 0, 1], axis = [1, 0, 1],
repetitions: 3, repetitions = 3,
distance: 6 distance = 6,
}, %)` )`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -251,7 +251,7 @@ extrude001 = extrude(sketch001, length = 50)
|> yLineTo(0, %) |> yLineTo(0, %)
|> close() |> close()
|> |>
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)
shell(exampleSketch, faces = ['end'], thickness = 0.25)` shell(exampleSketch, faces = ['end'], thickness = 0.25)`
) )
@ -306,113 +306,113 @@ extrude001 = extrude(sketch001, length = 50)
|> angledLine({ angle: 50, length: 45 }, %) |> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %) |> yLineTo(0, %)
|> close() |> close()
thing: "blah"`) thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
} }
) )
test('when engine fails export we handle the failure and alert the user', async ({ test(
scene, 'when engine fails export we handle the failure and alert the user',
page, { tag: '@skipLocalEngine' },
homePage, async ({ scene, page, homePage }) => {
}) => { const u = await getUtils(page)
const u = await getUtils(page) await page.addInitScript(
await page.addInitScript( async ({ code }) => {
async ({ code }) => { localStorage.setItem('persistCode', code)
localStorage.setItem('persistCode', code) ;(window as any).playwrightSkipFilePicker = true
;(window as any).playwrightSkipFilePicker = true },
}, { code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } )
)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await u.waitForPageLoad() await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
// expect zero errors in guter // expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// export the model // export the model
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() await expect(exportButton).toBeVisible()
// Click the export button // Click the export button
await exportButton.click() await exportButton.click()
// Click the stl. // Click the stl.
const stlOption = page.getByText('glTF') const stlOption = page.getByText('glTF')
await expect(stlOption).toBeVisible() await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Click the checkbox // Click the checkbox
const submitButton = page.getByText('Confirm Export') const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible() await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`) const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`) const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`) const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(engineErrorToastMessage).toBeVisible() await expect(engineErrorToastMessage).toBeVisible()
// Make sure the exporting toast is gone // Make sure the exporting toast is gone
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
// Click the code editor // Click the code editor
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.waitForTimeout(2000) await page.waitForTimeout(2000)
// Expect the toast to be gone // Expect the toast to be gone
await expect(errorToastMessage).not.toBeVisible() await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible() await expect(engineErrorToastMessage).not.toBeVisible()
// Now add in code that works. // Now add in code that works.
await page.locator('.cm-content').fill(bracket) await page.locator('.cm-content').fill(bracket)
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await scene.waitForExecutionDone() await scene.waitForExecutionDone()
// Now try exporting // Now try exporting
// Click the export button // Click the export button
await exportButton.click() await exportButton.click()
// Click the stl. // Click the stl.
await expect(stlOption).toBeVisible() await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Click the checkbox // Click the checkbox
await expect(submitButton).toBeVisible() await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed. // Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }) await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
await expect(errorToastMessage).not.toBeVisible() await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible() await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`) const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
}) }
)
test( test(
'ensure you can not export while an export is already going', 'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] }, { tag: ['@skipLinux', '@skipWin'] },

File diff suppressed because it is too large Load Diff

View File

@ -444,8 +444,7 @@ test(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += ` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|> startProfileAt([7.19, -9.7], %)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -456,7 +455,9 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) const lineEndClick = () =>
page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await lineEndClick()
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
@ -467,6 +468,15 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click on the end of the profile to continue it
await page.waitForTimeout(300)
await lineEndClick()
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -589,8 +599,7 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
) )
} }
) )
@ -634,8 +643,7 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += ` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|> startProfileAt([7.19, -9.7], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -653,6 +661,10 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
@ -739,8 +751,7 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += ` code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
|> startProfileAt([182.59, -246.32], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -758,6 +769,10 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
@ -1187,14 +1202,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0, angleStart = 0,
radius = 2 radius = 2
}, %) }, %)
|> sweep({ |> sweep(path = sweepPath)
path = sweepPath, |> appearance(
}, %)
|> appearance({
color = "#bb00ff", color = "#bb00ff",
metalness = 90, metalness = 90,
roughness = 90 roughness = 90
}, %) )
` `
) )
}) })
@ -1235,14 +1248,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0, angleStart = 0,
radius = 2 radius = 2
}, %) }, %)
|> sweep({ |> sweep(path = sweepPath)
path = sweepPath, |> appearance(
}, %)
|> appearance({
color = "#bb00ff", color = "#bb00ff",
metalness = 90, metalness = 90,
roughness = 90 roughness = 90
}, %) )
` `
) )
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,222 +1,251 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils' import { commonPoints, getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ test(
page, 'simulate network down and network little widget',
homePage, { tag: '@skipLocalEngine' },
}) => { async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online // This is how we wait until the stream is online
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 }) ).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]') const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible() await expect(networkWidget).toBeVisible()
await networkWidget.hover() await networkWidget.hover()
const networkPopover = page.locator('[data-testid="network-popover"]') const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible() await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up // (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
// Click the network widget // Click the network widget
await networkWidget.click() await networkWidget.click()
// Check the modal opened. // Check the modal opened.
await expect(networkPopover).toBeVisible() await expect(networkPopover).toBeVisible()
// Click off the modal. // Click off the modal.
await page.mouse.click(100, 100) await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible() await expect(networkPopover).not.toBeVisible()
// Turn off the network // Turn off the network
await u.emulateNetworkConditions({ await u.emulateNetworkConditions({
offline: true, offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9 // values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0, latency: 0,
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Problem') await expect(networkToggle).toContainText('Problem')
// Click the network widget // Click the network widget
await networkWidget.click() await networkWidget.click()
// Check the modal opened. // Check the modal opened.
await expect(networkPopover).toBeVisible() await expect(networkPopover).toBeVisible()
// Click off the modal. // Click off the modal.
await page.mouse.click(0, 0) await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible() await expect(networkPopover).not.toBeVisible()
// Turn back on the network // Turn back on the network
await u.emulateNetworkConditions({ await u.emulateNetworkConditions({
offline: false, offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9 // values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0, latency: 0,
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 }) ).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up // (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
}) }
)
test('Engine disconnect & reconnect in sketch mode', async ({ test(
page, 'Engine disconnect & reconnect in sketch mode',
homePage, { tag: '@skipLocalEngine' },
}) => { async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit // TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await homePage.goToModelingScene() await homePage.goToModelingScene()
await u.waitForPageLoad() await u.waitForPageLoad()
await u.openDebugPanel() await u.openDebugPanel()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// select a plane // select a plane
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')` `sketch001 = startSketchOn('XZ')`
) )
await u.closeDebugPanel() await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
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
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
// simulate network down // simulate network down
await u.emulateNetworkConditions({ await u.emulateNetworkConditions({
offline: true, offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9 // values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0, latency: 0,
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Problem') await expect(networkToggle).toContainText('Problem')
// Ensure we are not in sketch mode // Ensure we are not in sketch mode
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible() ).not.toBeVisible()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible() ).toBeVisible()
// simulate network up // simulate network up
await u.emulateNetworkConditions({ await u.emulateNetworkConditions({
offline: false, offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9 // values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0, latency: 0,
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
// Wait for the app to be ready for use // Wait for the app to be ready for use
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 }) ).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up // Expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
await expect(page.getByTestId('loading-stream')).not.toBeAttached() await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane. // Click off the code pane.
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(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(), () => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings' 'default_camera_get_settings'
) )
await page.waitForTimeout(150) await page.waitForTimeout(150)
// Click the line tool // Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(150) await page.waitForTimeout(150)
// Ensure we can continue sketching const camCommand: EngineCommand = {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) type: 'modeling_cmd_req',
await expect.poll(u.normalisedEditorCode) cmd_id: uuidv4(),
.toBe(`sketch001 = startSketchOn('XZ') cmd: {
|> startProfileAt([12.34, -12.34], %) 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
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line(end = [-12.34, 12.34]) |> line(end = [-12.34, 12.34])
`) `)
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.poll(u.normalisedEditorCode) await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %) profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(12.34, %) |> xLine(12.34, %)
|> line(end = [-12.34, 12.34]) |> line(end = [-12.34, 12.34])
|> xLine(-12.34, %) |> xLine(-12.34, %)
`) `)
// 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.
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible() ).toBeVisible()
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') ).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch // Exit sketch
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible() ).not.toBeVisible()
}) }
)
}) })

View File

@ -109,7 +109,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.mouse.move(600, 200) await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' }) await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 }) // Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
}, [-19, -85, -85]) }, [-19, -85, -85])

View File

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

View File

@ -66,33 +66,34 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content')).toHaveText(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|> startProfileAt(${commonPoints.startAt}, %)`) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> startProfileAt(${commonPoints.startAt}, %) |> xLine(${commonPoints.num1}, %)`)
|> xLine(${commonPoints.num1}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|> startProfileAt(${commonPoints.startAt}, %) commonPoints.startAt
|> xLine(${commonPoints.num1}, %) }, sketch001)
|> yLine(${commonPoints.num1 + 0.01}, %)`) |> xLine(${commonPoints.num1}, %)
|> 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
|> xLine(${commonPoints.num1}, %) }, sketch001)
|> yLine(${commonPoints.num1 + 0.01}, %) |> xLine(${commonPoints.num1}, %)
|> xLine(${commonPoints.num2 * -1}, %)`) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -248,82 +249,105 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
}) })
}) })
test('Solids should be select and deletable', async ({ page, homePage }) => { test('Solids should be select and deletable', async ({
page,
homePage,
scene,
}) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %) |> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02) |> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01) |> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude001 = extrude(sketch001, length = 50) extrude001 = extrude(sketch001, length = 50)
sketch005 = startSketchOn(extrude001, 'END') sketch005 = startSketchOn(extrude001, 'END')
|> startProfileAt([23.24, 136.52], %) |> startProfileAt([23.24, 136.52], %)
|> line(end = [-8.44, 36.61]) |> line(end = [-8.44, 36.61])
|> line(end = [49.4, 2.05]) |> line(end = [49.4, 2.05])
|> line(end = [29.69, -46.95]) |> line(end = [29.69, -46.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch003 = startSketchOn(extrude001, seg01) sketch003 = startSketchOn(extrude001, seg01)
|> startProfileAt([21.23, 17.81], %) |> startProfileAt([21.23, 17.81], %)
|> line(end = [51.97, 21.32]) |> line(end = [51.97, 21.32])
|> line(end = [4.07, -22.75]) |> line(end = [4.07, -22.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
sketch002 = startSketchOn(extrude001, seg02) sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-100.54, 16.99], %) |> startProfileAt([-100.54, 16.99], %)
|> line(end = [0, 20.03]) |> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03) |> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude002 = extrude(sketch002, length = 50) extrude002 = extrude(sketch002, length = 50)
sketch004 = startSketchOn(extrude002, seg03) sketch004 = startSketchOn(extrude002, seg03)
|> startProfileAt([57.07, 134.77], %) |> startProfileAt([57.07, 134.77], %)
|> line(end = [-4.72, 22.84]) |> line(end = [-4.72, 22.84])
|> line(end = [28.8, 6.71]) |> line(end = [28.8, 6.71])
|> line(end = [9.19, -25.33]) |> line(end = [9.19, -25.33])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
extrude003 = extrude(sketch004, length = 20) extrude003 = extrude(sketch004, length = 20)
pipeLength = 40 pipeLength = 40
pipeSmallDia = 10 pipeSmallDia = 10
pipeLargeDia = 20 pipeLargeDia = 20
thickness = 0.5 thickness = 0.5
part009 = startSketchOn('XY') part009 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line(end = [thickness, 0]) |> line(end = [thickness, 0])
|> line(end = [0, -1]) |> line(end = [0, -1])
|> angledLineToX({ |> angledLineToX({
angle = 60, angle = 60,
to = pipeSmallDia + thickness to = pipeSmallDia + thickness
}, %) }, %)
|> line(end = [0, -pipeLength]) |> line(end = [0, -pipeLength])
|> angledLineToX({ |> angledLineToX({
angle = -60, angle = -60,
to = pipeLargeDia + thickness to = pipeLargeDia + thickness
}, %) }, %)
|> line(end = [0, -1]) |> line(end = [0, -1])
|> line(end = [-thickness, 0]) |> line(end = [-thickness, 0])
|> line(end = [0, 1]) |> line(end = [0, 1])
|> angledLineToX({ angle = 120, to = pipeSmallDia }, %) |> angledLineToX({ angle = 120, to = pipeSmallDia }, %)
|> line(end = [0, pipeLength]) |> line(end = [0, pipeLength])
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close() |> close()
rev = revolve({ axis: 'y' }, part009) rev = revolve({ axis = 'y' }, part009)
` sketch006 = startSketchOn('XY')
profile001 = circle({
center = [42.91, -70.42],
radius = 17.96
}, sketch006)
profile002 = startProfileAt([86.92, -63.81], sketch006)
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
17.05
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(end = [26.95, 24.21])
|> line(end = [20.91, -28.61])
|> line(end = [32.46, 18.71])
`
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.sendCustomCmd({ await u.sendCustomCmd({
@ -346,9 +370,10 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const revolve = { x: 646, y: 248 } const revolve = { x: 635, y: 253 }
const parentExtrude = { x: 915, y: 133 } const parentExtrude = { x: 915, y: 133 }
const solid2d = { x: 770, y: 167 } const solid2d = { x: 770, y: 167 }
const individualProfile = { x: 694, y: 432 }
// DELETE REVOLVE // DELETE REVOLVE
await page.mouse.click(revolve.x, revolve.y) await page.mouse.click(revolve.x, revolve.y)
@ -365,43 +390,47 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
`rev = revolve({ axis: 'y' }, part009)` `rev = revolve({ axis: 'y' }, part009)`
) )
// DELETE PARENT EXTRUDE // FIXME (commented section below), this test would select a wall that had a sketch on it, and delete the underlying extrude
await page.mouse.click(parentExtrude.x, parentExtrude.y) // and replace the sketch on face with a hard coded custom plane, but since there was a sketch on that plane maybe it
await page.waitForTimeout(100) // should have delete the sketch? it's broken atm, but not sure if worth fixing since desired behaviour is a little
await expect(page.locator('.cm-activeLine')).toHaveText( // vague
'|> line(end = [170.36, -121.61], tag = $seg01)' // // DELETE PARENT EXTRUDE
) // await page.mouse.click(parentExtrude.x, parentExtrude.y)
await u.clearCommandLogs() // await page.waitForTimeout(100)
await page.keyboard.press('Backspace') // await expect(page.locator('.cm-activeLine')).toHaveText(
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) // '|> line(end = [170.36, -121.61], tag = $seg01)'
await page.waitForTimeout(200) // )
await expect(u.codeLocator).not.toContainText( // await u.clearCommandLogs()
`extrude001 = extrude(sketch001, length = 50)` // await page.keyboard.press('Backspace')
) // await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ // await page.waitForTimeout(200)
plane = { // await expect(u.codeLocator).not.toContainText(
origin = { x = 0, y = -50, z = 0 }, // `extrude001 = extrude(sketch001, length = 50)`
xAxis = { x = 1, y = 0, z = 0 }, // )
yAxis = { x = 0, y = 0, z = 1 }, // await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
zAxis = { x = 0, y = -1, z = 0 } // plane = {
} // origin = { x = 0, y = -50, z = 0 },
})`) // xAxis = { x = 1, y = 0, z = 0 },
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ // yAxis = { x = 0, y = 0, z = 1 },
plane = { // zAxis = { x = 0, y = -1, z = 0 }
origin = { x = 116.53, y = 0, z = 163.25 }, // }
xAxis = { x = -0.81, y = 0, z = 0.58 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
zAxis = { x = 0.58, y = 0, z = 0.81 } // plane = {
} // origin = { x = 116.53, y = 0, z = 163.25 },
})`) // xAxis = { x = -0.81, y = 0, z = 0.58 },
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ // yAxis = { x = 0, y = -1, z = 0 },
plane = { // zAxis = { x = 0.58, y = 0, z = 0.81 }
origin = { x = -91.74, y = 0, z = 80.89 }, // }
xAxis = { x = -0.66, y = 0, z = -0.75 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
zAxis = { x = -0.75, y = 0, z = 0.66 } // plane = {
} // origin = { x = -91.74, y = 0, z = 80.89 },
})`) // xAxis = { x = -0.66, y = 0, z = -0.75 },
// yAxis = { x = 0, y = -1, z = 0 },
// zAxis = { x = -0.75, y = 0, z = 0.66 }
// }
// })`)
// DELETE SOLID 2D // DELETE SOLID 2D
await page.mouse.click(solid2d.x, solid2d.y) await page.mouse.click(solid2d.x, solid2d.y)
@ -414,16 +443,29 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`) await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
// Delete a single profile
await page.mouse.click(individualProfile.x, individualProfile.y)
await page.waitForTimeout(100)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line(end = [20.91, -28.61])'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
}) })
test("Deleting solid that the AST mod can't handle results in a toast message", async ({ test.fixme(
page, "Deleting solid that the AST mod can't handle results in a toast message",
homePage, async ({ page, homePage }) => {
}) => { const u = await getUtils(page)
const u = await getUtils(page) await page.addInitScript(async () => {
await page.addInitScript(async () => { localStorage.setItem(
localStorage.setItem( 'persistCode',
'persistCode', `sketch001 = startSketchOn('XZ')
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %) |> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02) |> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01) |> line(end = [170.36, -121.61], tag = $seg01)
@ -438,48 +480,49 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
` `
)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [170.36, -121.61], tag = $seg01)'
) )
}, KCL_DEFAULT_LENGTH) await u.clearCommandLogs()
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.keyboard.press('Backspace')
await homePage.goToModelingScene() await expect(page.getByText('Unable to delete selection')).toBeVisible()
}
await u.openDebugPanel() )
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [170.36, -121.61], tag = $seg01)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await expect(page.getByText('Unable to delete selection')).toBeVisible()
})
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({ test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
page, page,
homePage, homePage,
@ -902,6 +945,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({ test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
const cases = [ const cases = [
{ {
@ -937,6 +981,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.sendCustomCmd({ await u.sendCustomCmd({
@ -970,6 +1015,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({ test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -988,6 +1034,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.sendCustomCmd({ await u.sendCustomCmd({
@ -1021,19 +1068,19 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor)) .poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(15) .toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100) await page.waitForTimeout(1000)
await page.mouse.move(extrudeWall.x, extrudeWall.y) await page.mouse.move(extrudeWall.x, extrudeWall.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText( await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(extrudeText) removeAfterFirstParenthesis(extrudeText)
) )
await page.waitForTimeout(200) await page.waitForTimeout(1000)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor) await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(15) ).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y) await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200) await page.waitForTimeout(1000)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15) ).toBeLessThan(15)
@ -1044,7 +1091,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
).toBeLessThan(15) ).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300) await page.waitForTimeout(1000)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face // because of shading, color is not exact everywhere on the face
@ -1058,11 +1105,11 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await expect(page.getByTestId('hover-highlight').first()).toContainText( await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText) removeAfterFirstParenthesis(capText)
) )
await page.waitForTimeout(200) await page.waitForTimeout(1000)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
await page.mouse.click(cap.x, cap.y) await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200) await page.waitForTimeout(1000)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
@ -1109,7 +1156,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [4.95, -8]) |> line(end = [4.95, -8])
|> line(end = [-20.38, -10.12]) |> line(end = [-20.38, -10.12])
|> line(end = [-15.79, 17.08]) |> line(end = [-15.79, 17.08])
fn yohey = (pos) => { fn yohey = (pos) => {
sketch004 = startSketchOn('XZ') sketch004 = startSketchOn('XZ')
${extrudeAndEditBlockedInFunction} ${extrudeAndEditBlockedInFunction}
@ -1119,7 +1166,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [-15.79, 17.08]) |> line(end = [-15.79, 17.08])
return '' return ''
} }
yohey([15.79, -34.6]) yohey([15.79, -34.6])
` `
) )
@ -1211,12 +1258,15 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.waitForTimeout(600) await page.waitForTimeout(600)
const firstClickCoords = { x: 650, y: 200 } as const
// Place a point because the line tool will exit if no points are pressed // Place a point because the line tool will exit if no points are pressed
await page.mouse.click(650, 200) await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(600) await page.waitForTimeout(600)
// Code before exiting the tool // Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = (
await page.locator('.cm-content').innerText()
).replace(/\s+/g, '')
// deselect the line tool by clicking it // deselect the line tool by clicking it
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -1228,14 +1278,23 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.mouse.click(750, 200) await page.mouse.click(750, 200)
await page.waitForTimeout(100) await page.waitForTimeout(100)
// expect no change await expect
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) .poll(async () => {
let str = await page.locator('.cm-content').innerText()
str = str.replace(/\s+/g, '')
return str
})
.toBe(previousCodeContent)
// select line tool again // select line tool again
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
await u.closeDebugPanel() await u.closeDebugPanel()
// Click to continue profile
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
await page.waitForTimeout(100)
// line tool should work as expected again // line tool should work as expected again
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).not.toHaveText( await expect(page.locator('.cm-content')).not.toHaveText(

View File

@ -32,15 +32,18 @@ test.fixme('Units menu', async ({ page, homePage }) => {
await expect(unitsMenuButton).toContainText('mm') await expect(unitsMenuButton).toContainText('mm')
}) })
test('Successful export shows a success toast', async ({ page, homePage }) => { test(
// FYI this test doesn't work with only engine running locally 'Successful export shows a success toast',
// And you will need to have the KittyCAD CLI installed { tag: '@skipLocalEngine' },
const u = await getUtils(page) async ({ page, homePage }) => {
await page.addInitScript(async () => { // FYI this test doesn't work with only engine running locally
;(window as any).playwrightSkipFilePicker = true // And you will need to have the KittyCAD CLI installed
localStorage.setItem( const u = await getUtils(page)
'persistCode', await page.addInitScript(async () => {
`topAng = 25 ;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`topAng = 25
bottomAng = 35 bottomAng = 35
baseLen = 3.5 baseLen = 3.5
baseHeight = 1 baseHeight = 1
@ -78,26 +81,27 @@ part001 = startSketchOn('-XZ')
|> xLineTo(ZERO, %) |> xLineTo(ZERO, %)
|> close() |> close()
|> extrude(length = 4)` |> extrude(length = 4)`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page
) )
}) }
await page.setBodyDimensions({ width: 1200, height: 500 }) )
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page
)
})
test('Paste should not work unless an input is focused', async ({ test('Paste should not work unless an input is focused', async ({
page, page,
@ -205,8 +209,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Draw a line // Draw a line
await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250) const secondMousePosition = { x: 800, y: 250 }
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
// Unequip line tool // Unequip line tool
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -215,11 +224,23 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Equip arc tool // Equip arc tool
await page.keyboard.press('a') await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
steps: 5,
})
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100) await page.mouse.click(1000, 100)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await page.keyboard.press('l') await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') await expect
.poll(async () => {
await page.keyboard.press('l')
return lineButton.getAttribute('aria-pressed')
})
.toBe('true')
// Do not close the sketch. // Do not close the sketch.
// On close it will exit sketch mode. // On close it will exit sketch mode.
@ -444,7 +465,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings') await expect.poll(() => page.url()).not.toContain('/settings')
}) })
test('Sketch on face', async ({ page, homePage }) => { test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -470,11 +491,7 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -519,9 +536,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect.poll(u.normalisedEditorCode).toContain( await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01) u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %) profile001 = startProfileAt([-12.34, 12.34], sketch002)
|> line(end = [2.45, -0.2]) |> line(end = [12.34, -12.34])
|> line(end = [-2.6, -1.25]) |> line(end = [-12.34, -12.34])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
`) `)
@ -537,9 +554,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.getByText('startProfileAt([-12').click() await page.getByText('startProfileAt([-12').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(500)
await page.waitForTimeout(150) await page.setViewportSize({ width: 1200, height: 1200 })
await page.setBodyDimensions({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166]) await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel() await u.closeDebugPanel()
@ -579,10 +595,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.getByRole('button', { name: 'arrow right Continue' }).click() await cmdBar.progressCmdBar()
await page.waitForTimeout(100)
await expect(page.getByText('Confirm Extrude')).toBeVisible() await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.getByRole('button', { name: 'checkmark Submit command' }).click() await cmdBar.progressCmdBar()
const result2 = result.genNext` const result2 = result.genNext`
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)` const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`

View File

@ -16,8 +16,7 @@ mac:
arch: arch:
- x64 - x64
- arm64 - arm64
notarize: notarize: true
teamId: 92H8YB3B95
fileAssociations: fileAssociations:
- ext: kcl - ext: kcl
name: kcl name: kcl
@ -32,14 +31,11 @@ win:
arch: arch:
- x64 - x64
- arm64 - arm64
# - target: msi signtoolOptions:
# arch: sign: "./scripts/sign-win.js"
# - x64 signingHashAlgorithms:
# - arm64 - sha256
signingHashAlgorithms: publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
- sha256
sign: "./scripts/sign-win.js"
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico" icon: "assets/icon.ico"
fileAssociations: fileAssociations:
- ext: kcl - ext: kcl
@ -47,15 +43,12 @@ win:
mimeType: text/vnd.zoo.kcl mimeType: text/vnd.zoo.kcl
description: Zoo KCL File description: Zoo KCL File
role: Editor role: Editor
# msi:
# oneClick: false
# perMachine: true
nsis: nsis:
oneClick: false oneClick: false
perMachine: true perMachine: true
allowElevation: true allowElevation: true
installerIcon: "assets/icon.ico" installerIcon: "assets/icon.ico"
include: "./installer.nsh" include: "./scripts/installer.nsh"
linux: linux:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}" artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target: target:

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.13", "@kittycad/lib": "2.0.17",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1", "@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -40,7 +40,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "6.3.0", "electron-updater": "^6.5.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-shell/manifest.json", "fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/offset-plane-kwargs/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
@ -120,6 +120,7 @@
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"", "test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'", "test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'", "test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:ubuntu:engine:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot|@skipLocalEngine'",
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000", "test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000" "test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
}, },
@ -144,10 +145,11 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "7.4.0", "@electron-forge/cli": "^7.6.1",
"@electron-forge/plugin-fuses": "7.4.0", "@electron-forge/plugin-fuses": "^7.6.1",
"@electron-forge/plugin-vite": "7.4.0", "@electron-forge/plugin-vite": "^7.6.1",
"@electron/fuses": "1.8.0", "@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@nabla/vite-plugin-eslint": "^2.0.5", "@nabla/vite-plugin-eslint": "^2.0.5",
@ -174,9 +176,8 @@
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"electron": "32.1.2", "electron": "^34.1.1",
"electron-builder": "24.13.3", "electron-builder": "^26.0.6",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",
@ -200,7 +201,7 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.19.1", "typescript-eslint": "^8.23.0",
"vite": "^5.4.12", "vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

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

View File

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

View File

@ -29,7 +29,7 @@
"vscode-uri": "^3.0.8" "vscode-uri": "^3.0.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.6", "@types/node": "^22.13.1",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
} }

View File

@ -109,10 +109,10 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/node@^22.10.6": "@types/node@^22.13.1":
version "22.10.6" version "22.13.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ== integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
dependencies: dependencies:
undici-types "~6.20.0" undici-types "~6.20.0"

View File

@ -34,6 +34,13 @@
"title": "Car Wheel Assembly", "title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts." "description": "A car wheel assembly with a rotor, tire, and lug nuts."
}, },
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "cycloidal-gear/main.kcl",
"multipleFiles": false,
"title": "Cycloidal Gear",
"description": "A cycloidal gear is a gear with a continuous, curved tooth profile. They are used in watchmaking and high precision robotics actuation"
},
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl", "pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
@ -48,6 +55,13 @@
"title": "Enclosure", "title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items" "description": "An enclosure body and sealing lid for storing items"
}, },
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "exhaust-manifold/main.kcl",
"multipleFiles": false,
"title": "Exhaust Manifold",
"description": "A welded exhaust header for an inline 4-cylinder engine"
},
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl", "pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",

View File

@ -11,6 +11,7 @@ echo "$PACKAGE" > package.json
# electron-builder.yml # electron-builder.yml
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
# Release notes # Release notes
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md

View File

@ -0,0 +1,8 @@
!macro preInit
SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
!macroend

View File

@ -5,7 +5,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -21,6 +20,7 @@ import {
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { isCursorInFunctionDefinition } from 'lang/queryAst'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
@ -37,7 +37,12 @@ export function Toolbar({
const buttonBorderClassName = '!border-transparent' const buttonBorderClassName = '!border-transparent'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) if (
isCursorInFunctionDefinition(
kclManager.ast,
context.selectionRanges.graphSelections[0]
)
)
return false return false
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,

View File

@ -31,7 +31,6 @@ import {
recast, recast,
defaultSourceRange, defaultSourceRange,
resultIsOk, resultIsOk,
ProgramMemory,
topLevelRange, topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { CustomIcon, CustomIconName } from 'components/CustomIcon'
@ -125,14 +124,7 @@ export const ClientSideScene = ({
'mouseup', 'mouseup',
toSync(sceneInfra.onMouseUp, reportRejection) toSync(sceneInfra.onMouseUp, reportRejection)
) )
sceneEntitiesManager sceneEntitiesManager.tearDownSketch({ removeAxis: true })
.tearDownSketch()
.then(() => {
// no op
})
.catch((e) => {
console.error(e)
})
} }
}, []) }, [])
@ -153,7 +145,8 @@ export const ClientSideScene = ({
state.matches({ Sketch: 'Line tool' }) || state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) || state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches({ Sketch: 'Circle tool' }) state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'Circle three point tool' })
) { ) {
cursor = 'crosshair' cursor = 'crosshair'
} else { } else {
@ -191,12 +184,15 @@ const Overlays = () => {
style={{ zIndex: '99999999' }} style={{ zIndex: '99999999' }}
> >
{Object.entries(context.segmentOverlays) {Object.entries(context.segmentOverlays)
.filter((a) => a[1].visible) .flatMap((a) =>
.map(([pathToNodeString, overlay], index) => { a[1].map((b) => ({ pathToNodeString: a[0], overlay: b }))
)
.filter((a) => a.overlay.visible)
.map(({ pathToNodeString, overlay }, index) => {
return ( return (
<Overlay <Overlay
overlay={overlay} overlay={overlay}
key={pathToNodeString} key={pathToNodeString + String(index)}
pathToNodeString={pathToNodeString} pathToNodeString={pathToNodeString}
overlayIndex={index} overlayIndex={index}
/> />
@ -237,11 +233,17 @@ const Overlay = ({
const constraints = const constraints =
callExpression.type === 'CallExpression' callExpression.type === 'CallExpression'
? getConstraintInfo(callExpression, codeManager.code, overlay.pathToNode) ? getConstraintInfo(
callExpression,
codeManager.code,
overlay.pathToNode,
overlay.filterValue
)
: getConstraintInfoKw( : getConstraintInfoKw(
callExpression, callExpression,
codeManager.code, codeManager.code,
overlay.pathToNode overlay.pathToNode,
overlay.filterValue
) )
const offset = 20 // px const offset = 20 // px
@ -261,7 +263,6 @@ const Overlay = ({
state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) state.matches({ Sketch: 'Rectangle tool' })
) )
return ( return (
<div className={`absolute w-0 h-0`}> <div className={`absolute w-0 h-0`}>
<div <div
@ -319,17 +320,18 @@ const Overlay = ({
this will likely change soon when we implement multi-profile so we'll leave it for now this will likely change soon when we implement multi-profile so we'll leave it for now
issue: https://github.com/KittyCAD/modeling-app/issues/3910 issue: https://github.com/KittyCAD/modeling-app/issues/3910
*/} */}
{callExpression?.callee?.name !== 'circle' && ( {callExpression?.callee?.name !== 'circle' &&
<SegmentMenu callExpression?.callee?.name !== 'circleThreePoint' && (
verticalPosition={ <SegmentMenu
overlay.windowCoords[1] > window.innerHeight / 2 verticalPosition={
? 'top' overlay.windowCoords[1] > window.innerHeight / 2
: 'bottom' ? 'top'
} : 'bottom'
pathToNode={overlay.pathToNode} }
stdLibFnName={constraints[0]?.stdLibFnName} pathToNode={overlay.pathToNode}
/> stdLibFnName={constraints[0]?.stdLibFnName}
)} />
)}
</div> </div>
)} )}
</div> </div>
@ -425,7 +427,7 @@ export async function deleteSegment({
modifiedAst = deleteSegmentFromPipeExpression( modifiedAst = deleteSegmentFromPipeExpression(
dependentRanges, dependentRanges,
modifiedAst, modifiedAst,
kclManager.programMemory, kclManager.variables,
codeManager.code, codeManager.code,
pathToNode pathToNode
) )
@ -439,8 +441,8 @@ export async function deleteSegment({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
engineCommandManager: engineCommandManager, engineCommandManager: engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode. isMock: true,
programMemoryOverride: ProgramMemory.empty(), usePrevMemory: false,
}) })
if (testExecute.errors.length) { if (testExecute.errors.length) {
toast.error('Segment tag used outside of current Sketch. Could not delete.') toast.error('Segment tag used outside of current Sketch. Could not delete.')
@ -450,6 +452,8 @@ export async function deleteSegment({
if (!sketchDetails) return if (!sketchDetails) return
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
pathToNode, pathToNode,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
modifiedAst, modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -675,7 +679,7 @@ const ConstraintSymbol = ({
shallowPath, shallowPath,
argPosition, argPosition,
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.variables
) )
if (!transform) return if (!transform) return

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,11 +1,5 @@
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { import { parse, BinaryPart, Expr, resultIsOk, VariableMap } from '../lang/wasm'
parse,
BinaryPart,
Expr,
ProgramMemory,
resultIsOk,
} from '../lang/wasm'
import { import {
createIdentifier, createIdentifier,
createLiteral, createLiteral,
@ -100,7 +94,7 @@ export function useCalc({
newVariableInsertIndex: number newVariableInsertIndex: number
setNewVariableName: (a: string) => void setNewVariableName: (a: string) => void
} { } {
const { programMemory } = useKclContext() const { variables } = useKclContext()
const { context } = useModelingContext() const { context } = useModelingContext()
const selectionRange = const selectionRange =
context.selectionRanges?.graphSelections[0]?.codeRef?.range context.selectionRanges?.graphSelections[0]?.codeRef?.range
@ -127,7 +121,7 @@ export function useCalc({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (programMemory.has(newVariableName)) { if (variables[newVariableName]) {
setIsNewVariableNameUnique(false) setIsNewVariableNameUnique(false)
} else { } else {
setIsNewVariableNameUnique(true) setIsNewVariableNameUnique(true)
@ -135,14 +129,14 @@ export function useCalc({
}, [newVariableName]) }, [newVariableName])
useEffect(() => { useEffect(() => {
if (!programMemory || !selectionRange) return if (!variables || !selectionRange) return
const varInfo = findAllPreviousVariables( const varInfo = findAllPreviousVariables(
kclManager.ast, kclManager.ast,
kclManager.programMemory, kclManager.variables,
selectionRange selectionRange
) )
setAvailableVarInfo(varInfo) setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.variables, selectionRange])
useEffect(() => { useEffect(() => {
try { try {
@ -150,9 +144,9 @@ export function useCalc({
const pResult = parse(code) const pResult = parse(code)
if (trap(pResult) || !resultIsOk(pResult)) return if (trap(pResult) || !resultIsOk(pResult)) return
const ast = pResult.program const ast = pResult.program
const _programMem: ProgramMemory = ProgramMemory.empty() const _variables: VariableMap = {}
for (const { key, value } of availableVarInfo.variables) { for (const { key, value } of availableVarInfo.variables) {
const error = _programMem.set(key, { const error = (_variables[key] = {
type: 'String', type: 'String',
value, value,
__meta: [], __meta: [],
@ -163,8 +157,8 @@ export function useCalc({
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode. isMock: true,
programMemoryOverride: kclManager.programMemory.clone(), variables,
}).then(({ execState }) => { }).then(({ execState }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
@ -174,7 +168,7 @@ export function useCalc({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declaration.init resultDeclaration?.declaration.init
const result = execState.memory?.get('__result__')?.value const result = execState.variables['__result__']?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
}) })

View File

@ -196,6 +196,7 @@ function ReviewingButton() {
type="submit" type="submit"
form="review-form" form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow" className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-submit"
iconStart={{ iconStart={{
icon: 'checkmark', icon: 'checkmark',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110', bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
@ -214,6 +215,7 @@ function GatheringArgsButton() {
type="submit" type="submit"
form="arg-form" form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow" className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-continue"
iconStart={{ iconStart={{
icon: 'arrowRight', icon: 'arrowRight',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110', bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',

View File

@ -20,6 +20,7 @@ import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor' import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import toast from 'react-hot-toast'
const machineContextSelector = (snapshot?: { const machineContextSelector = (snapshot?: {
context: Record<string, unknown> context: Record<string, unknown>
@ -97,6 +98,7 @@ function CommandBarKclInput({
value, value,
initialVariableName, initialVariableName,
}) })
const varMentionData: Completion[] = prevVariables.map((v) => ({ const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key, label: v.key,
detail: String(roundOff(v.value as number)), detail: String(roundOff(v.value as number)),
@ -170,7 +172,15 @@ function CommandBarKclInput({
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) { function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault() e?.preventDefault()
if (!canSubmit || valueNode === null) return if (!canSubmit || valueNode === null) {
// Gotcha: Our application can attempt to submit a command value before the command bar kcl input is ready. Notify the scene and user.
if (!canSubmit) {
toast.error('Unable to submit command')
} else if (valueNode === null) {
toast.error('Unable to submit undefined command value')
}
return
}
onSubmit( onSubmit(
createNewVariable createNewVariable

View File

@ -2,11 +2,15 @@ import { Dialog } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useState } from 'react' import { useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
const DownloadAppBanner = () => { const DownloadAppBanner = () => {
const [searchParams] = useSearchParams()
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const [isBannerDismissed, setIsBannerDismissed] = useState( const [isBannerDismissed, setIsBannerDismissed] = useState(
settings.context.app.dismissWebBanner.current settings.context.app.dismissWebBanner.current || hasCreateFileParam
) )
return ( return (

View File

@ -157,8 +157,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.requestSemanticTokens() plugin.requestSemanticTokens()
break break
case 'kcl/memoryUpdated':
break
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -25,7 +25,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { import {
isCursorInSketchCommandRange, isCursorInSketchCommandRange,
updatePathToNodeFromMap, updateSketchDetailsNodePaths,
} from 'lang/util' } from 'lang/util'
import { import {
kclManager, kclManager,
@ -65,17 +65,31 @@ import {
replaceValueAtNodePath, replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
splitPipedProfile,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import {
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst' KclValue,
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' PathToNode,
Program,
VariableDeclaration,
parse,
recast,
resultIsOk,
} from 'lang/wasm'
import {
artifactIsPlaneWithPaths,
doesSketchPipeNeedSplitting,
getNodeFromPath,
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap, reject } from 'lib/trap'
import { import {
ExportIntent, ExportIntent,
EngineConnectionStateType, EngineConnectionStateType,
@ -86,10 +100,16 @@ import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getFaceCodeRef,
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine' import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -254,7 +274,11 @@ export const ModelingMachineProvider = ({
'Set Segment Overlays': assign({ 'Set Segment Overlays': assign({
segmentOverlays: ({ context: { segmentOverlays }, event }) => { segmentOverlays: ({ context: { segmentOverlays }, event }) => {
if (event.type !== 'Set Segment Overlays') return {} if (event.type !== 'Set Segment Overlays') return {}
if (event.data.type === 'set-many') return event.data.overlays if (event.data.type === 'add-many')
return {
...segmentOverlays,
...event.data.overlays,
}
if (event.data.type === 'set-one') if (event.data.type === 'set-one')
return { return {
...segmentOverlays, ...segmentOverlays,
@ -287,7 +311,7 @@ export const ModelingMachineProvider = ({
return { return {
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: event.data, sketchEntryNodePath: event.data,
}, },
} }
}), }),
@ -329,11 +353,83 @@ export const ModelingMachineProvider = ({
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && editorManager.isShiftDown) { } else if (setSelections.selection && editorManager.isShiftDown) {
// selecting and deselecting multiple objects
/**
* There are two scenarios:
* 1. General case:
* When selecting and deselecting edges,
* faces or segment (during sketch edit)
* we use its artifact ID to identify the selection
* 2. Initial sketch setup:
* The artifact is not yet created
* so we use the codeRef.range
*/
let updatedSelections: typeof selectionRanges.graphSelections
// 1. General case: Artifact exists, use its ID
if (setSelections.selection.artifact?.id) {
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) =>
selection.artifact?.id ===
setSelections.selection?.artifact?.id
)
if (
alreadySelected &&
setSelections.selection?.artifact?.id
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
selection.artifact?.id !==
setSelections.selection?.artifact?.id
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
} else {
// 2. Initial sketch setup: Artifact not yet created use codeRef.range
const selectionRange = JSON.stringify(
setSelections.selection?.codeRef?.range
)
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) => {
const existingRange = JSON.stringify(
selection.codeRef?.range
)
return existingRange === selectionRange
}
)
if (
alreadySelected &&
setSelections.selection?.codeRef?.range
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
JSON.stringify(selection.codeRef?.range) !==
selectionRange
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
}
selections = { selections = {
graphSelections: [ graphSelections: updatedSelections,
...selectionRanges.graphSelections,
setSelections.selection,
],
otherSelections: selectionRanges.otherSelections, otherSelections: selectionRanges.otherSelections,
} }
} }
@ -411,9 +507,17 @@ export const ModelingMachineProvider = ({
selectionRanges: setSelections.selection, selectionRanges: setSelections.selection,
sketchDetails: { sketchDetails: {
...sketchDetails, ...sketchDetails,
sketchPathToNode: sketchEntryNodePath:
setSelections.updatedPathToNode || setSelections.updatedSketchEntryNodePath ||
sketchDetails?.sketchPathToNode || sketchDetails?.sketchEntryNodePath ||
[],
sketchNodePaths:
setSelections.updatedSketchNodePaths ||
sketchDetails?.sketchNodePaths ||
[],
planeNodePath:
setSelections.updatedPlaneNodePath ||
sketchDetails?.planeNodePath ||
[], [],
}, },
} }
@ -566,7 +670,12 @@ export const ModelingMachineProvider = ({
if (artifactIsPlaneWithPaths(selectionRanges)) { if (artifactIsPlaneWithPaths(selectionRanges)) {
return true return true
} }
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (
isCursorInFunctionDefinition(
kclManager.ast,
selectionRanges.graphSelections[0]
)
)
return false return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
@ -594,13 +703,33 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails } }) => { async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (kclManager.ast.body.length) { if (kclManager.ast.body.length) {
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
const newAst = structuredClone(kclManager.ast) const newAst = structuredClone(kclManager.ast)
const varDecIndex = sketchDetails.sketchPathToNode[1][0] const varDecIndex = sketchDetails.planeNodePath[1][0]
const varDec = getNodeFromPath<VariableDeclaration>(
newAst,
sketchDetails.planeNodePath,
'VariableDeclaration'
)
if (err(varDec)) return reject(new Error('No varDec'))
const variableName = varDec.node.declaration.id.name
let isIdentifierUsed = false
traverse(newAst, {
enter: (node) => {
if (
node.type === 'Identifier' &&
node.name === variableName
) {
isIdentifierUsed = true
}
},
})
if (isIdentifierUsed) return
// remove body item at varDecIndex // remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex) newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst) await kclManager.executeAstMock(newAst)
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
} }
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: () => {}, onClick: () => {},
@ -610,7 +739,7 @@ export const ModelingMachineProvider = ({
} }
), ),
'animate-to-face': fromPromise(async ({ input }) => { 'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return undefined if (!input) return null
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched = const sketched =
input.type === 'extrudeFace' input.type === 'extrudeFace'
@ -637,7 +766,9 @@ export const ModelingMachineProvider = ({
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchPathToNode: pathToNewSketchNode, sketchEntryNodePath: [],
planeNodePath: pathToNewSketchNode,
sketchNodePaths: [],
zAxis: input.zAxis, zAxis: input.zAxis,
yAxis: input.yAxis, yAxis: input.yAxis,
origin: input.position, origin: input.position,
@ -658,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],
@ -667,21 +800,70 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sourceRange = const plane = getPlaneFromArtifact(
selectionRanges.graphSelections[0]?.codeRef?.range selectionRanges.graphSelections[0].artifact,
const sketchPathToNode = getNodePathFromSourceRange( engineCommandManager.artifactGraph
kclManager.ast,
sourceRange
)
const info = await getSketchOrientationDetails(
sketchPathToNode || []
) )
if (err(plane)) return Promise.reject(plane)
let sketch: KclValue | null = null
for (const variable of Object.values(
kclManager.execState.variables
)) {
// find programMemory that matches path artifact
if (
variable?.type === 'Sketch' &&
variable.value.artifactId === plane.pathIds[0]
) {
sketch = variable
break
}
if (
// if the variable is an sweep, check if the underlying sketch matches the artifact
variable?.type === 'Solid' &&
variable.value.sketch.on.type === 'plane' &&
variable.value.sketch.artifactId === plane.pathIds[0]
) {
sketch = {
type: 'Sketch',
value: variable.value.sketch,
}
break
}
}
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
const info = await getSketchOrientationDetails(sketch.value)
await letEngineAnimateAndSyncCamAfter( await letEngineAnimateAndSyncCamAfter(
engineCommandManager, engineCommandManager,
info?.sketchDetails?.faceId || '' info?.sketchDetails?.faceId || ''
) )
const sketchArtifact = engineCommandManager.artifactGraph.get(
plane.pathIds[0]
)
if (sketchArtifact?.type !== 'path')
return Promise.reject(new Error('No sketch artifact'))
const sketchPaths = getPathsFromArtifact({
artifact: engineCommandManager.artifactGraph.get(plane.id),
sketchPathToNode: sketchArtifact?.codeRef?.pathToNode,
artifactGraph: engineCommandManager.artifactGraph,
ast: kclManager.ast,
})
if (err(sketchPaths)) return Promise.reject(sketchPaths)
let codeRef = getFaceCodeRef(plane)
if (!codeRef) return Promise.reject(new Error('No plane codeRef'))
// codeRef.pathToNode is not always populated correctly
const planeNodePath = getNodePathFromSourceRange(
kclManager.ast,
codeRef.range
)
return { return {
sketchPathToNode: sketchPathToNode || [], sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath,
zAxis: info.sketchDetails.zAxis || null, zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null, yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map( origin: info.sketchDetails.origin.map(
@ -694,7 +876,7 @@ export const ModelingMachineProvider = ({
'Get horizontal info': fromPromise( 'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, selectionRanges,
@ -706,13 +888,23 @@ export const ModelingMachineProvider = ({
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -733,13 +925,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get vertical info': fromPromise( 'Get vertical info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, selectionRanges,
@ -750,13 +944,23 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -777,7 +981,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -787,14 +993,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'))
@ -803,13 +1010,23 @@ export const ModelingMachineProvider = ({
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -830,7 +1047,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -845,20 +1064,30 @@ export const ModelingMachineProvider = ({
length: lengthValue, length: lengthValue,
}) })
if (err(constraintResult)) return Promise.reject(constraintResult) if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult const { modifiedAst, pathToNodeMap, exprInsertIndex } =
constraintResult
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -879,13 +1108,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get perpendicular distance info': fromPromise( 'Get perpendicular distance info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintIntersect({ await applyConstraintIntersect({
selectionRanges, selectionRanges,
}) })
@ -895,13 +1126,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -922,13 +1162,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS X info': fromPromise( 'Get ABS X info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'xAbs', constraint: 'xAbs',
selectionRanges, selectionRanges,
@ -939,13 +1181,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -966,13 +1217,15 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
'Get ABS Y info': fromPromise( 'Get ABS Y info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap, exprInsertIndex } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'yAbs', constraint: 'yAbs',
selectionRanges, selectionRanges,
@ -983,13 +1236,22 @@ export const ModelingMachineProvider = ({
const _modifiedAst = pResult.program const _modifiedAst = pResult.program
if (!sketchDetails) if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
const updatedPathToNode = updatePathToNodeFromMap(
sketchDetails.sketchPathToNode, const {
pathToNodeMap updatedSketchEntryNodePath,
) updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
_modifiedAst, _modifiedAst,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1010,7 +1272,9 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} }
} }
), ),
@ -1030,9 +1294,11 @@ export const ModelingMachineProvider = ({
let result: { let result: {
modifiedAst: Node<Program> modifiedAst: Node<Program>
pathToReplaced: PathToNode | null pathToReplaced: PathToNode | null
exprInsertIndex: number
} = { } = {
modifiedAst: parsed, modifiedAst: parsed,
pathToReplaced: null, pathToReplaced: null,
exprInsertIndex: -1,
} }
// If the user provided a constant name, // If the user provided a constant name,
// we need to insert the named constant // we need to insert the named constant
@ -1062,6 +1328,7 @@ export const ModelingMachineProvider = ({
result = { result = {
modifiedAst: parseResultAfterInsertion.program, modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced, pathToReplaced: astAfterReplacement.pathToReplaced,
exprInsertIndex: astAfterReplacement.exprInsertIndex,
} }
} else if ('valueText' in data.namedValue) { } else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name, // If they didn't provide a constant name,
@ -1092,10 +1359,22 @@ export const ModelingMachineProvider = ({
parsed = parsed as Node<Program> parsed = parsed as Node<Program>
if (!result.pathToReplaced) if (!result.pathToReplaced)
return Promise.reject(new Error('No path to replaced node')) return Promise.reject(new Error('No path to replaced node'))
const {
updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
} = updateSketchDetailsNodePaths({
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
exprInsertIndex: result.exprInsertIndex,
})
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
result.pathToReplaced || [], updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1116,7 +1395,194 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode: result.pathToReplaced, updatedSketchEntryNodePath,
updatedSketchNodePaths,
updatedPlaneNodePath,
}
}
),
'set-up-draft-circle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCircle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-circle-three-point': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result =
await sceneEntitiesManager.setupDraftCircleThreePoint(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data.p1,
data.p2
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-center-rectangle': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
if (!sketchDetails.sketchEntryNodePath.length) return
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
maybeModdedAst: kclManager.ast,
selectionRanges,
})
sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
forward: sketchDetails.zAxis,
up: sketchDetails.yAxis,
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
// We will want to pass sketchTools here
// to add their interactions
})
// We will want to update the context with sketchTools.
// They'll be used for their .destroy() in tearDownSketch
return undefined
}
),
'split-sketch-pipe-if-needed': fromPromise(
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return reject('No sketch details')
const existingSketchInfoNoOp = {
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1,
} as const
if (
!sketchDetails.sketchNodePaths.length &&
sketchDetails.planeNodePath.length
) {
// new sketch, no profiles yet
return existingSketchInfoNoOp
}
const doesNeedSplitting = doesSketchPipeNeedSplitting(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
let moddedAst: Program = structuredClone(kclManager.ast)
let pathToProfile = sketchDetails.sketchEntryNodePath
let updatedSketchNodePaths = sketchDetails.sketchNodePaths
if (doesNeedSplitting) {
const splitResult = splitPipedProfile(
moddedAst,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
moddedAst = splitResult.modifiedAst
pathToProfile = splitResult.pathToProfile
updatedSketchNodePaths = [pathToProfile]
}
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
if (indexToDelete >= 0) {
// this is the expression that was added when as sketch tool was used but not completed
// i.e first click for the center of the circle, but not the second click for the radius
// we added a circle to editor, but they bailed out early so we should remove it
moddedAst.body.splice(indexToDelete, 1)
// make sure the deleted expression is removed from the sketchNodePaths
updatedSketchNodePaths = updatedSketchNodePaths.filter(
(path) => path[1][0] !== indexToDelete
)
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
// as a safe default
pathToProfile =
pathToProfile[1][0] !== indexToDelete
? pathToProfile
: updatedSketchNodePaths[0]
}
if (doesNeedSplitting) {
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
}
return {
updatedEntryNodePath: pathToProfile,
updatedSketchNodePaths: updatedSketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1,
} }
} }
), ),
@ -1127,6 +1593,7 @@ export const ModelingMachineProvider = ({
selections: input.selection, selections: input.selection,
token, token,
artifactGraph: engineCommandManager.artifactGraph, artifactGraph: engineCommandManager.artifactGraph,
projectName: context.project.name,
}) })
}), }),
}, },

View File

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

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPane' import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { assertParse, initPromise, ProgramMemory } from '../../../lang/wasm' import { assertParse, initPromise } from '../../../lang/wasm'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -29,15 +29,11 @@ describe('processMemory', () => {
|> line(endAbsolute = [2.15, 4.32]) |> line(endAbsolute = [2.15, 4.32])
// |> rx(90, %)` // |> rx(90, %)`
const ast = assertParse(code) const ast = assertParse(code)
const execState = await enginelessExecutor(ast, ProgramMemory.empty()) const execState = await enginelessExecutor(ast)
const output = processMemory(execState.memory) const output = processMemory(execState.variables)
expect(output.myVar).toEqual(5) expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3) expect(output.otherVar).toEqual(3)
expect(output).toEqual({ expect(output).toEqual({
HALF_TURN: 180,
QUARTER_TURN: 90,
THREE_QUARTER_TURN: 270,
ZERO: 0,
myVar: 5, myVar: 5,
myFn: '__function(a)__', myFn: '__function(a)__',
otherVar: 3, otherVar: 3,

View File

@ -2,10 +2,10 @@ import toast from 'react-hot-toast'
import ReactJson from 'react-json-view' import ReactJson from 'react-json-view'
import { useMemo } from 'react' import { useMemo } from 'react'
import { import {
ProgramMemory,
Path, Path,
ExtrudeSurface, ExtrudeSurface,
sketchFromKclValueOptional, sketchFromKclValueOptional,
VariableMap,
} from 'lang/wasm' } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme' import { useResolvedTheme } from 'hooks/useResolvedTheme'
@ -15,12 +15,12 @@ import Tooltip from 'components/Tooltip'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
export const MemoryPaneMenu = () => { export const MemoryPaneMenu = () => {
const { programMemory } = useKclContext() const { variables } = useKclContext()
function copyProgramMemoryToClipboard() { function copyProgramMemoryToClipboard() {
if (globalThis && 'navigator' in globalThis) { if (globalThis && 'navigator' in globalThis) {
navigator.clipboard navigator.clipboard
.writeText(JSON.stringify(programMemory)) .writeText(JSON.stringify(variables))
.then(() => toast.success('Program memory copied to clipboard')) .then(() => toast.success('Program memory copied to clipboard'))
.catch((e) => .catch((e) =>
trap(new Error('Failed to copy program memory to clipboard')) trap(new Error('Failed to copy program memory to clipboard'))
@ -50,12 +50,9 @@ export const MemoryPaneMenu = () => {
export const MemoryPane = () => { export const MemoryPane = () => {
const theme = useResolvedTheme() const theme = useResolvedTheme()
const { programMemory } = useKclContext() const { variables } = useKclContext()
const { state } = useModelingContext() const { state } = useModelingContext()
const ProcessedMemory = useMemo( const ProcessedMemory = useMemo(() => processMemory(variables), [variables])
() => processMemory(programMemory),
[programMemory]
)
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<div className="absolute inset-0 p-2 flex flex-col items-start"> <div className="absolute inset-0 p-2 flex flex-col items-start">
@ -85,9 +82,10 @@ export const MemoryPane = () => {
) )
} }
export const processMemory = (programMemory: ProgramMemory) => { export const processMemory = (variables: VariableMap) => {
const processedMemory: any = {} const processedMemory: any = {}
for (const [key, val] of programMemory?.visibleEntries()) { for (const [key, val] of Object.entries(variables)) {
if (val === undefined) continue
if ( if (
val.type === 'Sketch' || val.type === 'Sketch' ||
// @ts-ignore // @ts-ignore

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