Compare commits

...

127 Commits

Author SHA1 Message Date
df8977407c make delet of extrusions work for multi profile 2025-02-13 10:06:34 -05:00
42016b7335 get delet working for walls 2025-02-13 10:06:34 -05:00
09f3e6e170 Add support for deleting sketches on caps (not walls) 2025-02-13 10:06:34 -05:00
3747db01b8 Delete paths associated with sketch when the sketch plane is deleted 2025-02-13 10:05:41 -05:00
bf1a42f6c0 remove stale comment 2025-02-13 21:06:36 +11:00
b4d1a36bd9 fix closing profile bug (tangentArcTo being ignored) 2025-02-13 20:55:57 +11:00
b937934834 fix close profiles 2025-02-13 19:38:09 +11:00
0208eecefa fix delete of circle profile 2025-02-13 19:06:38 +11:00
9999e4e62f fix changing tools part way through circle/rect tools 2025-02-13 19:01:28 +11:00
b390e3e083 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-13 17:41:10 +11: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
9a89926749 clean up for face codeRef 2025-02-13 12:18:07 +11:00
d7f834f23d Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-13 12:02:32 +11:00
0874891dd0 pass thru the kcl version (#5366)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-02-12 18:55:34 -05:00
500d92d1fc Merge branch 'main' into kurt-multi-profile-again 2025-02-12 18:34:02 -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
bd10c65bdb Fix edit/select sketch-on-cap via feature tree 2025-02-12 17:51:15 -05: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
039092ec24 Update output after adding cap-to-path graph edge 2025-02-12 13:54:39 -05: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
8a920b6cb7 Prevent double write to KCL code on revolve 2025-02-12 11:58:20 -05:00
c1db093e0f nit 2025-02-12 19:24:36 +11:00
5f8ae22b04 Update src/clientSideScene/segments.ts
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-02-12 19:23:30 +11:00
dc7b901eb9 remove file tree from diff 2025-02-12 19:21:07 +11:00
60dcf9d79e rename error 2025-02-12 19:08:30 +11:00
520f899ad4 Update src/lib/rectangleTool.ts
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-02-12 18:58:43 +11:00
41340c8bf0 explain function name 2025-02-12 18:51:05 +11:00
a78ec6cd17 fix snapshot test 2025-02-12 18:46:23 +11:00
de526ae36e make other startSketchOn's work 2025-02-12 18:19:39 +11:00
4a8e582865 fix going into edit sketch 2025-02-12 17:28:06 +11:00
1854064274 add path ids to cap 2025-02-12 17:07:32 +11:00
4a8897be4b Merge branch 'main' into kurt-multi-profile-again 2025-02-11 19:30:27 -05: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
83f458fc36 Merge branch 'main' into kurt-multi-profile-again 2025-02-11 17:58:00 -05: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
a686fe914b Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-11 17:33:43 +11:00
cba2349064 fix profile start snap bug 2025-02-11 17:31:49 +11:00
d4d9bf6c7f Add trivia to AST Node (#5330)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 05:30:14 +00:00
5ae92bcf5c Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-11 15:46:56 +11: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
ad8e306bdb Fix clippy warning 2025-02-10 19:46:33 -05:00
508e1c919c Update output after changing back to skipping none and both 2025-02-10 19:18:44 -05:00
58ec6100c4 Change back to skipping face cap none and both 2025-02-10 19:18:17 -05:00
11eceefedf Fix to use correct source ranges
This also reduces cloning.
2025-02-10 18:39:57 -05:00
11a678df5b Update output from simulation tests 2025-02-10 18:36:38 -05: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
22c0003eb1 Update src/lang/modifyAst.ts
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-11 07:52:35 +11:00
8f0a40ba6e remove onboarding test changes 2025-02-11 07:30:16 +11:00
89b0ccb090 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-11 07:25:52 +11:00
5d22308ad2 remove test skip 2025-02-11 07:21:17 +11:00
5580631c8f remove bad doc comment 2025-02-11 07:21:04 +11: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
e1494c9f16 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-02-10 11:53:51 +00:00
4a0d852a3c A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-02-10 11:37:53 +00:00
b213834316 clean up 2025-02-10 22:23:34 +11:00
1d8348c2cf 2 test for three point circle 2025-02-10 21:44:55 +11:00
2227287c9d 1 test for three point circle 2025-02-10 21:29:17 +11:00
680fc30682 add draft point for first click of three point circ 2025-02-10 18:45:14 +11:00
40fb6a44d3 fmt 2025-02-10 18:44:05 +11:00
5713bfd9fa fix up state diagram 2025-02-10 17:46:42 +11:00
77902d550f make three point have cross hair to make it consistent with othe rtools 2025-02-10 17:45:57 +11:00
db895d6801 small bug 2025-02-10 17:45:35 +11:00
3e1f8584ea test works now without me changing anything big-fixed-itself 2025-02-10 16:31:51 +11:00
2501a98cd9 clippy again 2025-02-10 16:11:14 +11:00
e60b0e64ba Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-10 16:01:27 +11:00
3379cc489a is clippy happy now? 2025-02-10 16:00:52 +11: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
a8b7328f65 commint snaps? 2025-02-10 15:59:43 +11:00
ab6995bde3 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-10 15:43:06 +11:00
a9182bf357 Bump typescript-eslint from 8.19.1 to 8.23.0 (#5322) 2025-02-10 04:28:21 +00:00
6df5e70186 update docs 2025-02-10 15:24:38 +11:00
6b1cc36911 more test tweaks 2025-02-10 13:27:20 +11:00
6a16e47491 tsc 2025-02-10 12:37:37 +11:00
f4f0533179 fix electron specific test 2025-02-10 12:36:03 +11:00
6360b8acac fix more tests 2025-02-10 11:08:00 +11:00
064a41d675 fix more tests and add a fix me 2025-02-10 10:00:14 +11:00
e075622a7f fix sweep point-and-click test 2025-02-10 09:52:51 +11:00
7a7929211a Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-10 09:08:59 +11: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
1bfc3a0a3c CM KCL: add named args to fn calls (#5303)
* CM KCL: add named args to fn calls

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

* Rename to match other code

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 16:47:45 +00:00
f6e975db84 Add a "back" button to the onboarding buttons, move the dismiss button to a little corner x button (#5296)
* Add previous button to OnboardingButtons, move dismiss to popover corner

* 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)

* Clean up diagnostics

I am thoroughly enjoying nvim now

* Amend "click through" test to also click back

* fmt

* Set this test back to fixme, that work should be its own PR

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 11:30:36 -05:00
b82eec85fd Add basic axis case for Helix point-and-click (#5240)
* Move Helix button to a section with offset plane (3d 'construction' elements)
Fixes #5234

* Add generix axis case for Helix point-and-click
Fixes #5072 #5236 #5073

* 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

* Clean up

* Temp remove point and click test

* Add back point and click 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)

* 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)

* 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)

* Fix helix arg after change to kwargs

* More fixes wrt helix arg after change to kwargs

* Fixed thanks to @adamchalmers

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 10:16:36 -05:00
5dc4213295 let users have big editors (#5295)
* let users have big editor

* 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)

* Update src/components/ModelingSidebar/ModelingSidebar.tsx

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-07 05:19:09 -05:00
1f5f42963d fix some tests 2025-02-07 17:00:05 +11:00
235e6a1056 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-07 13:50:26 +11:00
09cfbc1837 fix unit test 2025-02-07 13:50:16 +11:00
61807e7629 KCL: Make shell stdlib fn use keyword arguments (#5293)
Before:

```
shell({ faces = ['end'], thickness = caseThickness }, case)
```

After:
```
shell(case, faces = ['end'], thickness = caseThickness)
```
2025-02-07 02:03:12 +00:00
319029235c fix circ dep 2025-02-07 12:59:19 +11:00
a7f4b0f037 Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-07 12:02:42 +11:00
357bbffce5 refactor: Rename function to not have unneeded prefix (#5292) 2025-02-06 19:54:58 -05:00
6ac9c49773 KCL: Patterns of patterns can use the original sketch/solid as target (#5284)
Right now, if you model something like this box with a button:

<img width="413" alt="Screenshot 2025-02-06 at 3 08 03 PM" src="https://github.com/user-attachments/assets/04818a70-7cf3-4ee3-b8c5-df5959ac10db" />

Let's say you want to pattern the button, and repeat it a second time. If you try, you'll actually pattern the entire model (box + button).

<img width="486" alt="Screenshot 2025-02-06 at 3 08 52 PM" src="https://github.com/user-attachments/assets/09fc28d9-5d80-4ab3-b4dc-b8de2945fcba" />

Why? Because right now, when you sketch on a face (like the button was), both the box and the button share the same ID. All extrusions from a solid will share the same ID, because they all refer to the same composite solid.

This is helpful in some ways -- arguably the solid _is_ just one big complex shape now -- but it's not helpful in other ways. What if I want to only pattern the button? Luckily there's an original ID for the button part, which is still stored. So we just need a way to tell the pattern stdlib functions whether to use the target's main ID or its original ID. This PR adds a new optional bool, `useOriginal`, to patterns. It's false by default, to keep backwards-compatibility (make sure that old KCL code doesn't change).

This PR is based on https://github.com/KittyCAD/modeling-app/pull/3914. It's based on work Serena and I are doing to fix a bug (engine does not allow patterning a 3D solid which was sketched on a face of another solid). @gserena01 our test program is now:

```
w = 400

case = startSketchOn('XY')
  |> startProfileAt([-w, -w], %)
  |> line(endAbsolute = [-w, w])
  |> line(endAbsolute = [w, -w])
  |> line(endAbsolute = [-w, -w])
  |> close()
  |> extrude(length = 200)

bump1 = startSketchOn(case, 'end')
  |> circle({ center = [-50, -50], radius = 40 }, %)
  |> extrude(length = 20)

// We pass in "bump1" here since we want to pattern just this object on the face.
useOriginal = true
target = bump1
transform = {
  axis = [1, 0, 0],
  instances = 3,
  distance = -100
}
patternLinear3d(transform, target, useOriginal)
```

If you change the `useOriginal = true` to `false` you can see the difference.
2025-02-06 17:46:47 -06:00
020497cde2 Don't close the command palette on backspace (#5291)
* Don't close the command palette on backspace

It leads to awkward UX like the one reported in #5007. Users can still
go back between arguments using <kbd>Backspace</kbd> on an empty
argument, but to close it they have to hit <kbd>Escape</kbd>,
<kbd>Cmd+K</kbd>, or click the close button.

* fmt
'
2025-02-06 16:28:35 -05:00
688852a5df Bump @types/mocha from 10.0.7 to 10.0.10 (#5028)
Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 10.0.7 to 10.0.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha)

---
updated-dependencies:
- dependency-name: "@types/mocha"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-06 15:17:14 -05:00
6c635bd70d refactor: Remove dead code to update the artifact graph after mock execution (#5158)
Remove dead code to update the artifact graph after mock execution
2025-02-06 17:56:19 +00:00
1c0a38a1e2 Update lower-right corner units menu to read and edit inline settings annotations if present (#5212)
* WIP show annotation length unit setting in LowerRightControls if present

* Add logic for changing settings annotation if it's present

* Add E2E test

* Cleanup lints, fmt, tsc, logs

* Change to use settings from Rust helper function

- Fix thrown error to use the cause field
- Fix function names to not use "get"

* Remove unneeded constants

* Post-merge fixups

* 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)

* Add back `ImportStatement` to make tsc happy (thanks @jtran!)

* fmt

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 17:37:13 +00:00
019cb815f9 Fix resizing view breaking app on high DPI displays (#5275)
* Fix resizing view breaking app on high DPI displays

* 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>
2025-02-06 12:06:50 -05:00
c8653beae7 updating onboarding bracket with SSI (#5281)
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-06 16:15:21 +00:00
9ea3cb51ba Bump miette from 7.2.0 to 7.5.0 in /src/wasm-lib (#5224)
Bumps [miette](https://github.com/zkat/miette) from 7.2.0 to 7.5.0.
- [Release notes](https://github.com/zkat/miette/releases)
- [Changelog](https://github.com/zkat/miette/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zkat/miette/commits)

---
updated-dependencies:
- dependency-name: miette
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-06 08:06:10 -08:00
e0de0493ab Skip prompt-to-edit playwright tests on windows (#5290)
* Skip prompt-to-edit playwright tests on windows

* Fix lint

* Fix test hookk

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

* Clear bad snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-06 09:45:12 -05:00
11cac0c30e Skip text-to-cad playwright tests on windows (#5286) 2025-02-06 08:48:17 -05:00
d3afa38bd5 small thing 2025-02-06 23:18:02 +11:00
45416df215 maybe fix a unit test 2025-02-06 22:39:29 +11:00
ee54cdd27a lint and typing 2025-02-06 22:34:21 +11:00
84ae567f37 A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) 2025-02-06 11:21:42 +00:00
f94671f1bb Merge remote-tracking branch 'origin' into kurt-multi-profile-again 2025-02-06 22:09:21 +11:00
bcf3790266 remove duplicate test 2025-02-06 22:09:13 +11:00
4de50edf5a Bump @types/node from 22.7.8 to 22.13.1 (#5262)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.8 to 22.13.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-06 10:51:07 +00:00
558 changed files with 231938 additions and 184154 deletions

View File

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

View File

@ -3,7 +3,6 @@ on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
concurrency:
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 |
|----------|------|-------------|----------|
| `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

View File

@ -17,7 +17,7 @@ lastSegY(sketch: Sketch) -> number
| 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

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"
```
## 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

@ -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.
```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 |
|----------|------|-------------|----------|
| `data` | [`CircularPattern2dData`](/docs/kcl/types/CircularPattern2dData) | Data for a circular pattern on a 2D sketch. | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch(es) to pattern | 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
@ -34,12 +38,12 @@ exampleSketch = startSketchOn('XZ')
|> line(end = [-1, 0])
|> line(end = [0, -5])
|> close()
|> patternCircular2d({
|> patternCircular2d(
center = [0, 0],
instances = 13,
arcDegrees = 360,
rotateDuplicates = true
}, %)
rotateDuplicates = true,
)
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.
```js
patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
patternLinear2d(sketch_set: SketchSet, instances: integer, distance: number, axis: [number], use_original?: bool) -> [Sketch]
```
@ -17,8 +17,11 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
| 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) | A sketch or a group of sketches. | Yes |
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | 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 |
| `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
@ -30,11 +33,7 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
```js
exampleSketch = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
|> patternLinear2d({
axis = [1, 0],
instances = 7,
distance = 4
}, %)
|> patternLinear2d(axis = [1, 0], instances = 7, distance = 4)
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 |
|----------|------|-------------|----------|
| `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

View File

@ -17,7 +17,7 @@ segEnd(tag: TagIdentifier) -> [number]
| 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

View File

@ -17,7 +17,7 @@ segEndX(tag: TagIdentifier) -> number
| 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

View File

@ -17,7 +17,7 @@ segEndY(tag: TagIdentifier) -> number
| 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

View File

@ -17,7 +17,7 @@ segLen(tag: TagIdentifier) -> number
| 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

View File

@ -17,7 +17,7 @@ segStart(tag: TagIdentifier) -> [number]
| 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

View File

@ -17,7 +17,7 @@ segStartX(tag: TagIdentifier) -> number
| 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

View File

@ -17,7 +17,7 @@ segStartY(tag: TagIdentifier) -> number
| 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

File diff suppressed because one or more lines are too long

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 |
|----------|------|-------------|----------|
| `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

View File

@ -20,5 +20,6 @@ Data for a circular pattern on a 2D sketch.
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |

View File

@ -21,5 +21,6 @@ Data for a circular pattern on a 3D model.
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |

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 |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y 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 face's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |

View File

@ -311,7 +311,7 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `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 |
@ -351,6 +351,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

@ -98,6 +98,29 @@ a complete arc
| `__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.

View File

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

View File

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

View File

@ -22,6 +22,7 @@ A sketch is a collection of paths.
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |`string`| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -31,6 +31,7 @@ A sketch is a collection of paths.
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |`string`| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

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

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

@ -19,6 +19,8 @@ test.describe(
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// FIXME: Cannot use scene.waitForExecutionDone() since there is no KCL code
await page.waitForTimeout(10000)
await u.openDebugPanel()
const coord =

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 ({
page,
homePage,
scene,
}) => {
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 homePage.goToModelingScene()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
// Ensure no badge is present
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 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
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
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
// a lint error.
@ -204,8 +203,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type('foo_bar = 1')
await page.waitForTimeout(500)
await page.waitForTimeout(2000)
await page.keyboard.press('Enter')
await page.waitForTimeout(2000)
// ensure we have a lint error
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -301,7 +301,7 @@ test(
}
)
test.skip(
test(
'external change of file contents are reflected in editor',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {

View File

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

View File

@ -9,8 +9,8 @@ import fsp from 'fs/promises'
test(
'export works on the first try',
{ tag: '@electron' },
async ({ page, context }, testInfo) => {
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -118,8 +118,9 @@ test(
// Close the file pane
await u.closeFilePanel()
// wait for it to finish executing (todo: make this more robust)
await page.waitForTimeout(1000)
// FIXME: await scene.waitForExecutionDone() does not work. The modeling indicator stays in -receive-reliable and not execution done
await page.waitForTimeout(10000)
// expect zero errors in guter
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('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
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -641,7 +646,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
width = 0.500
height = 0.500
dia = 4
fn squareHole = (l, w) => {
squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
@ -714,7 +719,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> line(end = [0, -10], tag = $revolveAxis)
|> close()
|> extrude(length = 10)
sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line(end = [0, -10])

View File

@ -112,6 +112,9 @@ export class CmdBarFixture {
* and assumes we are past the `pickCommand` step.
*/
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) {
const arrowButton = this.page.getByRole('button', {
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') => {
// TODO why does this button not work in electron tests?
// await this.cmdBarOpenBtn.click()

View File

@ -1,6 +1,6 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import { uuidv4 } from 'lib/utils'
import { isArray, uuidv4 } from 'lib/utils'
import {
closeDebugPanel,
doAndWaitForImageDiff,
@ -255,7 +255,7 @@ export class SceneFixture {
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return Array.isArray(colour[0])
return isArray(colour[0])
}
export async function expectPixelColor(

View File

@ -20,6 +20,7 @@ export class ToolbarFixture {
shellButton!: Locator
revolveButton!: Locator
offsetPlaneButton!: Locator
helixButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
tangentialArcBtn!: Locator
@ -52,6 +53,7 @@ export class ToolbarFixture {
this.shellButton = page.getByTestId('shell')
this.revolveButton = page.getByTestId('revolve')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.helixButton = page.getByTestId('helix')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
@ -133,6 +135,16 @@ export class ToolbarFixture {
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) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
}

View File

@ -27,7 +27,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -68,7 +68,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ page, homePage }, testInfo) => {
async ({ page }) => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -154,7 +154,7 @@ test.describe('Onboarding tests', () => {
)
test(
'Click through each onboarding step',
'Click through each onboarding step and back',
{
appSettings: {
app: {
@ -187,15 +187,21 @@ test.describe('Onboarding tests', () => {
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
// Finish the onboarding
await nextButton.hover()
await nextButton.click()
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
@ -269,7 +275,7 @@ test.describe('Onboarding tests', () => {
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
@ -336,10 +342,10 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = await page
const avatarLocator = page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
@ -437,7 +443,7 @@ test.describe('Onboarding tests', () => {
)
})
test(
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{
appSettings: {
@ -447,7 +453,7 @@ test(
},
cleanProjectDir: true,
},
async ({ context, page, homePage }, testInfo) => {
async ({ context, page }) => {
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
@ -486,10 +492,6 @@ test(
})
await test.step('Navigate into project', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(
page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible()
@ -514,7 +516,10 @@ test(
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {

View File

@ -29,11 +29,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
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 scene.clickNoWhere()
// FIXME: Do not click, clicking removes the activeLines in future checks
// await scene.clickNoWhere()
await expect(toolbar.extrudeButton).toBeEnabled()
})
@ -199,6 +201,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -221,8 +224,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
|>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|>close()`,
})
await sketchOnAChamfer({
@ -248,8 +251,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
|>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|>close()`,
})
await sketchOnAChamfer({
@ -270,8 +273,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)|
>close(%)`,
|>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|>close()`,
})
/// last one
await sketchOnAChamfer({
@ -289,8 +292,8 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
|>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|>close()`,
})
await test.step('verify at the end of the test that final code is what is expected', async () => {
@ -306,9 +309,9 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
extrude001 = extrude(100, sketch001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(sketch001, length = 100)
|> chamfer({
length = 30,
tags = [getOppositeEdge(seg01)]
@ -333,8 +336,8 @@ profile004 = startProfileAt([-23.43, 19.69], sketch005)
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch004 = startSketchOn(extrude001, seg05)
profile003 = startProfileAt([82.57, 322.96], sketch004)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
@ -346,8 +349,8 @@ profile003 = startProfileAt([82.57, 322.96], sketch004)
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch003 = startSketchOn(extrude001, seg04)
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
@ -359,8 +362,8 @@ profile002 = startProfileAt([-209.64, 255.28], sketch003)
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(extrude001, seg03)
profile001 = startProfileAt([205.96, 254.59], sketch002)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
@ -372,8 +375,9 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`,
{ shouldNormalise: true }
)
@ -401,6 +405,7 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -423,8 +428,8 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|>line(endAbsolute=[profileStartX(%),profileStartY(%)],%)
|>close(%)`,
|>line(endAbsolute=[profileStartX(%),profileStartY(%)])
|>close()`,
})
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')
@ -464,8 +469,8 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)])
|> close()
`,
{ shouldNormalise: true }
)
@ -537,101 +542,6 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
}
await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane()
await toolbar.startSketchPlaneSelection()
await moveToXzPlane()
await clickOnXzPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
})
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
await clickOriginSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin)
})
await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => {
await moveXAxisSloppy()
await clickXAxisSloppy()
await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis)
})
await test.step(`Unequip line tool`, async () => {
await toolbar.lineBtn.click()
await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
})
await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => {
await dragToOffYAxis({
fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] },
})
})
})
// yo
test(`Verify axis, origin, and horizontal snapping`, async ({
page,
homePage,
editor,
toolbar,
scene,
}) => {
const viewPortSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewPortSize)
await homePage.goToModelingScene()
// Constants and locators
// These are mappings from screenspace to KCL coordinates,
// until we merge in our coordinate system helpers
const xzPlane = [
viewPortSize.width * 0.65,
viewPortSize.height * 0.3,
] as const
const originSloppy = {
screen: [
viewPortSize.width / 2 + 3, // 3px off the center of the screen
viewPortSize.height / 2,
],
kcl: [0, 0],
} as const
const xAxisSloppy = {
screen: [
viewPortSize.width * 0.75,
viewPortSize.height / 2 - 3, // 3px off the X-axis
],
kcl: [20.34, 0],
} as const
const offYAxis = {
screen: [
viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
viewPortSize.height * 0.3,
],
kcl: [8.14, 6.78],
} as const
const yAxisSloppy = {
screen: [
viewPortSize.width / 2 + 5, // 5px off the Y-axis
viewPortSize.height * 0.3,
],
kcl: [0, 6.78],
} as const
const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane)
const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen)
const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers(
...xAxisSloppy.screen
)
const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers(
...offYAxis.screen
)
const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
}
await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane()
await toolbar.startSketchPlaneSelection()
@ -670,6 +580,7 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
expectedCodeSnippets.afterSegmentDraggedOnYAxis
)
})
await editor.page.waitForTimeout(1000)
})
test(`Verify user can double-click to edit a sketch`, async ({
@ -781,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 ({
context,
page,
@ -796,6 +1031,9 @@ openSketch = startSketchOn('XY')
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
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 scene.expectPixelColor([50, 51, 96], testPoint, 15)
@ -844,6 +1082,72 @@ openSketch = startSketchOn('XY')
})
})
test('Helix point-and-click', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix( revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5,)`
const expectedLine = `revolutions=1,`
await homePage.goToModelingScene()
// await test.step(`Look for the red of the default plane`, async () => {
// await scene.expectPixelColor([96, 52, 52], testPoint, 15)
// })
await test.step(`Go through the command bar flow`, async () => {
await toolbar.helixButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'revolutions',
currentArgValue: '1',
headerArguments: {
AngleStart: '',
Axis: '',
CounterClockWise: '',
Length: '',
Radius: '',
Revolutions: '',
},
highlightedHeaderArg: 'revolutions',
commandName: 'Helix',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedLine],
highlightedCode: '',
})
// Red plane is now gone, white helix is there
await scene.expectPixelColor([250, 250, 250], testPoint, 15)
})
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
// Red plane is back
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
})
})
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
@ -956,6 +1260,7 @@ loft001 = loft([sketch001, sketch002])
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
@ -1033,7 +1338,7 @@ sketch002 = startSketchOn('XZ')
testPoint.x - 50,
testPoint.y
)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
@ -1068,27 +1373,12 @@ sketch002 = startSketchOn('XZ')
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await toolbar.openPane('code')
await page.waitForTimeout(500)
})
// // yo
// await clickOnSketch2()
// await page.waitForTimeout(500)
// await cmdBar.progressCmdBar()
// await toolbar.openPane('code')
// await page.waitForTimeout(500)
// })
// await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
// await scene.expectPixelColor([135, 64, 73], testPoint, 15)
// await editor.expectEditor.toContain(sweepDeclaration)
// await editor.expectState({
// diagnostics: [],
// activeLines: [sweepDeclaration],
// highlightedCode: '',
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
@ -1613,16 +1903,7 @@ extrude001 = extrude(sketch001, length = -12)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
await scene.waitForExecutionDone()
})
// Test 1: Command bar flow with preselected edges
@ -1847,6 +2128,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// verify modeling scene is loaded
await scene.expectPixelColor(
@ -1969,12 +2251,13 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration =
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
"shell001 = shell(extrude001, faces = ['end'], thickness = 5)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
@ -2067,6 +2350,7 @@ extrude001 = extrude(sketch001, length = 40)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 }
@ -2074,8 +2358,7 @@ extrude001 = extrude(sketch001, length = 40)
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
const mutatedCode = 'xLine(-40, %, $seg01)'
const shellDeclaration =
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
const formattedOutLastLine = '}, extrude001)'
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 5)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
@ -2118,7 +2401,7 @@ extrude001 = extrude(sketch001, length = 40)
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [formattedOutLastLine],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
@ -2172,9 +2455,8 @@ extrude002 = extrude(sketch002, length = 50)
// One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
})`
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code')
@ -2242,7 +2524,7 @@ extrude002 = extrude(sketch002, length = 50)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001)
sweep001 = sweep(sketch001, path = sketch002)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)

View File

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

View File

@ -35,106 +35,113 @@ sketch003 = startSketchOn('XY')
extrude003 = extrude(sketch003, length = 20)
`
test.describe('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
shouldReject: false,
},
{
desc: 'User rejects change',
shouldReject: true,
},
] as const
for (const { desc, shouldReject } of cases) {
test(`${desc}`, async ({
context,
homePage,
cmdBar,
editor,
page,
scene,
}) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.fixme('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
shouldReject: false,
},
{
desc: 'User rejects change',
shouldReject: true,
},
] as const
for (const { desc, shouldReject } of cases) {
test(`${desc}`, async ({
context,
homePage,
cmdBar,
editor,
page,
scene,
}) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const greenCheckCoords = { x: 565, y: 345 }
const body2WallCoords = { x: 609, y: 153 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const green: [number, number, number] = [108, 152, 75]
const notGreen: [number, number, number] = [132, 132, 132]
const body2NotGreen: [number, number, number] = [88, 88, 88]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
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 scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
const body1CapCoords = { x: 571, y: 351 }
const greenCheckCoords = { x: 565, y: 345 }
const body2WallCoords = { x: 609, y: 153 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const green: [number, number, number] = [108, 152, 75]
const notGreen: [number, number, number] = [132, 132, 132]
const body2NotGreen: [number, number, number] = [88, 88, 88]
const submittingToast = page.getByText(
'Submitting to Text-to-CAD API...'
)
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', {
name: 'checkmark Accept',
})
})
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
await test.step('fire off edit prompt', async () => {
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
await expect(successToast).toBeVisible()
})
await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
})
})
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance({')
})
if (!shouldReject) {
await test.step('check accept works and can be "undo"ed', async () => {
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
await test.step('fire off edit prompt', async () => {
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
}) // can take a while
await expect(successToast).toBeVisible()
})
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance({')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance({')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance(')
})
} else {
await test.step('check reject works', async () => {
await rejectBtn.click()
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance({')
})
}
})
}
})
if (!shouldReject) {
await test.step('check accept works and can be "undo"ed', async () => {
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance(')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
})
} else {
await test.step('check reject works', async () => {
await rejectBtn.click()
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance(')
})
}
})
}
})
test.describe('bad path', { tag: ['@skipWin'] }, () => {
test(`bad edit prompt`, async ({
context,
homePage,
@ -148,6 +155,7 @@ test.describe('bad path', { tag: ['@skipWin'] }, () => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(

View File

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

View File

@ -10,6 +10,7 @@ import {
TEST_COLORS,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
@ -188,7 +189,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
const doEditSegmentsByDraggingHandle = async (
page: Page,
homePage: HomePageFixture,
openPanes: string[]
openPanes: string[],
scene: SceneFixture
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
@ -204,6 +206,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -321,7 +324,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
async ({ page, homePage, scene }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -334,14 +337,14 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
}
)
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
async ({ page, homePage, scene }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -349,7 +352,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
}
)
})
@ -551,6 +554,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
homePage,
scene,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -566,6 +570,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -1348,6 +1353,85 @@ test.describe(`Sketching with offset planes`, () => {
})
test.describe('multi-profile sketching', () => {
test(`snapToProfile start only works for current profile`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
profile002 = startProfileAt([40.68, 87.67], sketch001)
|> xLine(239.17, %)
profile003 = startProfileAt([206.63, -56.73], sketch001)
|> xLine(-156.32, %)
`
)
})
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [onSegmentClick] = scene.makeMouseHelpers(604, 349)
const [endOfLowerSegClick, endOfLowerSegMove] = scene.makeMouseHelpers(
697,
360
)
const [profileStartOfHigherSegClick, profileStartOfHigherSegMove] =
scene.makeMouseHelpers(677, 78)
const tanArcLocation = { x: 624, y: 340 } as const
await test.step('enter sketch mode', async () => {
await onSegmentClick({ shouldDbClick: true })
await page.waitForTimeout(600)
})
const codeFromTangentialArc = ` |> tangentialArcTo([39.49, 88.22], %)`
await test.step('check that tangential tool does not snap to other profile starts', async () => {
await toolbar.tangentialArcBtn.click()
await endOfLowerSegMove()
await endOfLowerSegClick()
await profileStartOfHigherSegClick()
await editor.expectEditor.toContain(codeFromTangentialArc)
await editor.expectEditor.not.toContain(
`[profileStartX(%), profileStartY(%)]`
)
})
await test.step('remove tangential arc code to reset', async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, tanArcLocation, 15)
await editor.replaceCode(codeFromTangentialArc, '')
// check pixel is now gray at tanArcLocation to verify code has executed
await scene.expectPixelColor([26, 26, 26], tanArcLocation, 15)
await editor.expectEditor.not.toContain(
`tangentialArcTo([39.49, 88.22], %)`
)
})
await test.step('check that tangential tool does snap to current profile start', async () => {
await expect
.poll(async () => {
await toolbar.lineBtn.click()
return toolbar.lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
await profileStartOfHigherSegMove()
await endOfLowerSegMove()
await endOfLowerSegClick()
await profileStartOfHigherSegClick()
await editor.expectEditor.toContain('line(end = [-10.82, 144.95])')
await editor.expectEditor.not.toContain(
`[profileStartX(%), profileStartY(%)]`
)
})
})
test('Can add multiple profiles to a sketch (all tool types)', async ({
scene,
toolbar,
@ -1389,6 +1473,32 @@ test.describe('multi-profile sketching', () => {
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
const [circle3Point1p1, circle3Point1p1Move] = scene.makeMouseHelpers(
630,
465
)
const [circle3Point1p2, circle3Point1p2Move] = scene.makeMouseHelpers(
673,
340
)
const [circle3Point1p3, circle3Point1p3Move] = scene.makeMouseHelpers(
734,
414
)
const [circle3Point2p1, circle3Point2p1Move] = scene.makeMouseHelpers(
876,
351
)
const [circle3Point2p2, circle3Point2p2Move] = scene.makeMouseHelpers(
875,
279
)
const [circle3Point2p3, circle3Point2p3Move] = scene.makeMouseHelpers(
834,
306
)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
@ -1401,7 +1511,7 @@ test.describe('multi-profile sketching', () => {
)
await endLineStartTanArc()
await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`)
await editor.expectEditor.toContain(`|> line(end = [9.02, -0.55])`)
await toolbar.tangentialArcBtn.click()
await page.waitForTimeout(300)
await page.mouse.click(745, 359)
@ -1417,11 +1527,11 @@ test.describe('multi-profile sketching', () => {
await endArcStartLine()
await page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`)
await editor.expectEditor.toContain(`|> line(end = [-11.73, 5.35])`)
await startProfile1()
await editor.expectEditor.toContain(
`|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
`|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
{ shouldNormalise: true }
)
await page.waitForTimeout(300)
@ -1435,10 +1545,10 @@ test.describe('multi-profile sketching', () => {
)
await profile2Point2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`)
await editor.expectEditor.toContain(`|> line(end = [9.43, -0.68])`)
await profile2Point3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`)
await editor.expectEditor.toContain(`|> line(end = [2.17, -5.97])`)
})
await test.step('create two circles in a row without unequip', async () => {
@ -1465,8 +1575,12 @@ test.describe('multi-profile sketching', () => {
)
})
await test.step('create two corner rectangles in a row without unequip', async () => {
await page.screenshot({ path: 'rectangle.png' })
await toolbar.rectangleBtn.click()
await expect
.poll(async () => {
await toolbar.rectangleBtn.click()
return toolbar.rectangleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await crnRect1point1()
await editor.expectEditor.toContain(
@ -1480,8 +1594,8 @@ test.describe('multi-profile sketching', () => {
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`.replaceAll('\n', '')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
await crnRect2point1()
@ -1501,8 +1615,8 @@ test.describe('multi-profile sketching', () => {
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`.replaceAll('\n', '')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
@ -1526,8 +1640,8 @@ test.describe('multi-profile sketching', () => {
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`.replaceAll('\n', '')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
await page.waitForTimeout(300)
@ -1548,10 +1662,59 @@ test.describe('multi-profile sketching', () => {
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`.replaceAll('\n', '')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
await test.step('create two circle-three-points in a row without an unequip', async () => {
await toolbar.selectCircleThreePoint()
await circle3Point1p1Move()
await circle3Point1p1()
await page.waitForTimeout(300)
await circle3Point1p2Move()
await circle3Point1p2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
'profile009 = circleThreePoint(sketch001, p1 = [8.82, -14.58], p2 = [11.73, -6.1], p3 = [11.83, -6])'
)
await circle3Point1p3Move()
await circle3Point1p3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
'profile009 = circleThreePoint(sketch001, p1 = [8.82, -14.58], p2 = [11.73, -6.1], p3 = [15.87, -11.12])'
)
await circle3Point2p1Move()
await circle3Point2p1()
await page.waitForTimeout(300)
await circle3Point2p2Move()
await circle3Point2p2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
'profile010 = circleThreePoint(sketch001, p1 = [25.5, -6.85], p2 = [25.43, -1.97], p3 = [25.53, -1.87])'
)
await circle3Point2p3Move()
await circle3Point2p3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
'profile010 = circleThreePoint(sketch001, p1 = [25.5, -6.85], p2 = [25.43, -1.97], p3 = [22.65, -3.8])'
)
})
await test.step('double check that circle three point can be unequiped', async () => {
// this was implicitly for other tools, but not for circle three point since it's last
await page.waitForTimeout(300)
await expect
.poll(async () => {
await toolbar.lineBtn.click()
return toolbar.lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
})
})
test('Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile', async ({
@ -1566,9 +1729,9 @@ test.describe('multi-profile sketching', () => {
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
|> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74])
|> line(end = [10.99, -5.22])
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
@ -1579,9 +1742,10 @@ profile002 = startProfileAt([11.19, 5.02], sketch001)
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07], p3 = [18.75, -4.41])
`
)
})
@ -1593,6 +1757,9 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// The text to prompt popover gets in the way of pointOnSegment click otherwise
const moveToClearToolBarPopover = scene.makeMouseHelpers(590, 500)[1]
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [profileEnd] = scene.makeMouseHelpers(970, 105)
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
@ -1608,12 +1775,14 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
const [rectStart] = scene.makeMouseHelpers(794, 322)
const [rectEnd] = scene.makeMouseHelpers(757, 395)
const [circ3PStart] = scene.makeMouseHelpers(854, 332)
const [circ3PEnd] = scene.makeMouseHelpers(870, 275)
await test.step('enter sketch and setup', async () => {
await page.screenshot({ path: 'toolbar1.png' })
await moveToClearToolBarPopover()
await pointOnSegment({ shouldDbClick: true })
await page.waitForTimeout(600)
await page.screenshot({ path: 'toolbar2.png' })
await toolbar.lineBtn.click()
await page.waitForTimeout(100)
})
@ -1622,7 +1791,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await profileEnd()
await page.waitForTimeout(100)
await newProfileEnd()
await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`)
await editor.expectEditor.toContain(`|> line(end = [-11.35, 0.73])`)
await toolbar.lineBtn.click()
await page.waitForTimeout(100)
})
@ -1632,7 +1801,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await page.mouse.down()
await dragSegmentTo()
await page.mouse.up()
await editor.expectEditor.toContain(`line([4.16, -4.51], %)`)
await editor.expectEditor.toContain(`line(end = [4.22, -4.49])`)
})
await test.step('edit existing rect', async () => {
@ -1641,7 +1810,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await rectDragTo()
await page.mouse.up()
await editor.expectEditor.toContain(
`angledLine([-7, 10.2], %, $rectangleSegmentA001)`
`angledLine([-7, 10.27], %, $rectangleSegmentA001)`
)
})
@ -1651,7 +1820,17 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await dragCircleTo()
await page.mouse.up()
await editor.expectEditor.toContain(
`profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)`
`profile003 = circle({ center = [6.92, -4.2], radius = 4.81 }, sketch001)`
)
})
await test.step('edit existing circle three point', async () => {
await circ3PStart()
await page.mouse.down()
await circ3PEnd()
await page.mouse.up()
await editor.expectEditor.toContain(
`profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07], p3 = [19.73, -1.33])`
)
})
@ -1660,7 +1839,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await page.waitForTimeout(100)
await rectStart()
await editor.expectEditor.toContain(
`profile004 = startProfileAt([15.62, -3.83], sketch001)`
`profile005 = startProfileAt([15.68, -3.84], sketch001)`
)
await page.waitForTimeout(100)
await rectEnd()
@ -1668,14 +1847,14 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`|> angledLine([180, 1.97], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) + 90,
3.88
3.89
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`.replaceAll('\n', '')
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
})
@ -1692,9 +1871,9 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
|> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74])
|> line(end = [10.99, -5.22])
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
@ -1705,8 +1884,8 @@ profile002 = startProfileAt([11.19, 5.02], sketch001)
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`
)
@ -1718,6 +1897,9 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// The text to prompt popover gets in the way of pointOnSegment click otherwise
const moveToClearToolBarPopover = scene.makeMouseHelpers(590, 500)[1]
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [segment1Click] = scene.makeMouseHelpers(616, 131)
const sketchIsDrawnProperly = async () => {
@ -1729,6 +1911,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
}
await test.step('enter sketch and setup', async () => {
await moveToClearToolBarPopover()
await pointOnSegment({ shouldDbClick: true })
await page.waitForTimeout(600)
@ -1737,7 +1920,7 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
})
await test.step('select and delete code for a profile', async () => {})
await page.getByText('close(%)').click()
await page.getByText('close()').click()
await page.keyboard.down('Shift')
for (let i = 0; i < 11; i++) {
await page.keyboard.press('ArrowUp')
@ -1761,8 +1944,8 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
await segment1Click()
await editor.expectState({
diagnostics: [],
activeLines: ['|>line([-0.41,6.99],%)'],
highlightedCode: 'line([-0.41,6.99],%)',
activeLines: ['|>line(end = [-0.41,6.99])'],
highlightedCode: 'line(end = [-0.41,6.99])',
})
}).toPass({ timeout: 5_000, intervals: [500] })
@ -1794,12 +1977,12 @@ profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([-63.43, 193.08], sketch001)
|> line([168.52, 149.87], %)
|> line([190.29, -39.18], %)
|> line(end = [168.52, 149.87])
|> line(end = [190.29, -39.18])
|> tangentialArcTo([319.63, 129.65], %)
|> line([-217.65, -21.76], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [-217.65, -21.76])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([16.79, 38.24], sketch001)
|> angledLine([0, 182.82], %, $rectangleSegmentA001)
|> angledLine([
@ -1810,14 +1993,14 @@ profile003 = startProfileAt([16.79, 38.24], sketch001)
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile004 = circle({
center = [280.45, 47.57],
radius = 55.26
}, sketch001)
extrude002 = extrude(50, profile001)
extrude001 = extrude(5, profile003)
extrude002 = extrude(profile001, length = 50)
extrude001 = extrude(profile003, length = 5)
`
)
})
@ -1836,9 +2019,9 @@ extrude001 = extrude(5, profile003)
await page.waitForTimeout(600)
await test.step('check the sketch is still drawn properly', async () => {
await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 596, y: 165 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 641, y: 220 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 763, y: 214 }, 15)
})
})
test('exit new sketch without drawing anything should not be a problem', async ({
@ -1897,10 +2080,10 @@ extrude001 = extrude(5, profile003)
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([85.19, 338.59], sketch001)
|> line([213.3, -94.52], %)
|> line([-230.09, -55.34], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [213.3, -94.52])
|> line(end = [-230.09, -55.34])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn('XY')
profile002 = startProfileAt([85.81, 52.55], sketch002)
@ -1927,7 +2110,7 @@ profile002 = startProfileAt([85.81, 52.55], sketch002)
await startProfileAt()
await page.waitForTimeout(100)
await nextPoint()
await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`)
await editor.expectEditor.toContain(`|> line(end = [126.05, 44.12])`)
})
test('old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch', async ({
homePage,
@ -1941,12 +2124,12 @@ profile002 = startProfileAt([85.81, 52.55], sketch002)
'persistCode',
`thePart = startSketchOn('XZ')
|> startProfileAt([7.53, 10.51], %)
|> line([12.54, 1.83], %)
|> line([6.65, -6.91], %)
|> line([-6.31, -8.69], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(75, thePart)
|> line(end = [12.54, 1.83])
|> line(end = [6.65, -6.91])
|> line(end = [-6.31, -8.69])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(thePart, length = 75)
`
)
})
@ -1984,10 +2167,10 @@ extrude001 = extrude(75, thePart)
await test.step('can continue on to add a new profile to this sketch', async () => {
await profilePoint1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([19.77, -7.08], sketch001)`
`profile001 = startProfileAt([19.69, -7.05], sketch001)`
)
await profilePoint2()
await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`)
await editor.expectEditor.toContain(`|> line(end = [18.97, -18.06])`)
})
})
test('Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections', async ({
@ -2004,15 +2187,15 @@ extrude001 = extrude(75, thePart)
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.71, -3.66], sketch001)
|> line([2.65, 9.02], %, $seg02)
|> line([3.73, -9.36], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(20, profile001)
|> line(end = [2.65, 9.02], tag = $seg02)
|> line(end = [3.73, -9.36], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 20)
sketch002 = startSketchOn(extrude001, seg01)
profile002 = startProfileAt([0.75, 13.46], sketch002)
|> line([4.52, 3.79], %)
|> line([5.98, -2.81], %)
|> line(end = [4.52, 3.79])
|> line(end = [5.98, -2.81])
profile003 = startProfileAt([3.19, 13.3], sketch002)
|> angledLine([0, 6.64], %, $rectangleSegmentA001)
|> angledLine([
@ -2023,38 +2206,38 @@ profile003 = startProfileAt([3.19, 13.3], sketch002)
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile004 = startProfileAt([3.15, 9.39], sketch002)
|> xLine(6.92, %)
|> line([-7.41, -2.85], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [-7.41, -2.85])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile005 = circle({ center = [5.15, 4.34], radius = 1.66 }, sketch002)
profile006 = startProfileAt([9.65, 3.82], sketch002)
|> line([2.38, 5.62], %)
|> line([2.13, -5.57], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [2.38, 5.62])
|> line(end = [2.13, -5.57])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
revolve001 = revolve({
angle = 45,
axis = getNextAdjacentEdge(seg01)
}, profile004)
extrude002 = extrude(4, profile006)
extrude002 = extrude(profile006, length = 4)
sketch003 = startSketchOn('-XZ')
profile007 = startProfileAt([4.8, 7.55], sketch003)
|> line([7.39, 2.58], %)
|> line([7.02, -2.85], %)
|> line(end = [7.39, 2.58])
|> line(end = [7.02, -2.85])
profile008 = startProfileAt([5.54, 5.49], sketch003)
|> line([6.34, 2.64], %)
|> line([6.33, -2.96], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [6.34, 2.64])
|> line(end = [6.33, -2.96])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile009 = startProfileAt([5.23, 1.95], sketch003)
|> line([6.8, 2.17], %)
|> line([7.34, -2.75], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [6.8, 2.17])
|> line(end = [7.34, -2.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile010 = circle({
center = [7.18, -2.11],
radius = 2.67
@ -2069,9 +2252,9 @@ profile011 = startProfileAt([5.07, -6.39], sketch003)
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(2.5, profile011)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(profile011, length = 2.5)
// TODO this breaks the test,
// revolve002 = revolve({ angle = 45, axis = seg02 }, profile008)
`
@ -2226,17 +2409,17 @@ extrude003 = extrude(2.5, profile011)
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([34, 42.66], sketch001)
|> line([102.65, 151.99], %)
|> line([76, -138.66], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [102.65, 151.99])
|> line(end = [76, -138.66])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfileAt([39.43, 172.21], sketch002)
|> xLine(183.99, %)
|> line([-77.95, -145.93], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [-77.95, -145.93])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
loft([profile001, profile002])
`
@ -2282,17 +2465,17 @@ loft([profile001, profile002])
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([34, 42.66], sketch001)
|> line([102.65, 151.99], %)
|> line([76, -138.66], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [102.65, 151.99])
|> line(end = [76, -138.66])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfileAt([39.43, 172.21], sketch002)
|> xLine(183.99, %)
|> line([-77.95, -145.93], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [-77.95, -145.93])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
loft([profile001, profile002])
`
@ -2338,6 +2521,7 @@ loft([profile001, profile002])
await editor.expectEditor.toContain(
`angledLine([0, 106.42], %, $rectangleSegmentA001)`
)
await page.waitForTimeout(100)
})
})

View File

@ -455,7 +455,9 @@ test(
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)
code += `
@ -466,6 +468,11 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.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)
@ -1195,14 +1202,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
|> sweep(path = sweepPath)
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})
@ -1243,14 +1248,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
|> sweep(path = sweepPath)
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 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: 60 KiB

After

Width:  |  Height:  |  Size: 60 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: 51 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: 66 KiB

After

Width:  |  Height:  |  Size: 67 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

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

View File

@ -109,7 +109,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
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.keyboard.up('Shift')
}, [-19, -85, -85])

View File

@ -708,9 +708,8 @@ part002 = startSketchOn('XZ')
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.screenshot({ path: 'ok.png' })
await editor.scrollToText('line(end = [74.36, 130.4])', true)
await page.getByText('line(end = [74.36, 130.4])').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(

View File

@ -63,10 +63,6 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
@ -253,71 +249,75 @@ 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)
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %)
|> line(end=[112.54, 127.64], %, $seg02)
|> line(end=[170.36, -121.61], %, $seg01)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(50, sketch001)
|> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
sketch005 = startSketchOn(extrude001, 'END')
|> startProfileAt([23.24, 136.52], %)
|> line(end=[-8.44, 36.61], %)
|> line(end=[49.4, 2.05], %)
|> line(end=[29.69, -46.95], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [-8.44, 36.61])
|> line(end = [49.4, 2.05])
|> line(end = [29.69, -46.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch003 = startSketchOn(extrude001, seg01)
|> startProfileAt([21.23, 17.81], %)
|> line(end=[51.97, 21.32], %)
|> line(end=[4.07, -22.75], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> line(end = [51.97, 21.32])
|> line(end = [4.07, -22.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-100.54, 16.99], %)
|> line(end=[0, 20.03], %)
|> line(end=[62.61, 0], %, $seg03)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(50, sketch002)
|> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude002 = extrude(sketch002, length = 50)
sketch004 = startSketchOn(extrude002, seg03)
|> startProfileAt([57.07, 134.77], %)
|> line(end=[-4.72, 22.84], %)
|> line(end=[28.8, 6.71], %)
|> line(end=[9.19, -25.33], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(20, sketch004)
|> line(end = [-4.72, 22.84])
|> line(end = [28.8, 6.71])
|> line(end = [9.19, -25.33])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(sketch004, length = 20)
pipeLength = 40
pipeSmallDia = 10
pipeLargeDia = 20
thickness = 0.5
part009 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line(end=[thickness, 0], %)
|> line(end=[0, -1], %)
|> line(end = [thickness, 0])
|> line(end = [0, -1])
|> angledLineToX({
angle = 60,
to = pipeSmallDia + thickness
}, %)
|> line(end=[0, -pipeLength], %)
|> line(end = [0, -pipeLength])
|> angledLineToX({
angle = -60,
to = pipeLargeDia + thickness
}, %)
|> line(end=[0, -1], %)
|> line(end=[-thickness, 0], %)
|> line(end=[0, 1], %)
|> line(end = [0, -1])
|> line(end = [-thickness, 0])
|> line(end = [0, 1])
|> angledLineToX({ angle = 120, to = pipeSmallDia }, %)
|> line(end=[0, pipeLength], %)
|> line(end = [0, pipeLength])
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close(%)
|> close()
rev = revolve({ axis = 'y' }, part009)
sketch006 = startSketchOn('XY')
profile001 = circle({
@ -334,12 +334,12 @@ profile002 = startProfileAt([86.92, -63.81], sketch006)
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute=[profileStartX(%), profileStartY(%)], %)
|> close(%)
|> 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], %)
|> line(end = [26.95, 24.21])
|> line(end = [20.91, -28.61])
|> line(end = [32.46, 18.71])
`
)
@ -347,10 +347,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -449,7 +446,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
const codeToBeDeletedSnippet =
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
await expect(page.locator('.cm-activeLine')).toHaveText(
' |> line([20.91, -28.61], %)'
' |> line(end = [20.91, -28.61])'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
@ -944,6 +941,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
page,
homePage,
scene,
}) => {
const cases = [
{
@ -979,6 +977,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -1012,6 +1011,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({
page,
homePage,
scene,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -1030,6 +1030,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -1063,19 +1064,19 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(15)
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 expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(extrudeText)
)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15)
@ -1086,7 +1087,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300)
await page.waitForTimeout(1000)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face
@ -1100,11 +1101,11 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText)
)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
await page.mouse.click(cap.x, cap.y)
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 page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
@ -1151,7 +1152,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(end = [4.95, -8])
|> line(end = [-20.38, -10.12])
|> line(end = [-15.79, 17.08])
fn yohey = (pos) => {
sketch004 = startSketchOn('XZ')
${extrudeAndEditBlockedInFunction}
@ -1161,7 +1162,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(end = [-15.79, 17.08])
return ''
}
yohey([15.79, -34.6])
`
)

View File

@ -896,4 +896,53 @@ test.describe('Testing settings', () => {
})
}
)
test(`Change inline units setting`, async ({
page,
homePage,
context,
editor,
}) => {
const initialInlineUnits = 'yd'
const editedInlineUnits = { short: 'mm', long: 'Millimeters' }
const inlineSettingsString = (s: string) =>
`@settings(defaultLengthUnit = ${s})`
const unitsIndicator = page.getByRole('button', {
name: 'Current units are:',
})
const unitsChangeButton = (name: string) =>
page.getByRole('button', { name, exact: true })
await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
})
await test.step(`Initial units from settings`, async () => {
await homePage.openProject('project-000')
await expect(unitsIndicator).toHaveText('Current units are: in')
})
await test.step(`Manually write inline settings`, async () => {
await editor.openPane()
await editor.replaceCode(
`fn cube`,
`${inlineSettingsString(initialInlineUnits)}
fn cube`
)
await expect(unitsIndicator).toContainText(initialInlineUnits)
})
await test.step(`Change units setting via lower-right control`, async () => {
await unitsIndicator.click()
await unitsChangeButton(editedInlineUnits.long).click()
await expect(
page.getByText(`Updated per-file units to ${editedInlineUnits.short}`)
).toBeVisible()
})
})
})

View File

@ -3,7 +3,7 @@ import { getUtils, createProject } from './test-utils'
import { join } from 'path'
import fs from 'fs'
test.describe('Text-to-CAD tests', () => {
test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page)

View File

@ -32,15 +32,18 @@ test.fixme('Units menu', async ({ page, homePage }) => {
await expect(unitsMenuButton).toContainText('mm')
})
test('Successful export shows a success toast', async ({ page, homePage }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`topAng = 25
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`topAng = 25
bottomAng = 35
baseLen = 3.5
baseHeight = 1
@ -78,26 +81,27 @@ part001 = startSketchOn('-XZ')
|> xLineTo(ZERO, %)
|> close()
|> 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 ({
page,
@ -231,8 +235,12 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await page.keyboard.press('l')
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.
// On close it will exit sketch mode.
@ -457,7 +465,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
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)
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -483,11 +491,7 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -532,11 +536,11 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect.poll(u.normalisedEditorCode).toContain(
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
profile001 = startProfileAt([-12.88, 6.66], sketch002)
|> line(end = [2.71, -0.22], %)
|> line(end = [-2.87, -1.38], %)
|> lineTo(endAbsolute = [profileStartX(%), profileStartY(%)], %)
|> close(%)
profile001 = startProfileAt([-12.34, 12.34], sketch002)
|> line(end = [12.34, -12.34])
|> line(end = [-12.34, -12.34])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`)
)
@ -591,10 +595,9 @@ profile001 = startProfileAt([-12.88, 6.66], sketch002)
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.getByRole('button', { name: 'checkmark Submit command' }).click()
await cmdBar.progressCmdBar()
const result2 = result.genNext`
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`

View File

@ -32,10 +32,6 @@ win:
arch:
- x64
- arm64
# - target: msi
# arch:
# - x64
# - arm64
signingHashAlgorithms:
- sha256
sign: "./scripts/sign-win.js"
@ -47,15 +43,12 @@ win:
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
# msi:
# oneClick: false
# perMachine: true
nsis:
oneClick: false
perMachine: true
allowElevation: true
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
include: "./scripts/installer.nsh"
linux:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.13",
"@kittycad/lib": "2.0.17",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"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/main/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/kw-pattern-transform2/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)",
"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",
@ -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: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: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:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
},
@ -158,8 +159,8 @@
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/node": "^22.7.8",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.1",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4",
@ -200,7 +201,7 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.19.1",
"typescript-eslint": "^8.23.0",
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -59,7 +59,9 @@ UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName (":" | Equals) expression }
ArgumentList { "(" commaSep<expression> ")" }
LabeledArgument { ArgumentLabel Equals expression }
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
type[@isGroup=Type] {
@specialize[@name=PrimitiveType]<
@ -74,6 +76,8 @@ VariableDefinition { identifier }
VariableName { identifier }
ArgumentLabel { identifier }
@skip { whitespace | LineComment | BlockComment }
kw<term> { @specialize[@name={term}]<identifier, term> }

View File

@ -0,0 +1,85 @@
# empty
f()
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList)))
# single anon arg
f(1)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number))))
# deprecated multiple anon args
f(1, 2)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number,
Number))))
# deprecated trailing %
startSketchOn('XY')
|> line([thickness, 0], %)
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(ArrayExpression(VariableName,
Number),
PipeSubstitution)))))
# % and named arg
startSketchOn('XY')
|> line(%, end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(PipeSubstitution,
LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# implied % and named arg
startSketchOn('XY')
|> line(end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# multiple named arg
ngon(plane = "XY", numSides = 5, radius = pentR)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
String),
LabeledArgument(ArgumentLabel,
Equals,
Number),
LabeledArgument(ArgumentLabel,
Equals,
VariableName)))))

View File

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

View File

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

View File

@ -1 +1,226 @@
404: Not Found
[
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
"multipleFiles": false,
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
"multipleFiles": false,
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
"multipleFiles": false,
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
"multipleFiles": false,
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
"multipleFiles": true,
"title": "Car Wheel Assembly",
"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",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
"multipleFiles": false,
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
"multipleFiles": false,
"title": "Enclosure",
"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",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"multipleFiles": false,
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
"multipleFiles": false,
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
"multipleFiles": false,
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
"multipleFiles": false,
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
"multipleFiles": false,
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
"multipleFiles": false,
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
"multipleFiles": false,
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
"multipleFiles": false,
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
"multipleFiles": false,
"title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
"multipleFiles": false,
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
"multipleFiles": false,
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
"multipleFiles": false,
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
"multipleFiles": true,
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
"multipleFiles": false,
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
"multipleFiles": false,
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
"multipleFiles": false,
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
"multipleFiles": false,
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
"multipleFiles": false,
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
"multipleFiles": false,
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
"multipleFiles": false,
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
"multipleFiles": false,
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
"multipleFiles": true,
"title": "Walkie Talkie",
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
"multipleFiles": false,
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
}
]

View File

@ -11,6 +11,7 @@ echo "$PACKAGE" > package.json
# 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 '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
# Release notes
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

@ -31,7 +31,6 @@ import {
recast,
defaultSourceRange,
resultIsOk,
ProgramMemory,
topLevelRange,
} from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
@ -146,7 +145,8 @@ export const ClientSideScene = ({
state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches({ Sketch: 'Circle tool' })
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'Circle three point tool' })
) {
cursor = 'crosshair'
} else {
@ -427,7 +427,7 @@ export async function deleteSegment({
modifiedAst = deleteSegmentFromPipeExpression(
dependentRanges,
modifiedAst,
kclManager.programMemory,
kclManager.variables,
codeManager.code,
pathToNode
)
@ -441,8 +441,8 @@ export async function deleteSegment({
const testExecute = await executeAst({
ast: modifiedAst,
engineCommandManager: engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride: ProgramMemory.empty(),
isMock: true,
usePrevMemory: false,
})
if (testExecute.errors.length) {
toast.error('Segment tag used outside of current Sketch. Could not delete.')
@ -679,7 +679,7 @@ const ConstraintSymbol = ({
shallowPath,
argPosition,
kclManager.ast,
kclManager.programMemory
kclManager.variables
)
if (!transform) return

View File

@ -1,12 +1,8 @@
import {
BoxGeometry,
Color,
DoubleSide,
Group,
Intersection,
Line,
LineDashedMaterial,
BufferGeometry,
Mesh,
MeshBasicMaterial,
Object3D,
@ -17,7 +13,6 @@ import {
Points,
Quaternion,
Scene,
SphereGeometry,
Vector2,
Vector3,
} from 'three'
@ -45,7 +40,6 @@ import {
PathToNode,
PipeExpression,
Program,
ProgramMemory,
recast,
Sketch,
VariableDeclaration,
@ -57,6 +51,7 @@ import {
SourceRange,
topLevelRange,
CallExpressionKw,
VariableMap,
} from 'lang/wasm'
import {
engineCommandManager,
@ -70,7 +65,6 @@ import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { executeAst, ToolTip } from 'lang/langHelpers'
import {
createProfileStartHandle,
createCircleGeometry,
SegmentUtils,
segmentUtils,
} from './segments'
@ -121,8 +115,6 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { radToDeg } from 'three/src/math/MathUtils'
import toast from 'react-hot-toast'
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
@ -172,7 +164,6 @@ type Vec3Array = [number, number, number]
export class SceneEntities {
engineCommandManager: EngineCommandManager
scene: Scene
sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
activeSegments: { [key: string]: Group } = {}
intersectionPlane: Mesh | null = null
axisGroup: Group | null = null
@ -584,33 +575,25 @@ export class SceneEntities {
selectionRanges?: Selections
}): Promise<{
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}> {
this.createIntersectionPlane()
const prepared = this.prepareTruncatedMemoryAndAst(
sketchNodePaths,
maybeModdedAst
)
const prepared = this.prepareTruncatedAst(sketchNodePaths, maybeModdedAst)
if (err(prepared)) return Promise.reject(prepared)
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
prepared
const { truncatedAst, variableDeclarationName } = prepared
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
const sketchesInfo = getSketchesInfo({
sketchNodePaths,
ast: maybeModdedAst,
programMemory,
variables: execState.variables,
})
this.sceneProgramMemory = programMemory
const group = new Group()
position && group.position.set(...position)
group.userData = {
@ -781,7 +764,6 @@ export class SceneEntities {
return {
truncatedAst,
programMemoryOverride,
variableDeclarationName,
}
}
@ -837,16 +819,16 @@ export class SceneEntities {
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
const sg = sketchFromKclValue(
kclManager.programMemory.get(variableDeclarationName),
kclManager.variables[variableDeclarationName],
variableDeclarationName
)
if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `.length -1`
const mod = addNewSketchLn({
node: _ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
input: {
type: 'straight-segment',
to: lastSeg.to,
@ -865,7 +847,7 @@ export class SceneEntities {
if (shouldTearDown) this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride } = await this.setupSketch({
const { truncatedAst } = await this.setupSketch({
sketchEntryNodePath,
sketchNodePaths,
forward,
@ -888,14 +870,14 @@ export class SceneEntities {
let intersection2d = intersectionPoint?.twoD
const intersectsProfileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
.find(isGroupStartProfileForCurrentProfile(sketchEntryNodePath))
let modifiedAst: Program | Error = structuredClone(kclManager.ast)
const sketch = sketchFromPathToNode({
pathToNode: sketchEntryNodePath,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
})
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject(new Error('No sketch found'))
@ -913,10 +895,10 @@ export class SceneEntities {
])
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
pathToNode: sketchEntryNodePath,
expressions: [
lastSegment.type === 'TangentialArcTo'
segmentName === 'tangentialArcTo'
? createCallExpressionStdLib('tangentialArcTo', [
originCoords,
createPipeSubstitution(),
@ -929,7 +911,7 @@ export class SceneEntities {
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
modifiedAst = addCloseToPipe({
node: modifiedAst,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
pathToNode: sketchEntryNodePath,
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
@ -983,7 +965,7 @@ export class SceneEntities {
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
input: {
type: 'straight-segment',
from: [lastSegment.to[0], lastSegment.to[1]],
@ -1038,7 +1020,6 @@ export class SceneEntities {
planeNodePath,
draftInfo: {
truncatedAst,
programMemoryOverride,
variableDeclarationName,
},
})
@ -1119,7 +1100,7 @@ export class SceneEntities {
return Promise.reject(_recastAst)
_ast = _recastAst.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
const { truncatedAst } = await this.setupSketch({
sketchEntryNodePath: updatedEntryNodePath,
sketchNodePaths: updatedSketchNodePaths,
forward,
@ -1158,12 +1139,9 @@ export class SceneEntities {
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(programMemory.get(varName), varName)
const sketch = sketchFromKclValue(execState.variables[varName], varName)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1225,7 +1203,11 @@ export class SceneEntities {
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
},
})
return { updatedEntryNodePath, updatedSketchNodePaths }
return {
updatedEntryNodePath,
updatedSketchNodePaths,
expressionIndexToDelete: insertIndex,
}
}
setupDraftCenterRectangle = async (
sketchEntryNodePath: PathToNode,
@ -1299,7 +1281,7 @@ export class SceneEntities {
return Promise.reject(__recastAst)
_ast = __recastAst.program
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
const { truncatedAst } = await this.setupSketch({
sketchEntryNodePath: updatedEntryNodePath,
sketchNodePaths: updatedSketchNodePaths,
forward,
@ -1345,12 +1327,9 @@ export class SceneEntities {
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(programMemory.get(varName), varName)
const sketch = sketchFromKclValue(execState.variables[varName], varName)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1416,7 +1395,11 @@ export class SceneEntities {
}
},
})
return { updatedEntryNodePath, updatedSketchNodePaths }
return {
updatedEntryNodePath,
updatedSketchNodePaths,
expressionIndexToDelete: insertIndex,
}
}
setupDraftCircleThreePoint = async (
sketchEntryNodePath: PathToNode,
@ -1469,7 +1452,7 @@ export class SceneEntities {
// do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast)
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
const { truncatedAst } = await this.setupSketch({
sketchEntryNodePath: updatedEntryNodePath,
sketchNodePaths: updatedSketchNodePaths,
forward,
@ -1481,12 +1464,13 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onMove: async (args) => {
const firstProfileIndex = Number(updatedSketchNodePaths[0][1][0])
const nodePathWithCorrectedIndexForTruncatedAst =
structuredClone(updatedEntryNodePath)
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
Number(planeNodePath[1][0]) -
1
firstProfileIndex
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
nodePathWithCorrectedIndexForTruncatedAst,
@ -1499,7 +1483,7 @@ export class SceneEntities {
if (sketchInit.type === 'CallExpressionKw') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
kclManager.variables,
{
type: 'path',
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
@ -1521,12 +1505,9 @@ export class SceneEntities {
const { execState } = await executeAst({
ast: modded,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(programMemory.get(varName), varName)
const sketch = sketchFromKclValue(execState.variables[varName], varName)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1567,7 +1548,7 @@ export class SceneEntities {
if (sketchInit.type === 'CallExpressionKw') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
kclManager.variables,
{
type: 'path',
pathToNode: updatedEntryNodePath,
@ -1596,7 +1577,11 @@ export class SceneEntities {
}
},
})
return { updatedEntryNodePath, updatedSketchNodePaths }
return {
updatedEntryNodePath,
updatedSketchNodePaths,
expressionIndexToDelete: insertIndex,
}
}
setupDraftCircle = async (
sketchEntryNodePath: PathToNode,
@ -1650,7 +1635,7 @@ export class SceneEntities {
// do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast)
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
const { truncatedAst } = await this.setupSketch({
sketchEntryNodePath: updatedEntryNodePath,
sketchNodePaths: updatedSketchNodePaths,
forward,
@ -1683,7 +1668,7 @@ export class SceneEntities {
if (sketchInit.type === 'CallExpression') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
kclManager.variables,
{
type: 'path',
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
@ -1702,12 +1687,9 @@ export class SceneEntities {
const { execState } = await executeAst({
ast: modded,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(programMemory.get(varName), varName)
const sketch = sketchFromKclValue(execState.variables[varName], varName)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1751,7 +1733,7 @@ export class SceneEntities {
if (sketchInit.type === 'CallExpression') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
kclManager.variables,
{
type: 'path',
pathToNode: updatedEntryNodePath,
@ -1780,7 +1762,11 @@ export class SceneEntities {
}
},
})
return { updatedEntryNodePath, updatedSketchNodePaths }
return {
updatedEntryNodePath,
updatedSketchNodePaths,
expressionIndexToDelete: insertIndex,
}
}
setupSketchIdleCallbacks = ({
sketchEntryNodePath,
@ -1842,7 +1828,7 @@ export class SceneEntities {
const sketch = sketchFromPathToNode({
pathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
})
if (trap(sketch)) return
if (!sketch) {
@ -1855,7 +1841,7 @@ export class SceneEntities {
const prevSegment = sketch.paths[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
input: {
type: 'straight-segment',
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
@ -1932,15 +1918,15 @@ export class SceneEntities {
...this.mouseEnterLeaveCallbacks(),
})
}
prepareTruncatedMemoryAndAst = (
prepareTruncatedAst = (
sketchNodePaths: PathToNode[],
ast?: Node<Program>,
draftSegment?: DraftSegment
) =>
prepareTruncatedMemoryAndAst(
prepareTruncatedAst(
sketchNodePaths,
ast || kclManager.ast,
kclManager.lastSuccessfulProgramMemory,
kclManager.lastSuccessfulVariables,
draftSegment
)
onDragSegment({
@ -1960,7 +1946,6 @@ export class SceneEntities {
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: {
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}
}) {
@ -1968,7 +1953,7 @@ export class SceneEntities {
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
.find(isGroupStartProfileForCurrentProfile(sketchEntryNodePath))
const intersection2d = intersectsProfileStart
? new Vector2(
intersectsProfileStart.position.x,
@ -2105,12 +2090,12 @@ export class SceneEntities {
to: dragTo,
from,
},
previousProgramMemory: kclManager.programMemory,
variables: kclManager.variables,
})
} else {
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
kclManager.variables,
{
type: 'sourceRange',
sourceRange: topLevelRange(node.start, node.end),
@ -2123,9 +2108,9 @@ export class SceneEntities {
modifiedAst = modded.modifiedAst
const info = draftInfo
? draftInfo
: this.prepareTruncatedMemoryAndAst(sketchNodePaths || [], modifiedAst)
: this.prepareTruncatedAst(sketchNodePaths || [], modifiedAst)
if (trap(info, { suppress: true })) return
const { truncatedAst, programMemoryOverride } = info
const { truncatedAst } = info
;(async () => {
const code = recast(modifiedAst)
if (trap(code)) return
@ -2136,15 +2121,13 @@ export class SceneEntities {
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
isMock: true,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const variables = execState.variables
const sketchesInfo = getSketchesInfo({
sketchNodePaths,
ast: truncatedAst,
programMemory,
variables,
})
const callBacks: (() => SegmentOverlayPayload | null)[] = []
for (const sketchInfo of sketchesInfo) {
@ -2470,15 +2453,14 @@ export class SceneEntities {
// calculations/pure-functions/easy to test so no excuse not to
function prepareTruncatedMemoryAndAst(
function prepareTruncatedAst(
sketchNodePaths: PathToNode[],
ast: Node<Program>,
programMemory: ProgramMemory,
variables: VariableMap,
draftSegment?: DraftSegment
):
| {
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
// can I remove the below?
variableDeclarationName: string
}
@ -2497,7 +2479,7 @@ function prepareTruncatedMemoryAndAst(
if (err(_node)) return _node
const variableDeclarationName = _node.node?.declaration?.id?.name || ''
const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variables[variableDeclarationName],
variableDeclarationName
)
if (err(sg)) return sg
@ -2556,43 +2538,8 @@ function prepareTruncatedMemoryAndAst(
body: structuredClone(_ast.body.slice(bodyStartIndex, bodyEndIndex + 1)),
}
// Grab all the TagDeclarators and TagIdentifiers from memory.
let start = _node.node.start
const programMemoryOverride = programMemory.filterVariables(true, (value) => {
if (
!('__meta' in value) ||
value.__meta === undefined ||
value.__meta.length === 0 ||
value.__meta[0].sourceRange === undefined
) {
return false
}
if (value.__meta[0].sourceRange[0] >= start) {
// We only want things before our start point.
return false
}
return value.type === 'TagIdentifier'
})
if (err(programMemoryOverride)) return programMemoryOverride
for (let i = 0; i < bodyStartIndex; i++) {
const node = _ast.body[i]
if (node.type !== 'VariableDeclaration') {
continue
}
const name = node.declaration.id.name
const memoryItem = programMemory.get(name)
if (!memoryItem) {
continue
}
const error = programMemoryOverride.set(name, structuredClone(memoryItem))
if (err(error)) return error
}
return {
truncatedAst,
programMemoryOverride,
variableDeclarationName,
}
}
@ -2609,14 +2556,14 @@ export function getParentGroup(
return null
}
export function sketchFromPathToNode({
function sketchFromPathToNode({
pathToNode,
ast,
programMemory,
variables,
}: {
pathToNode: PathToNode
ast: Program
programMemory: ProgramMemory
variables: VariableMap
}): Sketch | null | Error {
const _varDec = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
@ -2625,7 +2572,7 @@ export function sketchFromPathToNode({
)
if (err(_varDec)) return _varDec
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
const result = variables[varDec?.id?.name || '']
if (result?.type === 'Solid') {
return result.value.sketch
}
@ -2664,7 +2611,7 @@ export function getSketchQuaternion(
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
variables: kclManager.variables,
})
if (err(sketch)) return sketch
const zAxis = sketch?.on.zAxis || sketchNormalBackUp
@ -2672,23 +2619,13 @@ export function getSketchQuaternion(
return getQuaternionFromZAxis(massageFormats(zAxis))
}
export async function getSketchOrientationDetails(
sketchEntryNodePath: PathToNode
): Promise<{
export async function getSketchOrientationDetails(sketch: Sketch): Promise<{
quat: Quaternion
sketchDetails: Omit<
SketchDetails & { faceId?: string },
'sketchNodePaths' | 'sketchEntryNodePath' | 'planeNodePath'
>
}> {
const sketch = sketchFromPathToNode({
pathToNode: sketchEntryNodePath,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (sketch.on.type === 'plane') {
const zAxis = sketch?.on.zAxis
return {
@ -2701,30 +2638,24 @@ export async function getSketchOrientationDetails(
},
}
}
const faceInfo = await getFaceDetails(sketch.on.id)
if (sketch.on.type === 'face') {
const faceInfo = await getFaceDetails(sketch.on.id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return Promise.reject('face info')
const { z_axis, y_axis, origin } = faceInfo
const quaternion = quaternionFromUpNForward(
new Vector3(y_axis.x, y_axis.y, y_axis.z),
new Vector3(z_axis.x, z_axis.y, z_axis.z)
)
return {
quat: quaternion,
sketchDetails: {
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z],
faceId: sketch.on.id,
},
}
}
return Promise.reject(
'sketch.on.type not recognized, has a new type been added?'
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return Promise.reject('face info')
const { z_axis, y_axis, origin } = faceInfo
const quaternion = quaternionFromUpNForward(
new Vector3(y_axis.x, y_axis.y, y_axis.z),
new Vector3(z_axis.x, z_axis.y, z_axis.z)
)
return {
quat: quaternion,
sketchDetails: {
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z],
faceId: sketch.on.id,
},
}
}
/**
@ -2797,11 +2728,11 @@ function massageFormats(a: Vec3Array | Point3d): Vector3 {
function getSketchesInfo({
sketchNodePaths,
ast,
programMemory,
variables,
}: {
sketchNodePaths: PathToNode[]
ast: Node<Program>
programMemory: ProgramMemory
variables: VariableMap
}): {
sketch: Sketch
pathToNode: PathToNode
@ -2814,7 +2745,7 @@ function getSketchesInfo({
const sketch = sketchFromPathToNode({
pathToNode: path,
ast,
programMemory,
variables,
})
if (err(sketch)) continue
if (!sketch) continue
@ -2848,3 +2779,13 @@ function computeSelectionFromSourceRangeAndAST(
}
return selection
}
function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
return (group: Group<Object3DEventMap> | null) => {
if (group?.name !== PROFILE_START) return false
const groupExpressionIndex = Number(group.userData.pathToNode[1][0])
const isProfileStartOfCurrentExpr =
groupExpressionIndex === sketchEntryNodePath[1][0]
return isProfileStartOfCurrentExpr
}
}

View File

@ -54,13 +54,11 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import {
ARROWHEAD,
CIRCLE_3_POINT_DRAFT_CIRCLE,
DRAFT_POINT,
SceneInfra,
SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT,
SKETCH_LAYER,
} from './sceneInfra'
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { normaliseAngle, roundOff } from 'lib/utils'
@ -71,7 +69,7 @@ import {
} from 'machines/modelingMachine'
import { SegmentInputs } from 'lang/std/stdTypes'
import { err } from 'lib/trap'
import { editorManager, sceneInfra } from 'lib/singletons'
import { sceneInfra } from 'lib/singletons'
import { Selections } from 'lib/selections'
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
import { commandBarActor } from 'machines/commandBarMachine'
@ -1012,10 +1010,7 @@ function createCircleCenterHandle(
function createCircleThreePointHandle(
scale = 1,
theme: Themes,
name:
| 'circle-three-point-handle1'
| 'circle-three-point-handle2'
| 'circle-three-point-handle3',
name: `circle-three-point-handle${'1' | '2' | '3'}`,
color?: number
): Group {
const circleCenterGroup = new Group()

View File

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

View File

@ -196,6 +196,7 @@ function ReviewingButton() {
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-submit"
iconStart={{
icon: 'checkmark',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
@ -214,6 +215,7 @@ function GatheringArgsButton() {
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-continue"
iconStart={{
icon: 'arrowRight',
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 { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import toast from 'react-hot-toast'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
@ -97,6 +98,7 @@ function CommandBarKclInput({
value,
initialVariableName,
})
const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key,
detail: String(roundOff(v.value as number)),
@ -170,7 +172,15 @@ function CommandBarKclInput({
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
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(
createNewVariable

View File

@ -57,7 +57,7 @@ function CommandComboBox({
onKeyDown={(event) => {
if (
(event.metaKey && event.key === 'k') ||
(event.key === 'Backspace' && !event.currentTarget.value)
event.key === 'Escape'
) {
event.preventDefault()
commandBarActor.send({ type: 'Close' })

View File

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

View File

@ -329,7 +329,7 @@ export const FileMachineProvider = ({
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode({ zoomToFit: true })
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({

View File

@ -21,6 +21,7 @@ import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) {
@ -189,25 +190,24 @@ const FileTreeItem = ({
// Because subtrees only render when they are opened, that means this
// only listens when they open. Because this acts like a useEffect, when
// the ReactNodes are destroyed, so is this listener :)
/** Disabling this in favor of faster file writes until we fix file writing **/
/* useFileSystemWatcher(
* async (eventType, path) => {
* // Prevents a cyclic read / write causing editor problems such as
* // misplaced cursor positions.
* if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
* codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
* return
* }
useFileSystemWatcher(
async (eventType, path) => {
// Prevents a cyclic read / write causing editor problems such as
// misplaced cursor positions.
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
return
}
* if (isCurrentFile && eventType === 'change') {
* let code = await window.electron.readFile(path, { encoding: 'utf-8' })
* code = normalizeLineEndings(code)
* codeManager.updateCodeStateEditor(code)
* }
* fileSend({ type: 'Refresh' })
* },
* [fileOrDir.path]
* ) */
if (isCurrentFile && eventType === 'change') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)
}
fileSend({ type: 'Refresh' })
},
[fileOrDir.path]
)
const showNewTreeEntry =
newTreeEntry !== undefined &&
@ -263,7 +263,7 @@ const FileTreeItem = ({
await codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
await kclManager.executeCode({ zoomToFit: true })
await kclManager.executeCode(true)
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)

View File

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

View File

@ -69,7 +69,6 @@ import {
startSketchOnDefault,
} from 'lang/modifyAst'
import {
CodeRef,
PathToNode,
Program,
VariableDeclaration,
@ -84,7 +83,6 @@ import {
isCursorInFunctionDefinition,
traverse,
} from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
@ -102,6 +100,7 @@ import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
getFaceCodeRef,
getPathsFromArtifact,
getPlaneFromArtifact,
} from 'lang/std/artifactGraph'
@ -109,6 +108,7 @@ import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -182,57 +182,38 @@ export const ModelingMachineProvider = ({
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
// tsc reports this typing as perfectly fine, but eslint is complaining.
// It's actually nonsensical, so I'm quieting.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
'sketch exit execute': async ({
context: { store },
}): Promise<void> => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager.executeCode().then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
'sketch exit execute': ({ context: { store } }) => {
// TODO: Remove this async callback. For some reason eslint wouldn't
// let me disable @typescript-eslint/no-misused-promises for the line.
;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
})
})
.catch(reportRejection)
.catch(reportRejection)
})().catch(reportRejection)
},
'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {}
@ -371,11 +352,83 @@ export const ModelingMachineProvider = ({
otherSelections: [],
}
} 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 = {
graphSelections: [
...selectionRanges.graphSelections,
setSelections.selection,
],
graphSelections: updatedSelections,
otherSelections: selectionRanges.otherSelections,
}
}
@ -649,8 +702,6 @@ export const ModelingMachineProvider = ({
async ({ input: { sketchDetails } }) => {
if (!sketchDetails) return
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 varDecIndex = sketchDetails.planeNodePath[1][0]
@ -748,37 +799,49 @@ export const ModelingMachineProvider = ({
}),
'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => {
const sketchPathToNode =
selectionRanges.graphSelections[0]?.codeRef?.pathToNode
const plane = getPlaneFromArtifact(
selectionRanges.graphSelections[0].artifact,
engineCommandManager.artifactGraph
)
if (err(plane)) return Promise.reject(plane)
const info = await getSketchOrientationDetails(
sketchPathToNode || []
const sketch = Object.values(kclManager.execState.variables).find(
(variable) =>
variable?.type === 'Sketch' &&
variable.value.artifactId === plane.pathIds[0]
)
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
const info = await getSketchOrientationDetails(sketch.value)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
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: selectionRanges.graphSelections[0].artifact,
sketchPathToNode: sketchPathToNode || [],
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 =
'faceCodeRef' in plane && plane.faceCodeRef
? plane.faceCodeRef
: 'codeRef' in plane && plane.codeRef
? plane.codeRef
: null
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 {
sketchEntryNodePath: sketchPathToNode || [],
sketchEntryNodePath: sketchArtifact.codeRef.pathToNode || [],
sketchNodePaths: sketchPaths,
planeNodePath: codeRef.pathToNode,
planeNodePath,
zAxis: info.sketchDetails.zAxis || null,
yAxis: info.sketchDetails.yAxis || null,
origin: info.sketchDetails.origin.map(
@ -1443,6 +1506,7 @@ export const ModelingMachineProvider = ({
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1,
} as const
if (
!sketchDetails.sketchNodePaths.length &&
@ -1456,22 +1520,44 @@ export const ModelingMachineProvider = ({
sketchDetails.sketchEntryNodePath
)
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
if (!doesNeedSplitting) return existingSketchInfoNoOp
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 splitResult = splitPipedProfile(
kclManager.ast,
sketchDetails.sketchEntryNodePath
)
if (err(splitResult)) return reject(splitResult)
await kclManager.executeAstMock(splitResult.modifiedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
splitResult.modifiedAst
)
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]
}
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
return {
updatedEntryNodePath: splitResult.pathToProfile,
updatedSketchNodePaths: [splitResult.pathToProfile],
updatedEntryNodePath: pathToProfile,
updatedSketchNodePaths: updatedSketchNodePaths,
updatedPlaneNodePath: sketchDetails.planeNodePath,
expressionIndexToDelete: -1,
}
}
),
@ -1482,6 +1568,7 @@ export const ModelingMachineProvider = ({
selections: input.selection,
token,
artifactGraph: engineCommandManager.artifactGraph,
projectName: context.project.name,
})
}),
},

View File

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

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