Compare commits

...

126 Commits

Author SHA1 Message Date
6c78dbd4c8 Force dev from main.ts :sad: 2025-01-22 16:17:19 +01:00
059593372a Attempt at building debug and dev 2025-01-22 15:47:42 +01:00
1ba8c5af00 @pierremtb spinner feedback 2025-01-22 09:35:19 -05:00
410b4e81eb @lf94 nit 2025-01-22 09:32:46 -05:00
30275d86cc Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-22 09:12:59 -05:00
39c40b2cde fmt 2025-01-22 09:12:40 -05:00
10789d9c3c set scene units based on a module's default units (#5127)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-22 15:23:55 +13:00
67cc4f5835 Tweaks to clarify tooltips from tool dropdown menus (#5123)
* Separate content from ToolbarItemTooltip, make simple and "rich" versions

* Add support for dropdown-arrow-only tooltip

* Add toolbar-wide hover timeouts and clears to switch between simple and rich tooltips

* Fix the dropdown arrow button hover styling now that they're separate

* Add missing doc links to rich toolbar tooltips

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

* Re-run CI after snapshots

* fix codespell

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-21 18:32:56 -05:00
2692f2b73a Add units to geometry structs (#5075)
* Make all geometry KclValue variants into struct variants

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

* Add units to geometry types

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-21 20:42:09 +00:00
965cb18059 Parse units on numeric literals and keep them in the AST (#5061)
* Code changes

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

* test changes

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

* Frontend changes

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

* Refactor asNum

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-01-22 08:29:30 +13:00
a022b8ef6c Fix suggestion for updating function decl syntax for anon functions (#5088)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-22 07:10:07 +13:00
907102a8fa Update dumb use of site URL instead of prod app URL 2025-01-21 12:46:45 -05:00
4d24bf7c94 Add API Call ID log for debugging (#5107) 2025-01-20 19:49:02 +00:00
9a537da183 Show toolbar tooltips on hover only, hide when dropdowns are open (#5109)
* Show toolbar tooltips on hover only, hide when dropdowns are open

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

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-18 05:22:22 -05:00
353eca110e Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-17 18:41:44 -05:00
df81b76b8b Bug fix follow-up for create project (#5105)
* fix dumb mistake in command flow for #5083

* Add e2e test for creating projects with the default interpolated name

* Drop that number to 12 ain't got all day

* Why do I have a kcl-samples submodule hanging around?

* Empty commit to remove the submodule
2025-01-17 23:10:28 +00:00
ac3f7ab712 Rust: Remove iai benchmark tests (#5102)
We don't get much value from these, we can always run criterion or valgrind locally.

If we want to measure instruction counts, we should be using codspeed.io instead because
they support visualizing and tracking over time.

If we want to track performance over time we should be using Kevin's perf monitor machine.
2025-01-17 15:42:51 -06:00
dac91d3b79 Add uniqueness check to "Create project" command (#5100)
* Add failing playwright test

* Make create generate a unique name if the given one collides

* Add a new consolidated getUniqueProjectName function with tests

* Use getUniqueProjectName

* Replace "New project" button text with "Create project"
cc @pierremtb

* Extend the e2e test to show the incrementing behavior
cc @lf94
2025-01-17 19:46:52 +00:00
0698432abf Rust artifact graph (#5068)
* Start porting artifact graph creation to Rust

* Add most of artifact graph creation

* Add handling loft command from recent PR

* Refactor artifact merge code so that it errors when a new artifact type is added

* Add sweep subtype

* Finish implementation of build artifact graph

* Fix wasm.ts to use new combined generated ts-rs file

* Fix Rust lints

* Fix lints

* Fix up replacement code

* Add artifact graph to WASM outcome

* Add artifact graph to simulation test output

* Add new artifact graph output snapshots

* Fix wall field and reduce unreachable code

* Change field order for subtype

* Change subtype to be determined from the request, like the TS

* Fix plane sweep_id

* Condense code

* Change ID types to be properly optional

* Change to favor the new ID, the same as TS

* Fix to make error impossible

* Rename artifact type tag values to match TS

* Fix name of field on Cap

* Update outputs

* Change to use Rust source range

* Update output snapshots

* Add conversion to mermaid mind map and add to snapshot tests

* Add new mermaid mind map output

* Add flowchart

* Remove raw artifact graph from tests

* Remove JSON artifact graph output

* Update output file with header

* Update output after adding flowchart

* Fix flowchart to not have duplicate edges, one in each direction

* Fix not not output duplicate edges in flowcharts

* Change flowchart edge style to be more obvious when a direction is missing

* Update output after deduplication of edges

* Fix not not skip sketch-on-face artifacts

* Add docs

* Fix edge iteration order to be stable

* Update output after fixing order

* Port TS artifactGraph.test.ts tests to simulation tests

* Add grouping segments and solid2ds with their path

* Update output flowcharts since grouping paths

* Remove TS artifactGraph tests

* Remove unused d3 dependencies

* Fix to track loft ID on paths

* Add command ID to error messages

* Move artifact graph test code to a separate file since it's a large file

* Reduce function visibility

* Remove TS artifact graph code

* Fix spelling error with serde

* Add TODO for edge cut consumed ID

* Add comment about mermaid edge rank

* Fix mermaid flowchart edge cuts to appear as children of their edges

* Update output since fixing flowchart order

* Fix to always build the artifact graph even when there's a KCL error

* Add artifact graph to error output

* Change optional ID merge to match TS

* Remove redundant SourceRange definition

* Remove Rust-flavored default source range function

* Add helper for source range creation

* Update doc comment for the website

* Update docs after doc comment change

* Fix to save engine responses in execution cache

* Remove unused import

* Fix to not call WASM function before beforeAll callback is run

* Remove more unused imports
2025-01-17 14:34:36 -05:00
fb56820811 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-17 11:25:30 -05:00
fb37bb83a8 Add "Share file" to command palette 2025-01-17 11:24:59 -05:00
f90811695d Refactor: break out copyFileShareLink into standalone function 2025-01-17 11:24:38 -05:00
0592d3b5da Selection dry-run validation for Shell (#4775)
* WIP: mess with shell selection validation
Will eventually fix #4711

* Update from main

* WIP: not working yet

* Working loft dry run validator

* Clean up shell (still not working)

* Bump kittycad-modeling-cmds

* Clean up

* Add logging

* Add proper object_id and face_id mapping, still not working for shell

* Fix faceId

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

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

* Trigger CI

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

* Working validation after engine merge; Clean up

* Fix codespell

* Add pw test

* More clean up

* Back to basics

* 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

* Fix tests

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

* Trigger CI

* Remove kcl-samples

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 10:16:18 -05:00
max
31e4d60045 Refactor Fillet Playwright Test to the new Fixture-based Approach (#4909)
* add locator

* new test

* remove old test

* separation of setup steps

* improved click reliability

* fix ubuntu

* ubuntu fix 2

* ubuntu fix 3

* flaky cmdbar ubuntu

* ubuntu fix second yellow

* ubuntu update

* enable test for windows

* step(Initial test setup)

* extra await, just in case

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

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

* Trigger CI

* screenshot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 10:21:55 +01:00
5c1dfe0c8e Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-16 14:12:20 -05:00
f06873a0e2 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-16 09:52:26 -05:00
09025179f9 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-15 19:34:40 -05:00
521a593451 Bump typescript from 5.7.2 to 5.7.3 (#5021)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.2 to 5.7.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.2...v5.7.3)

---
updated-dependencies:
- dependency-name: typescript
  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-01-15 12:37:22 -05:00
87c4e6c74e custom axis and origin example for helix (#5057)
* custom axis and origin for helix

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

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

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-15 12:37:22 -05:00
82cd106898 Bump @types/node from 20.14.9 to 22.10.6 in /packages/codemirror-lsp-client (#5041) 2025-01-15 12:36:48 -05:00
max
e14cc4ace3 Remove Redundant Fillet Button State Test (#5009)
delete obsolete test
2025-01-15 12:36:48 -05:00
max
2a2a31d0ef Hook up chamfer UI with AST-mod (#4694)
* button

* config

* hook up with ast

* cmd bar test

* button states fix and test

* little naming fix

* xState action to actor

* remove button state test updates

* fixture-based approach

* nightly

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

* Update src/lib/toolbar.ts

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

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-01-15 12:36:48 -05:00
f2669223c5 Bump xstate from 5.17.4 to 5.19.2 (#5027) 2025-01-15 12:36:48 -05:00
c3bc1fad6d ci: Add yarn test of packages/codemirror-lang-kcl (#5035)
* ci: Add yarn test of packages/codemirror-lang-kcl

* Fix CI error running tests

* Fix postcss config error
2025-01-15 12:36:48 -05:00
96ff1dd55b turns on helix from edge (#5036)
* updates for new lib

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

* autocomplete

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

* bump version

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

* bump all the things

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

* new samples

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

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-15 12:36:48 -05:00
82bd04631a Disable auto-updater on non-versioned builds (#5042) 2025-01-15 12:36:48 -05:00
abec2d6d66 Upgrade all wasm-bindgen dependencies together (#5037) 2025-01-15 12:36:48 -05:00
6089b1932a Fix Cargo.lock to not have changes (#5034) 2025-01-15 12:36:48 -05:00
074fd2b5c7 Fix artifact types to be more accurate (#5022) 2025-01-15 12:36:48 -05:00
b2485b804c Bump syn from 2.0.95 to 2.0.96 in /src/wasm-lib (#5015)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.95 to 2.0.96.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.95...2.0.96)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  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-01-15 12:36:48 -05:00
e753082653 Bump handlebars from 6.2.0 to 6.3.0 in /src/wasm-lib (#5012)
Bumps [handlebars](https://github.com/sunng87/handlebars-rust) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.2.0...v6.3.0)

---
updated-dependencies:
- dependency-name: handlebars
  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-01-15 12:36:48 -05:00
634745bb81 Bump @lezer/generator from 1.7.1 to 1.7.2 (#5018)
Bumps [@lezer/generator](https://github.com/lezer-parser/generator) from 1.7.1 to 1.7.2.
- [Changelog](https://github.com/lezer-parser/generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lezer-parser/generator/compare/1.7.1...1.7.2)

---
updated-dependencies:
- dependency-name: "@lezer/generator"
  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-01-15 12:36:48 -05:00
e3660c75fc Add packages to Dependabot updates (#5024) 2025-01-15 12:36:48 -05:00
ef61d10615 Change Dependabot PRs to always be made on Mondays (#5025) 2025-01-15 12:36:48 -05:00
c208e16c76 Developer workflow: added auto generated workspace file from vitest extension in vscode (#4997)
* chore: added auto generated workspace file from vitest extension in vscode

* fix: auto fmt fixes
2025-01-15 12:36:48 -05:00
585ca7e80f Fix lost lints and add new ones (#5011)
* Add eslint-plugin-jsx-a11y dependency

* Add jsx-a11y lint

* Add eslint-plugin-react-hooks dependency

* Add react hooks lints

* Ignore new react hooks lint in tests

* Add eslint-plugin-testing-library dependency

* Add testing-library lint

* Fix yarn lint to use all files recursively
2025-01-15 12:36:48 -05:00
f7bae1d221 Upgrade typescript-eslint from 5.62.0 to 8.19.1 and remove eslint-config-react-app (#5006) 2025-01-15 12:36:48 -05:00
339de00e68 Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

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

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

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

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

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

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

* Trigger CI

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

* Trigger CI

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

* Trigger CI

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

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

* WIP: sweep point-and-click

* Working sweep

* Add test

* Make sweep a development command

* Fix tsc error

* Clean up for review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-15 12:36:47 -05:00
4f02e45da3 Add new lint to disallow use of confusing isNaN (#4999) 2025-01-15 12:36:47 -05:00
1908383f0e Fix so that all artifact commands are returned regardless of caching (#5005)
* Fix so that all artifact commands are returned regardless of caching

* Add some more docs and fix up old ones
2025-01-15 12:36:47 -05:00
68204bb23d Make the test executor a bit more patient (#5004) 2025-01-15 12:36:47 -05:00
5438a987ab Fix browser command flow, because we had made the projectMachine desktop-only on main 2025-01-15 11:35:59 -05:00
fa3f934948 Clean up unneeded PROD_TOKEN 2025-01-14 17:23:39 -05:00
08e714080e Use dev urls everywhere when configured that way
I think we were just using some constants that ended up returning bad
values for dev, it seemed to return a working shortlink when I went
through the flow.
2025-01-14 17:16:16 -05:00
df01c233e4 Fix unit test, use kebab-case for url query param 2025-01-13 10:42:08 -05:00
b30a37a0b3 Fix broken rename and delete project commands
Something about the `optionsFromContext` config no longer works with file I/O-related commands. I suspect this has to do with our read/write loop patching
2025-01-13 10:34:38 -05:00
82aefec34d Fix unit test 2025-01-10 18:04:33 -05:00
679b65f643 Lints, fmt, tsc 2025-01-10 17:04:09 -05:00
d64270d494 Undo mistaken or unecessary changes 2025-01-10 17:00:18 -05:00
c06b2b4029 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-10 16:50:12 -05:00
8b8a2bc4e2 Add E2E test for "add to existing project" user flow 2025-01-10 16:22:28 -05:00
af702ae1b2 Fix the "existing project" user flow 2025-01-10 16:11:15 -05:00
83e72dafa3 Add a couple component tests for OpenInDesktopAppHandler 2025-01-10 15:55:43 -05:00
e417e60053 Add E2E test for importing file from URL 2025-01-10 15:08:56 -05:00
ebc6b6460d Separate creating createFileUrl and shortlink so it is unit testable 2025-01-10 12:57:16 -05:00
91f0cfe467 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-10 11:32:36 -05:00
a2ff0aeceb Clean up unecessary file 2025-01-10 11:18:14 -05:00
f05acf92cc Styling updates to OpenInDesktopAppHandler 2025-01-10 10:36:05 -05:00
670faac1e8 Rework to open browser app first, then send along to the desktop app if asked 2025-01-09 17:58:12 -05:00
ca09224c92 Get primary user flow working on desktop 2025-01-08 16:56:48 -05:00
5cbd11cec8 Add useCreateFileLinkQuery on Home page 2025-01-08 12:15:59 -05:00
28eb99f655 Merge branch 'main' into franknoirot/4088/create-file-url 2025-01-08 12:00:19 -05:00
c29be6e341 Everything's pretty much done but url.zoo.dev has been broken and we need to think about how to test reliably 2024-10-19 00:12:58 -04:00
2193d563c5 wip 2024-10-17 19:03:38 -04:00
570d159c29 wip 2024-10-17 18:56:49 -04:00
713886b274 rerun CI 2024-10-17 09:25:44 -07:00
2aa4a01cb7 Merge branch 'franknoirot/4088/decouple-homeMachine' into franknoirot/4088/create-file-url 2024-10-16 18:56:53 -04:00
2048c26b9f Tests always run on localhost, don't expect the prod origin 2024-10-16 15:56:17 -07:00
cbb8df5904 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-15 20:16:54 +00:00
bb67a9e9cf Merge branch 'franknoirot/4088/decouple-homeMachine' into franknoirot/4088/create-file-url 2024-10-15 13:07:29 -07:00
b84d5951b7 Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-15 13:07:00 -07:00
1e5954e5ed Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-10 20:50:48 -04:00
d58a147b7d Get query-triggered command working in browser too 2024-10-10 18:55:31 -04:00
96b06247a4 Side quest: Only register commands once, power their disabled status while selecting commands via optional actor 2024-10-10 18:54:53 -04:00
36d49b1bcb A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-10 20:23:44 +00:00
4748c2d1e0 Ahhh more flaky toasts, they're everywhere! 2024-10-10 16:19:58 -04:00
698ce671df Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-10 15:48:56 -04:00
a2330a0dbc A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-10 19:38:47 +00:00
c882e34ea9 Merge branch 'franknoirot/4088/decouple-homeMachine' into franknoirot/4088/create-file-url 2024-10-10 15:36:32 -04:00
1ce3d8ccd0 Dumb test error because I was rushing 2024-10-10 15:34:07 -04:00
15bedd56f4 FMT 2024-10-10 15:04:25 -04:00
746ebf80d1 De-flake another text that could be thrown off by toast-based selectors 2024-10-10 14:57:25 -04:00
02b249bd31 Merge branch 'main' into franknoirot/update-download-progress 2024-10-10 12:20:48 -04:00
524fcb03ad Merge branch 'franknoirot/4088/decouple-homeMachine' into franknoirot/4088/create-file-url 2024-10-08 15:32:06 -04:00
3a9e0c72a8 Fix a couple stray tests that still relied on the old way of creating projects 2024-10-08 15:31:49 -04:00
5dc983ad7b Add (broken) event logic and command triggering logic 2024-10-08 14:52:38 -04:00
81411033d7 Forward query params while redirecting to /home or /file 2024-10-08 12:36:48 -04:00
30a24c8ae6 Add menu item to share link to file 2024-10-08 12:32:47 -04:00
403cee5f16 Fix tsc 2024-10-08 10:42:42 -04:00
14eeafb70a Fix lint 2024-10-08 10:38:25 -04:00
f4ecd16ffa Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-08 10:31:13 -04:00
48380be480 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-08 14:11:34 +00:00
80e32b337f Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-08 10:07:50 -04:00
9378d9862b A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-08 13:27:43 +00:00
1f515b712b A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-08 13:22:12 +00:00
372f2eebcc Add a mask to the state indicator to client-side scale test 2024-10-08 09:18:24 -04:00
e22a9edde8 Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"
This reverts commit 3d2e48732c.
2024-10-08 09:17:45 -04:00
75e3f843eb Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"
This reverts commit 7545b61b49.
2024-10-08 09:17:21 -04:00
f0136a5939 Fix tests that relied on one-click, no-navigation project creation 2024-10-08 09:15:18 -04:00
3d2e48732c A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-07 19:23:02 +00:00
7545b61b49 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-10-07 19:21:28 +00:00
d1be6d7b64 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-10-07 19:18:03 +00:00
8ab24ceee7 @jtran feedback, use the type guard util 2024-10-07 15:13:52 -04:00
f163870b86 Fix tsc 2024-10-07 11:08:36 -04:00
3fc707a2a4 Remove console logs 2024-10-07 11:07:09 -04:00
238163d7db Tests second version: flattened 2024-10-07 11:06:25 -04:00
bfccb79c1c Tests first version: nested loops 2024-10-07 10:56:00 -04:00
fe6d1f8119 Make projects watching code not run in web 2024-10-07 09:13:24 -04:00
f496d94258 Merge branch 'main' into franknoirot/4088/decouple-homeMachine 2024-10-07 09:01:27 -04:00
5d8f3f988a More explicit warning message text 2024-10-04 17:15:54 -04:00
4f06524776 Update "New project" button to use command bar flow
Closes #2585
2024-10-04 17:07:41 -04:00
d7fe827a9e Make it navigate when you create a project 2024-10-04 17:03:23 -04:00
049e487ac4 Show a warning in the command palette for deleting a project 2024-10-04 16:59:09 -04:00
5bd89047b2 Add logic to navigate out from deleted or renamed project 2024-10-04 16:51:06 -04:00
5822321f35 Separate out /home route from projectsMachine 2024-10-04 16:18:16 -04:00
401dcf8152 Rename homeMachine and accessories to projectsMachine 2024-10-04 16:16:09 -04:00
595 changed files with 145881 additions and 70083 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo

View File

@ -5,6 +5,7 @@ on:
push:
branches:
- main
- pierremtb/4088/create-file-url
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
schedule:

View File

@ -1,44 +0,0 @@
on:
push:
branches:
- main
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo bench
jobs:
cargo-bench:
name: Benchmark with iai
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

File diff suppressed because it is too large Load Diff

28
docs/kcl/types/Face.md Normal file
View File

@ -0,0 +1,28 @@
---
title: "Face"
excerpt: "A face."
layout: manual
---
A face.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces 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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -20,6 +20,7 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -20,6 +20,7 @@ A helix.
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -168,7 +168,6 @@ Any KCL value.
----
A plane.
**Type:** `object`
@ -181,17 +180,10 @@ A plane.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | 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 |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
----
A face.
**Type:** `object`
@ -203,14 +195,8 @@ A face.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Face`| | No |
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces 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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
----
@ -246,7 +232,6 @@ A face.
----
An solid is a collection of extrude surfaces.
**Type:** `object`
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
| `id` |`string`| The id of the solid. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |`number`| The height of the solid. | No |
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
----
@ -286,7 +264,6 @@ An solid is a collection of extrude surfaces.
----
A helix.
**Type:** `object`
@ -299,11 +276,7 @@ A helix.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
----

View File

@ -22,6 +22,7 @@ A plane.
| `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 |
| `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

@ -21,6 +21,7 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | 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

@ -30,6 +30,7 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | 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

@ -31,6 +31,7 @@ A plane.
| `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 |
| `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 |
@ -54,6 +55,7 @@ A face.
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces 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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -23,6 +23,7 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -32,6 +32,7 @@ An solid is a collection of extrude surfaces.
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

107
docs/kcl/types/UnitLen.md Normal file
View File

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

View File

@ -280,7 +280,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
})
await test.step('Opening the router-template project should load', async () => {

View File

@ -1,7 +1,8 @@
import { test, expect } from './zoo-test'
import { getUtils } from './test-utils'
import * as fsp from 'fs/promises'
import { executorInputPath, getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path from 'path'
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
@ -45,46 +46,6 @@ test.describe('Command bar tests', () => {
)
})
// TODO: fix this test after the electron migration
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // skip selection
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accept default radius
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // submit
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
homePage,
@ -345,4 +306,132 @@ test.describe('Command bar tests', () => {
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
test(`Reacts to query param to open "import from URL" command`, async ({
page,
cmdBar,
editor,
homePage,
}) => {
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'New Project' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
})
})
test(`"import from URL" can add to existing project`, async ({
page,
cmdBar,
editor,
homePage,
toolbar,
context,
}) => {
await context.folderSetupFn(async (dir) => {
const testProjectDir = path.join(dir, 'testProjectDir')
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('cylinder.kcl'),
path.join(testProjectDir, 'main.kcl')
),
])
})
await test.step(`Prepare and navigate to home page with query params`, async () => {
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
await homePage.expectState({
projectCards: [
{
fileCount: 1,
title: 'testProjectDir',
},
],
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'method',
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
})
await cmdBar.selectOption({ name: 'Existing Project' }).click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Import file from URL',
currentArgKey: 'projectName',
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'test',
ProjectName: '',
Code: '1 line',
},
highlightedHeaderArg: 'projectName',
})
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
await cmdBar.expectState({
stage: 'review',
commandName: 'Import file from URL',
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'test',
Code: '1 line',
},
})
await cmdBar.progressCmdBar()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
})
})
})

View File

@ -135,4 +135,27 @@ export class CmdBarFixture {
await promptEditCommand.first().click()
}
}
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
get argumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
chooseCommand = async (commandName: string) => {
await this.cmdOptions.getByText(commandName).click()
}
/**
* Select an option from the command bar
*/
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
return this.page.getByRole('option', options)
}
}

View File

@ -103,7 +103,7 @@ export class HomePageFixture {
.toEqual(expectedState)
}
createAndGoToProject = async (projectTitle: string) => {
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click()
await this.projectTextName.click()

View File

@ -15,6 +15,7 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
filletButton!: Locator
chamferButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
@ -43,6 +44,7 @@ export class ToolbarFixture {
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.filletButton = page.getByTestId('fillet3d')
this.chamferButton = page.getByTestId('chamfer3d')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
@ -61,6 +63,10 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)

View File

@ -1020,6 +1020,221 @@ sketch002 = startSketchOn('XZ')
})
})
test(`Fillet point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-12, sketch001)
`
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondFilletDeclaration =
'fillet({ radius = 5, tags = [getOppositeEdge(seg01)] }, %)'
// Locators
const firstEdgeLocation = { x: 600, y: 193 }
const secondEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
const [clickOnFirstEdge] = scene.makeMouseHelpers(
firstEdgeLocation.x,
firstEdgeLocation.y
)
const [clickOnSecondEdge] = scene.makeMouseHelpers(
secondEdgeLocation.x,
secondEdgeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// 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()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test 1: Command bar flow with preselected edges
await test.step(`Select first edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
firstEdgeLocation,
lowTolerance
)
await clickOnFirstEdge()
await scene.expectPixelColor(
edgeColorYellow,
firstEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 face',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(firstFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance)
})
// Test 2: Command bar flow without preselected edges
await test.step(`Open fillet UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Fillet',
})
})
await test.step(`Select second edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
secondEdgeLocation,
lowTolerance
)
await clickOnSecondEdge()
await scene.expectPixelColor(
edgeColorYellow,
secondEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the second edge`, async () => {
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['radius=5,'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
})
})
test(`Chamfer point-and-click`, async ({
context,
page,
@ -1029,9 +1244,6 @@ test(`Chamfer point-and-click`, async ({
toolbar,
cmdBar,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
@ -1069,13 +1281,13 @@ extrude001 = extrude(-12, sketch001)
const highTolerance = 40
// Setup
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
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()
await test.step(`Verify scene is loaded`, async () => {
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
@ -1103,6 +1315,7 @@ extrude001 = extrude(-12, sketch001)
})
await test.step(`Apply chamfer to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
commandName: 'Chamfer',
@ -1154,6 +1367,7 @@ extrude001 = extrude(-12, sketch001)
// Test 2: Command bar flow without preselected edges
await test.step(`Open chamfer UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
stage: 'arguments',
@ -1289,6 +1503,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1309,6 +1524,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1390,6 +1606,7 @@ extrude001 = extrude(40, sketch001)
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1513,3 +1730,61 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
})
})
})
test(`Shell dry-run validation rejects sweeps`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 500, y: 250 }
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
await test.step(`Confirm sweep exists`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
})
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnSweep()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the provided selection')
).toBeVisible()
await page.waitForTimeout(1000)
})
})

View File

@ -172,7 +172,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
@ -985,6 +985,126 @@ test.describe(`Project management commands`, () => {
})
}
)
test(`Create a new project with a colliding name`, async ({
context,
homePage,
toolbar,
cmdBar,
}) => {
const projectName = 'test-project'
await test.step(`Setup`, async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
path.join(projectDir, 'main.kcl')
),
])
})
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create a new project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create another project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-2',
fileCount: 1,
},
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})
test(`Create a few projects using the default project name`, async ({
homePage,
toolbar,
}) => {
for (let i = 0; i < 12; i++) {
await test.step(`Create project ${i}`, async () => {
await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({
title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1,
})).toReversed(),
sortBy: 'last-modified-desc',
})
await homePage.createAndGoToProject()
await toolbar.logoLink.click()
})
}
})
test(
@ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click()
await expect(
page.getByRole('button', { name: 'New project' })
page.getByRole('button', { name: 'Create project' })
).toBeVisible()
for (let i = 1; i <= 10; i++) {
@ -1465,7 +1585,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
})
await test.step('Opening the router-template project should load the stream', async () => {
@ -1494,7 +1614,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
await expect(page.getByText('Create project')).toBeVisible()
})
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1078,7 +1078,7 @@ export async function createProject({
returnHome?: boolean
}) {
await test.step(`Create project and navigate to it`, async () => {
await page.getByRole('button', { name: 'New project' }).click()
await page.getByRole('button', { name: 'Create project' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'Continue' }).click()

View File

@ -154,7 +154,6 @@
"@playwright/test": "^1.49.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/diff": "^6.0.0",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
@ -175,7 +174,6 @@
"@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "32.1.2",
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",

View File

@ -22,6 +22,8 @@ import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { maybeWriteToDisk } from 'lib/telemetry'
maybeWriteToDisk()
.then(() => {})
@ -29,6 +31,20 @@ maybeWriteToDisk()
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
const { commandBarSend } = useCommandsContext()
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()

View File

@ -35,7 +35,7 @@ import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -47,6 +47,7 @@ import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -58,33 +59,44 @@ const router = createRouter([
/* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */
element: (
<CommandBarProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
</CommandBarProvider>
<OpenInDesktopAppHandler>
<CommandBarProvider>
<RouteProvider>
<SettingsAuthProvider>
<LspProvider>
<ProjectsContextProvider>
<KclContextProvider>
<AppStateProvider>
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</ProjectsContextProvider>
</LspProvider>
</SettingsAuthProvider>
</RouteProvider>
</CommandBarProvider>
</OpenInDesktopAppHandler>
),
errorElement: <ErrorPage />,
children: [
{
path: PATHS.INDEX,
loader: async () => {
loader: async ({ request }) => {
const onDesktop = isDesktop()
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
const url = new URL(request.url)
if (onDesktop) {
return redirect(PATHS.HOME + (url.search || ''))
} else {
const searchParams = new URLSearchParams(url.search)
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
return redirect(
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
}
}
return null
},
},
{

View File

@ -1,4 +1,4 @@
import { useRef, useMemo, memo } from 'react'
import { useRef, useMemo, memo, useCallback, useState } from 'react'
import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
@ -34,8 +34,7 @@ export function Toolbar({
const bgClassName = '!bg-transparent'
const buttonBgClassName =
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
const buttonBorderClassName =
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const buttonBorderClassName = '!border-transparent'
const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
@ -50,6 +49,7 @@ export function Toolbar({
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const [showRichContent, setShowRichContent] = useState(false)
const disableAllButtons =
(overallState !== NetworkHealthState.Ok &&
@ -77,6 +77,40 @@ export function Toolbar({
[state, send, commandBarSend, sketchPathId]
)
const tooltipContentClassName = !showRichContent
? ''
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
const richContentTimeout = useRef<number | null>(null)
const richContentClearTimeout = useRef<number | null>(null)
// On mouse enter, show rich content after a 1s delay
const handleMouseEnter = useCallback(() => {
// Cancel the clear timeout if it's already set
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
// Start our own timeout to show the rich content
richContentTimeout.current = window.setTimeout(() => {
setShowRichContent(true)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 1000)
}, [setShowRichContent])
// On mouse leave, clear the timeout and hide rich content
const handleMouseLeave = useCallback(() => {
// Clear the timeout to show rich content
if (richContentTimeout.current) {
clearTimeout(richContentTimeout.current)
}
// Start a timeout to hide the rich content
richContentClearTimeout.current = window.setTimeout(() => {
setShowRichContent(false)
if (richContentClearTimeout.current) {
clearTimeout(richContentClearTimeout.current)
}
}, 500)
}, [setShowRichContent])
/**
* Resolve all the callbacks and values for the current mode,
* so we don't need to worry about the other modes
@ -174,43 +208,64 @@ export function Toolbar({
status: itemConfig.status,
}))}
>
<ActionButton
Element="button"
id={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id}
iconStart={{
icon: maybeIconConfig[0].icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBgClassName
}
aria-pressed={maybeIconConfig[0].isActive}
disabled={
disableAllButtons ||
maybeIconConfig[0].status !== 'available' ||
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
}
<div
className="contents"
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
<ActionButton
Element="button"
id={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id}
iconStart={{
icon: maybeIconConfig[0].icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBgClassName
}
aria-pressed={maybeIconConfig[0].isActive}
disabled={
disableAllButtons ||
maybeIconConfig[0].status !== 'available' ||
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
}
>
{maybeIconConfig[0].title}
</span>
</ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
>
{maybeIconConfig[0].title}
</span>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
wrapperClassName="ui-open:!hidden"
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent
itemConfig={maybeIconConfig[0]}
/>
) : (
<ToolbarItemTooltipShortContent
status={maybeIconConfig[0].status}
title={maybeIconConfig[0].title}
hotkey={maybeIconConfig[0].hotkey}
/>
)}
</ToolbarItemTooltip>
</ActionButton>
</div>
</ActionButtonDropdown>
)
}
@ -218,7 +273,13 @@ export function Toolbar({
// A single button
return (
<div className="relative" key={itemConfig.id}>
<div
className="relative"
key={itemConfig.id}
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<ActionButton
Element="button"
key={itemConfig.id}
@ -255,7 +316,18 @@ export function Toolbar({
<ToolbarItemTooltip
itemConfig={itemConfig}
configCallbackProps={configCallbackProps}
/>
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
) : (
<ToolbarItemTooltipShortContent
status={itemConfig.status}
title={itemConfig.title}
hotkey={itemConfig.hotkey}
/>
)}
</ToolbarItemTooltip>
</div>
)
})}
@ -269,6 +341,12 @@ export function Toolbar({
)
}
interface ToolbarItemContentsProps extends React.PropsWithChildren {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
wrapperClassName?: string
contentClassName?: string
}
/**
* The single button and dropdown button share content, so we extract it here
* It contains a tooltip with the title, description, and links
@ -277,12 +355,10 @@ export function Toolbar({
const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
}: {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
}) {
const { state } = useModelingContext()
wrapperClassName = '',
contentClassName = '',
children,
}: ToolbarItemContentsProps) {
useHotkeys(
itemConfig.hotkey || '',
() => {
@ -305,11 +381,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
hoverOnly
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
contentClassName={contentClassName}
delay={0}
>
{children}
</Tooltip>
)
})
const ToolbarItemTooltipShortContent = ({
status,
title,
hotkey,
}: {
status: string
title: string
hotkey?: string | string[]
}) => (
<span
className={`text-sm ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
}`}
>
{title}
{hotkey && (
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
)}
</span>
)
const ToolbarItemTooltipRichContent = ({
itemConfig,
}: {
itemConfig: ToolbarItemResolved
}) => {
const { state } = useModelingContext()
return (
<>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
{itemConfig.icon && (
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
)}
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
@ -378,6 +493,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
</ul>
</>
)}
</Tooltip>
</>
)
})
}

View File

@ -25,13 +25,13 @@ import {
CallExpression,
PathToNode,
Program,
SourceRange,
Expr,
parse,
recast,
defaultSourceRange,
resultIsOk,
ProgramMemory,
topLevelRange,
} from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes'
@ -600,8 +600,8 @@ const ConstraintSymbol = ({
if (err(_node)) return
const node = _node.node
const range: SourceRange = node
? [node.start, node.end, true]
const range = node
? topLevelRange(node.start, node.end)
: defaultSourceRange()
if (_type === 'intersectionTag') return null

View File

@ -59,6 +59,7 @@ import {
sourceRangeFromRust,
resultIsOk,
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
@ -628,7 +629,7 @@ export class SceneEntities {
const startRange = _node1.node.start
const endRange = _node1.node.end
const sourceRange: SourceRange = [startRange, endRange, true]
const sourceRange = topLevelRange(startRange, endRange)
const selection: Selections = computeSelectionFromSourceRangeAndAST(
sourceRange,
maybeModdedAst
@ -1397,23 +1398,23 @@ export class SceneEntities {
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].value = { value: points[0].x, suffix: 'None' }
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
arg0[1].value = { value: points[0].y, suffix: 'None' }
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = points[1].x
arg1[0].value = { value: points[1].x, suffix: 'None' }
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
arg1[1].value = { value: points[1].y, suffix: 'None' }
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = points[2].x
arg2[0].value = { value: points[2].x, suffix: 'None' }
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].value = { value: points[2].y, suffix: 'None' }
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
@ -2012,7 +2013,7 @@ export class SceneEntities {
kclManager.programMemory,
{
type: 'sourceRange',
sourceRange: [node.start, node.end, true],
sourceRange: topLevelRange(node.start, node.end),
},
getChangeSketchInput()
)
@ -2050,8 +2051,8 @@ export class SceneEntities {
)
if (!(sk instanceof Reason)) {
sketch = sk
} else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch as Solid).sketch
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
sketch = (maybeSketch.value as Solid).sketch
}
if (!sketch) return
@ -2263,7 +2264,7 @@ export class SceneEntities {
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([[node.start, node.end, true]])
editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -2540,7 +2541,7 @@ export function sketchFromPathToNode({
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') {
return result.sketch
return result.value.sketch
}
const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) {

View File

@ -1,9 +1,11 @@
import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon'
import Tooltip from './Tooltip'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string
dropdownTooltipText?: string
splitMenuItems: {
id: string
label: string
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({
splitMenuItems,
className,
dropdownTooltipText = 'More tools',
children,
...props
}: ActionButtonSplitProps) {
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
{({ close }) => (
<>
{children}
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
<Popover.Button
className={
'!border-transparent dark:!border-transparent ' +
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
>
<CustomIcon
name="caretDown"
className={
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
<span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu
</span>
<Tooltip
delay={0}
position="bottom"
hoverOnly
wrapperClassName="ui-open:!hidden"
>
{dropdownTooltipText}
</Tooltip>
</Popover.Button>
<Popover.Panel
as="ul"

View File

@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
export function AstExplorer() {
const { context } = useModelingContext()
@ -118,19 +118,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
}}
onClick={(e) => {
const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const range = topLevelRange(obj?.start || 0, obj.end || 0)
const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) },
])[0]

View File

@ -129,11 +129,13 @@ function CommandArgOptionInput({
<label
htmlFor="option-input"
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
data-testid="cmd-bar-arg-name"
>
{argName}
</label>
<Combobox.Input
id="option-input"
data-testid="cmd-bar-arg-value"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)

View File

@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2D'],
face: ['wall', 'cap', 'solid2d'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
plane: ['defaultPlane'],

View File

@ -52,6 +52,7 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/>
<Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => {
@ -85,6 +86,7 @@ function CommandComboBox({
value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />

View File

@ -1,10 +1,7 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
import { ArtifactGraph } from 'lang/wasm'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() {

View File

@ -48,8 +48,9 @@ export const FileMachineProvider = ({
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { settings, auth } = useSettingsAuthContext()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[]
)
@ -296,40 +297,47 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands(
async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
kclCommands({
authToken: auth?.context?.token ?? '',
projectData,
settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
},
kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title,
}))
).filter(
specialPropsForSampleCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
// Either way, we want to overwrite the defaultUnit project setting
// with the sample's setting.
if (data.sampleUnits) {
settings.send({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: data.sampleUnits,
},
})
}
},
providedOptions: kclSamples.map((sample) => ({
value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title,
})),
},
}).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
),
[codeManager, kclManager, send, kclSamples]

View File

@ -95,9 +95,11 @@ export const processMemory = (programMemory: ProgramMemory) => {
) {
const sk = sketchFromKclValueOptional(val, key)
if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
processedMemory[key] = val.value.value.map(
({ ...rest }: ExtrudeSurface) => {
return rest
}
)
} else if (!(sk instanceof Reason)) {
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest

View File

@ -0,0 +1,68 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { OpenInDesktopAppHandler } from './OpenInDesktopAppHandler'
/**
* The behavior under test requires a router,
* so we wrap the component in a minimal router setup.
*/
function TestingMinimalRouterWrapper({
children,
location,
}: {
location?: string
children: React.ReactNode
}) {
return (
<Routes location={location}>
<Route
path="/"
element={<OpenInDesktopAppHandler>{children}</OpenInDesktopAppHandler>}
/>
</Routes>
)
}
describe('OpenInDesktopAppHandler tests', () => {
test(`does not render the modal if no query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper>
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
const dummyAppContents = screen.getByText('Dummy app contents')
const modalContents = screen.queryByText('Open in desktop app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalContents).not.toBeInTheDocument()
})
test(`renders the modal if the query param is present`, () => {
render(
<BrowserRouter>
<TestingMinimalRouterWrapper location="/?ask-open-desktop">
<p>Dummy app contents</p>
</TestingMinimalRouterWrapper>
</BrowserRouter>
)
let dummyAppContents = screen.queryByText('Dummy app contents')
let modalButton = screen.queryByText('Continue to web app')
// Starts as disconnected
expect(dummyAppContents).not.toBeInTheDocument()
expect(modalButton).not.toBeFalsy()
expect(modalButton).toBeInTheDocument()
fireEvent.click(modalButton as Element)
// I don't like that you have to re-query the screen here
dummyAppContents = screen.queryByText('Dummy app contents')
modalButton = screen.queryByText('Continue to web app')
expect(dummyAppContents).toBeInTheDocument()
expect(modalButton).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,125 @@
import { getSystemTheme, Themes } from 'lib/theme'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import { isDesktop } from 'lib/isDesktop'
import { useSearchParams } from 'react-router-dom'
import { ASK_TO_OPEN_QUERY_PARAM } from 'lib/constants'
import { VITE_KC_SITE_BASE_URL } from 'env'
import { ActionButton } from './ActionButton'
import { Transition } from '@headlessui/react'
/**
* This component is a handler that checks if a certain query parameter
* is present, and if so, it will show a modal asking the user if they
* want to open the current page in the desktop app.
*/
export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
const theme = getSystemTheme()
const buttonClasses =
'bg-transparent flex-0 hover:bg-primary/10 dark:hover:bg-primary/10'
const pathLogomarkSvg = `${isDesktop() ? '.' : ''}/zma-logomark${
theme === Themes.Light ? '-dark' : ''
}.svg`
const [searchParams, setSearchParams] = useSearchParams()
// We also ignore this param on desktop, as it is redundant
const hasAskToOpenParam =
!isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
/**
* This function removes the query param to ask to open in desktop app
* and then navigates to the same route but with our custom protocol
* `zoo-studio:` instead of `https://${BASE_URL}`, to trigger the user's
* desktop app to open.
*/
function onOpenInDesktopApp() {
const newSearchParams = new URLSearchParams(globalThis.location.search)
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
'/',
''
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
globalThis.location.href = newURL
}
/**
* Just remove the query param to ask to open in desktop app
* and continue to the web app.
*/
function continueToWebApp() {
searchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
setSearchParams(searchParams)
}
return hasAskToOpenParam ? (
<Transition
appear
show={true}
as="div"
className={
theme +
` fixed inset-0 grid p-4 place-content-center ${
theme === Themes.Dark ? '!bg-chalkboard-110 text-chalkboard-20' : ''
}`
}
>
<Transition.Child
as="div"
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-8
mx-auto border rounded-lg shadow-lg dark:bg-chalkboard-100`}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
style={{ zIndex: 10 }}
>
<div>
<h1 className="text-2xl">
Launching{' '}
<img
src={pathLogomarkSvg}
className="w-48"
alt="Zoo Modeling App"
/>
</h1>
</div>
<p className="text-primary flex items-center gap-2">
Choose where to open this link...
</p>
<div className="flex flex-col md:flex-row items-start justify-between gap-4 xl:gap-8">
<div className="flex flex-col gap-2">
<ActionButton
Element="button"
className={buttonClasses + ' !text-base'}
onClick={onOpenInDesktopApp}
iconEnd={{ icon: 'arrowRight' }}
>
Open in desktop app
</ActionButton>
<ActionButton
Element="externalLink"
className={
buttonClasses +
' text-sm border-transparent justify-center dark:bg-transparent'
}
to={`${VITE_KC_SITE_BASE_URL}/modeling-app/download`}
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
>
Download desktop app
</ActionButton>
</div>
<ActionButton
Element="button"
className={buttonClasses + ' -order-1 !text-base'}
onClick={continueToWebApp}
iconStart={{ icon: 'arrowLeft' }}
>
Continue to web app
</ActionButton>
</div>
</Transition.Child>
</Transition>
) : (
props.children
)
}

View File

@ -10,11 +10,13 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
const ProjectSidebarMenu = ({
project,
@ -95,6 +97,7 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext()
const machineManager = useContext(MachineManagerContext)
const { commandBarState, commandBarSend } = useCommandsContext()
@ -155,7 +158,6 @@ function ProjectMenuPopover({
data: exportCommandInfo,
}),
},
'break',
{
id: 'make',
Element: 'button',
@ -181,6 +183,19 @@ function ProjectMenuPopover({
})
},
},
{
id: 'share-link',
Element: 'button',
children: 'Share link to file',
onClick: async () => {
await copyFileShareLink({
token: auth?.context.token || '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
},
},
'break',
{
id: 'go-home',

View File

@ -3,11 +3,11 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine'
import { createContext, useEffect, useState } from 'react'
import { createContext, useCallback, useEffect, useState } from 'react'
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import {
createNewProjectDirectory,
@ -18,11 +18,28 @@ import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
getUniqueProjectName,
getNextFileName,
} from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop'
import {
CREATE_FILE_URL_PARAM,
FILE_EXT,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T>
@ -52,12 +69,110 @@ export const ProjectsContextProvider = ({
)
}
/**
* We need some of the functionality of the ProjectsContextProvider in the web version
* but we can't perform file system operations in the browser,
* so most of the behavior of this machine is stubbed out.
*/
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const {
settings: { context: settings, send: settingsSend },
} = useSettingsAuthContext()
const [state, send, actor] = useMachine(
projectsMachine.provide({
actions: {
navigateToProject: () => {},
navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
'message' in event.output &&
typeof event.output.message === 'string' &&
event.output.message) ||
''
),
toastError: ({ event }) =>
toast.error(
('data' in event && typeof event.data === 'string' && event.data) ||
('output' in event &&
typeof event.output === 'string' &&
event.output) ||
''
),
},
actors: {
readProjects: fromPromise(async () => [] as Project[]),
createProject: fromPromise(async () => ({
message: 'not implemented on web',
})),
renameProject: fromPromise(async () => ({
message: 'not implemented on web',
oldName: '',
newName: '',
})),
deleteProject: fromPromise(async () => ({
message: 'not implemented on web',
name: '',
})),
createFile: fromPromise(async ({ input }) => {
// Browser version doesn't navigate, just overwrites the current file
clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile()
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
return {
message: 'File and units overwritten successfully',
fileName: input.name,
projectName: '',
}
}),
},
}),
{
input: {
projects: [],
defaultProjectName: settings.projects.defaultProjectName.current,
defaultDirectory: settings.app.projectDirectory.current,
},
}
)
// register all project-related command palette commands
useStateMachineCommands({
machineId: 'projects',
send,
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return (
<ProjectsMachineContext.Provider
value={{
state: undefined,
send: () => {},
state,
send,
}}
>
{children}
@ -72,19 +187,22 @@ const ProjectsContextDesktop = ({
}) => {
const navigate = useNavigate()
const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams()
const clearImportSearchParams = useCallback(() => {
// Clear the search parameters related to the "Import file from URL" command
// or we'll never be able cancel or submit it.
searchParams.delete(CREATE_FILE_URL_PARAM)
searchParams.delete('code')
searchParams.delete('name')
searchParams.delete('units')
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const { commandBarSend } = useCommandsContext()
const { onProjectOpen } = useLspContext()
const {
settings: { context: settings },
} = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger,
@ -168,6 +286,31 @@ const ProjectsContextDesktop = ({
}
}
},
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-file') return
// For now, the browser version of create-file doesn't need to navigate
// since it just overwrites the current file.
if (!isDesktop()) return
let projectPath = window.electron.join(
context.defaultDirectory,
event.output.projectName
)
let filePath = window.electron.join(
projectPath,
event.output.fileName
)
onProjectOpen(
{
name: event.output.projectName,
path: projectPath,
},
null
)
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
filePath
)}`
navigate(pathToNavigateTo)
},
toastSuccess: ({ event }) =>
toast.success(
('data' in event && typeof event.data === 'string' && event.data) ||
@ -195,16 +338,12 @@ const ProjectsContextDesktop = ({
: settings.projects.defaultProjectName.current
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = getNextProjectIndex(name, input.projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
const uniqueName = getUniqueProjectName(name, input.projects)
await createNewProjectDirectory(uniqueName)
return {
message: `Successfully created "${name}"`,
name,
message: `Successfully created "${uniqueName}"`,
name: uniqueName,
}
}),
renameProject: fromPromise(async ({ input }) => {
@ -221,8 +360,6 @@ const ProjectsContextDesktop = ({
name = interpolateProjectNameWithIndex(name, nextIndex)
}
console.log('from Project')
await renameProjectDirectory(
window.electron.path.join(defaultDirectory, oldName),
name
@ -245,13 +382,82 @@ const ProjectsContextDesktop = ({
name: input.name,
}
}),
},
guards: {
'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
console.log(`from has at least 1 project: ${event.output.length}`)
return event.output.length ? event.output.length >= 1 : false
},
createFile: fromPromise(async ({ input }) => {
let projectName =
(input.method === 'newProject' ? input.name : input.projectName) ||
settings.projects.defaultProjectName.current
let fileName =
input.method === 'newProject'
? PROJECT_ENTRYPOINT
: input.name.endsWith(FILE_EXT)
? input.name
: input.name + FILE_EXT
let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
},
modeling: {
base_unit: input.units,
},
},
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) {
const nextIndex = getNextProjectIndex(projectName, input.projects)
projectName = interpolateProjectNameWithIndex(
projectName,
nextIndex
)
}
// Create the project around the file if newProject
if (input.method === 'newProject') {
await createNewProjectDirectory(
projectName,
input.code,
unitsConfiguration
)
message = `Project "${projectName}" created successfully with link contents`
} else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
}
// Create the file
let baseDir = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
const { name, path } = getNextFileName({
entryName: fileName,
baseDir,
})
fileName = name
await window.electron.writeFile(path, input.code || '')
return {
message,
fileName,
projectName,
}
}),
},
}),
{
@ -274,6 +480,7 @@ const ProjectsContextDesktop = ({
state,
commandBarConfig: projectsCommandBarConfig,
actor,
onCancel: clearImportSearchParams,
})
return (

View File

@ -301,7 +301,7 @@ export const Stream = () => {
return
}
const path = getArtifactOfTypes(
{ key: entity_id, types: ['path', 'solid2D', 'segment'] },
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
engineCommandManager.artifactGraph
)
if (err(path)) {

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import { PathToNode, Program, Expr } from '../../lang/wasm'
import { PathToNode, Program, Expr, topLevelRange } from '../../lang/wasm'
import { getNodeFromPath } from '../../lang/queryAst'
import {
PathToNodeMap,
@ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({
graphSelections: nodes.map(
(node): Selection => ({
codeRef: codeRefFromRange(
[node.start, node.end, true],
topLevelRange(node.start, node.end),
kclManager.ast
),
})

View File

@ -6,5 +6,6 @@ export const useCommandsContext = () => {
return {
commandBarSend: commandBarActor.send,
commandBarState,
commandBarActor,
}
}

View File

@ -0,0 +1,65 @@
import { base64ToString } from 'lib/base64'
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
// For initializing the command arguments, we actually want `method` to be undefined
// so that we don't skip it in the command palette.
export type CreateFileSchemaMethodOptional = Omit<
ProjectsCommandSchema['Import file from URL'],
'method'
> & {
method?: 'newProject' | 'existingProject'
}
/**
* companion to createFileLink. This hook runs an effect on mount that
* checks the URL for the CREATE_FILE_URL_PARAM and triggers the "Create file"
* command if it is present, loading the command's default values from the other
* URL parameters.
*/
export function useCreateFileLinkQuery(
callback: (args: CreateFileSchemaMethodOptional) => void
) {
const [searchParams] = useSearchParams()
const { settings } = useSettingsAuthContext()
useEffect(() => {
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
if (createFileParam) {
const params: FileLinkParams = {
code: base64ToString(
decodeURIComponent(searchParams.get('code') ?? '')
),
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
units:
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current,
}
const argDefaultValues: CreateFileSchemaMethodOptional = {
name: params.name
? isDesktop()
? params.name.replace('.kcl', '')
: params.name
: isDesktop()
? settings.context.projects.defaultProjectName.current
: DEFAULT_FILE_NAME,
code: params.code || '',
units: params.units,
method: isDesktop() ? undefined : 'existingProject',
}
callback(argDefaultValues)
}
}, [searchParams])
}

View File

@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
useEffect(() => {
// Useless on web, until we get fake filesystems over there.
if (!isDesktop) return
if (!isDesktop()) return
if (deps && deps[0] === lastTs) return

View File

@ -22,6 +22,7 @@ import {
ProgramMemory,
recast,
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
@ -376,11 +377,7 @@ export class KclManager {
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(
this.ast,
execState.artifactCommands,
execState.artifacts
)
this.engineCommandManager.updateArtifactGraph(execState.artifactGraph)
this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
@ -473,7 +470,7 @@ export class KclManager {
...artifact,
codeRef: {
...artifact.codeRef,
range: [node.start, node.end, true],
range: topLevelRange(node.start, node.end),
},
})
}
@ -594,7 +591,7 @@ export class KclManager {
if (start && end) {
returnVal.graphSelections.push({
codeRef: {
range: [start, end, true],
range: topLevelRange(start, end),
pathToNode: path,
},
})

View File

@ -24,7 +24,10 @@ describe('testing AST', () => {
type: 'Literal',
start: 0,
end: 1,
value: 5,
value: {
suffix: 'None',
value: 5,
},
raw: '5',
},
operator: '+',
@ -32,7 +35,10 @@ describe('testing AST', () => {
type: 'Literal',
start: 3,
end: 4,
value: 6,
value: {
suffix: 'None',
value: 6,
},
raw: '6',
},
},

View File

@ -54,6 +54,9 @@ const mySketch001 = startSketchOn('XY')
},
],
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }],
},
})
@ -72,56 +75,65 @@ const mySketch001 = startSketchOn('XY')
const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [77, 102, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [108, 132, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-1.59, -1.54],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [77, 102, 0],
},
id: expect.any(String),
sourceRange: [77, 102, 0],
},
{
type: 'ToPoint',
from: [-1.59, -1.54],
to: [0.46, -5.82],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [108, 132, 0],
},
id: expect.any(String),
sourceRange: [108, 132, 0],
},
],
sketch: {
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-1.59, -1.54],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [77, 102, 0],
},
},
{
type: 'ToPoint',
from: [-1.59, -1.54],
to: [0.46, -5.82],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [108, 132, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [46, 71, 0] }],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [46, 71, 0] }],
})
})
test('sketch extrude and sketch on one of the faces', async () => {
@ -154,187 +166,205 @@ const sk2 = startSketchOn('XY')
expect(geos).toEqual([
{
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [69, 89, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
id: expect.any(String),
sourceRange: [95, 117, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [123, 142, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
p: {
__meta: [
{
sourceRange: [114, 116, 0],
},
],
type: 'TagIdentifier',
value: 'p',
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [69, 89, 0],
},
id: expect.any(String),
sourceRange: [69, 89, 0],
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 10],
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [95, 117, 0],
},
id: expect.any(String),
sourceRange: [95, 117, 0],
},
{
type: 'ToPoint',
from: [0, 10],
to: [2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [123, 142, 0],
},
id: expect.any(String),
sourceRange: [123, 142, 0],
},
],
sketch: {
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
units: {
type: 'Mm',
},
tags: {
p: {
__meta: [
{
sourceRange: [114, 116, 0],
},
],
type: 'TagIdentifier',
value: 'p',
info: expect.any(Object),
},
},
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [69, 89, 0],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 10],
tag: {
end: 116,
start: 114,
type: 'TagDeclarator',
value: 'p',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [95, 117, 0],
},
},
{
type: 'ToPoint',
from: [0, 10],
to: [2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [123, 142, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [38, 63, 0] }],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [38, 63, 0] }],
},
{
type: 'Solid',
id: expect.any(String),
value: [
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [373, 393, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
id: expect.any(String),
sourceRange: [399, 420, 0],
},
{
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
id: expect.any(String),
sourceRange: [426, 445, 0],
},
],
sketch: {
value: {
type: 'Solid',
id: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
o: {
__meta: [
{
sourceRange: [417, 419, 0],
},
],
type: 'TagIdentifier',
value: 'o',
info: expect.any(Object),
},
},
paths: [
value: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [373, 393, 0],
},
id: expect.any(String),
sourceRange: [373, 393, 0],
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 3],
type: 'extrudePlane',
faceId: expect.any(String),
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [399, 420, 0],
},
id: expect.any(String),
sourceRange: [399, 420, 0],
},
{
type: 'ToPoint',
from: [0, 3],
to: [2.5, 0],
type: 'extrudePlane',
faceId: expect.any(String),
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [426, 445, 0],
},
id: expect.any(String),
sourceRange: [426, 445, 0],
},
],
sketch: {
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
tags: {
o: {
__meta: [
{
sourceRange: [417, 419, 0],
},
],
type: 'TagIdentifier',
value: 'o',
info: expect.any(Object),
},
},
paths: [
{
type: 'ToPoint',
from: [0, 0],
to: [-2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [373, 393, 0],
},
},
{
type: 'ToPoint',
from: [-2.5, 0],
to: [0, 3],
tag: {
end: 419,
start: 417,
type: 'TagDeclarator',
value: 'o',
},
__geoMeta: {
id: expect.any(String),
sourceRange: [399, 420, 0],
},
},
{
type: 'ToPoint',
from: [0, 3],
to: [2.5, 0],
tag: null,
__geoMeta: {
id: expect.any(String),
sourceRange: [426, 445, 0],
},
},
],
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }],
units: {
type: 'Mm',
},
},
height: 2,
startCapId: expect.any(String),
endCapId: expect.any(String),
__meta: [{ sourceRange: [342, 367, 0] }],
},
])
})

View File

@ -1,4 +1,5 @@
import { kclErrorsToDiagnostics, KCLError } from './errors'
import { defaultArtifactGraph, topLevelRange } from 'lang/wasm'
describe('test kclErrToDiagnostic', () => {
it('converts KCL errors to CodeMirror diagnostics', () => {
@ -8,18 +9,20 @@ describe('test kclErrToDiagnostic', () => {
message: '',
kind: 'semantic',
msg: 'Semantic error',
sourceRange: [0, 1, true],
sourceRange: topLevelRange(0, 1),
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
},
{
name: '',
message: '',
kind: 'type',
msg: 'Type error',
sourceRange: [4, 5, true],
sourceRange: topLevelRange(4, 5),
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
},
]
const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -5,7 +5,13 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror'
import { ArtifactCommand, SourceRange } from 'lang/wasm'
import {
ArtifactCommand,
ArtifactGraph,
defaultArtifactGraph,
isTopLevelModule,
SourceRange,
} from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -15,13 +21,15 @@ export class KCLError extends Error {
msg: string
operations: Operation[]
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
constructor(
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super()
this.kind = kind
@ -29,6 +37,7 @@ export class KCLError extends Error {
this.sourceRange = sourceRange
this.operations = operations
this.artifactCommands = artifactCommands
this.artifactGraph = artifactGraph
Object.setPrototypeOf(this, KCLError.prototype)
}
}
@ -38,9 +47,17 @@ export class KCLLexicalError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('lexical', msg, sourceRange, operations, artifactCommands)
super(
'lexical',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
@ -50,9 +67,17 @@ export class KCLInternalError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('internal', msg, sourceRange, operations, artifactCommands)
super(
'internal',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
@ -62,9 +87,17 @@ export class KCLSyntaxError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('syntax', msg, sourceRange, operations, artifactCommands)
super(
'syntax',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
@ -74,9 +107,17 @@ export class KCLSemanticError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('semantic', msg, sourceRange, operations, artifactCommands)
super(
'semantic',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSemanticError.prototype)
}
}
@ -86,9 +127,10 @@ export class KCLTypeError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('type', msg, sourceRange, operations, artifactCommands)
super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
Object.setPrototypeOf(this, KCLTypeError.prototype)
}
}
@ -98,9 +140,17 @@ export class KCLUnimplementedError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('unimplemented', msg, sourceRange, operations, artifactCommands)
super(
'unimplemented',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
}
}
@ -110,9 +160,17 @@ export class KCLUnexpectedError extends KCLError {
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super('unexpected', msg, sourceRange, operations, artifactCommands)
super(
'unexpected',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
}
}
@ -122,14 +180,16 @@ export class KCLValueAlreadyDefined extends KCLError {
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super(
'name',
`Key ${key} was already defined elsewhere`,
sourceRange,
operations,
artifactCommands
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
@ -140,14 +200,16 @@ export class KCLUndefinedValueError extends KCLError {
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super(
'name',
`Key ${key} has not been defined`,
sourceRange,
operations,
artifactCommands
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
@ -167,9 +229,10 @@ export function lspDiagnosticsToKclErrors(
new KCLError(
'unexpected',
message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
[],
[]
[],
defaultArtifactGraph()
)
)
.sort((a, b) => {
@ -193,7 +256,7 @@ export function kclErrorsToDiagnostics(
errors: KCLError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2])
?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => {
return {
from: err.sourceRange[0],
@ -208,7 +271,7 @@ export function complilationErrorsToDiagnostics(
errors: CompilationError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2] === 0)
?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => {
let severity: any = 'error'
if (err.severity === 'Warning') {

View File

@ -6,6 +6,8 @@ import {
Sketch,
initPromise,
sketchFromKclValue,
defaultArtifactGraph,
topLevelRange,
} from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors'
@ -219,6 +221,9 @@ const newVar = myVar + 1`
},
],
id: expect.any(String),
units: {
type: 'Mm',
},
__meta: [{ sourceRange: [39, 63, 0] }],
},
})
@ -480,9 +485,10 @@ const theExtrude = startSketchOn('XY')
new KCLError(
'undefined_value',
'memory item key `myVarZ` is not defined',
[129, 135, true],
topLevelRange(129, 135),
[],
[]
[],
defaultArtifactGraph()
)
)
})

View File

@ -1,5 +1,12 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, assertParse, initPromise, Parameter } from './wasm'
import {
Identifier,
assertParse,
initPromise,
Parameter,
SourceRange,
topLevelRange,
} from './wasm'
import { err } from 'lib/trap'
beforeAll(async () => {
@ -17,11 +24,10 @@ const sk3 = startSketchAt([0, 0])
`
const subStr = 'lineTo([3, 4], %, $yo)'
const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
const sourceRange = topLevelRange(
lineToSubstringIndex,
lineToSubstringIndex + subStr.length,
true,
]
lineToSubstringIndex + subStr.length
)
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -29,7 +35,7 @@ const sk3 = startSketchAt([0, 0])
if (err(_node)) throw _node
const { node } = _node
expect([node.start, node.end, true]).toEqual(sourceRange)
expect(topLevelRange(node.start, node.end)).toEqual(sourceRange)
expect(node.type).toBe('CallExpression')
})
it('gets path right for function definition params', () => {
@ -45,11 +51,7 @@ const sk3 = startSketchAt([0, 0])
const b1 = cube([0,0], 10)`
const subStr = 'pos, scale'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'pos'.length,
true,
]
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length)
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -81,11 +83,7 @@ const b1 = cube([0,0], 10)`
const b1 = cube([0,0], 10)`
const subStr = 'scale, 0'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'scale'.length,
true,
]
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'scale'.length)
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)

View File

@ -1,4 +1,11 @@
import { assertParse, recast, initPromise, Identifier } from './wasm'
import {
assertParse,
recast,
initPromise,
Identifier,
SourceRange,
topLevelRange,
} from './wasm'
import {
createLiteral,
createIdentifier,
@ -32,7 +39,7 @@ describe('Testing createLiteral', () => {
it('should create a literal', () => {
const result = createLiteral(5)
expect(result.type).toBe('Literal')
expect(result.value).toBe(5)
expect((result as any).value.value).toBe(5)
})
})
describe('Testing createIdentifier', () => {
@ -49,7 +56,7 @@ describe('Testing createCallExpression', () => {
expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value).toBe(5)
expect((result.arguments[0] as any).value.value).toBe(5)
})
})
describe('Testing createObjectExpression', () => {
@ -61,7 +68,7 @@ describe('Testing createObjectExpression', () => {
expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value).toBe(5)
expect((result.properties[0].value as any).value.value).toBe(5)
})
})
describe('Testing createArrayExpression', () => {
@ -69,7 +76,7 @@ describe('Testing createArrayExpression', () => {
const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value).toBe(5)
expect((result.elements[0] as any).value.value).toBe(5)
})
})
describe('Testing createPipeSubstitution', () => {
@ -86,7 +93,7 @@ describe('Testing createVariableDeclaration', () => {
expect(result.declaration.id.type).toBe('Identifier')
expect(result.declaration.id.name).toBe('myVar')
expect(result.declaration.init.type).toBe('Literal')
expect((result.declaration.init as any).value).toBe(5)
expect((result.declaration.init as any).value.value).toBe(5)
})
})
describe('Testing createPipeExpression', () => {
@ -94,7 +101,7 @@ describe('Testing createPipeExpression', () => {
const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value).toBe(5)
expect((result.body[0] as any).value.value).toBe(5)
})
})
@ -148,11 +155,7 @@ function giveSketchFnCallTagTestHelper(
// making it more of an integration test, but easier to read the test intention is the goal
const ast = assertParse(code)
const start = code.indexOf(searchStr)
const range: [number, number, boolean] = [
start,
start + searchStr.length,
true,
]
const range = topLevelRange(start, start + searchStr.length)
const sketchRes = giveSketchFnCallTag(ast, range)
if (err(sketchRes)) throw sketchRes
const { modifiedAst, tag, isTagExisting } = sketchRes
@ -230,7 +233,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -244,7 +247,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -258,7 +261,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -272,7 +275,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -286,7 +289,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -306,18 +309,16 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = assertParse(code)
const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -346,18 +347,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = assertParse(code)
const segmentSnippet = `close(%)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -386,18 +385,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = assertParse(code)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number, boolean] = [
const sketchRange = topLevelRange(
code.indexOf(sketchSnippet),
code.indexOf(sketchSnippet) + sketchSnippet.length,
true,
]
code.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -435,18 +432,16 @@ sketch001 = startSketchOn(part001, 'END')`)
part001 = extrude(5 + 7, sketch001)`
const ast = assertParse(code)
const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, sketch001)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const updatedAst = sketchOnExtrudedFace(
@ -471,11 +466,10 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression(
[],
@ -549,11 +543,10 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = line
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
const modifiedAst = deleteSegmentFromPipeExpression(
@ -638,11 +631,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') {
@ -692,11 +684,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') {
argPosition = {
@ -889,11 +880,10 @@ sketch002 = startSketchOn({
const execState = await enginelessExecutor(ast)
// deleteFromSelection
const range: [number, number, boolean] = [
const range = topLevelRange(
codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length
)
const artifact = { type } as Artifact
const newAst = await deleteFromSelection(
ast,

View File

@ -743,14 +743,18 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
}
export function createLiteral(value: LiteralValue): Node<Literal> {
export function createLiteral(value: LiteralValue | number): Node<Literal> {
const raw = `${value}`
if (typeof value === 'number') {
value = { value, suffix: 'None' }
}
return {
type: 'Literal',
start: 0,
end: 0,
moduleId: 0,
value,
raw: `${value}`,
raw,
}
}

View File

@ -8,6 +8,8 @@ import {
makeDefaultPlanes,
PipeExpression,
VariableDeclarator,
SourceRange,
topLevelRange,
} from '../wasm'
import {
EdgeTreatmentType,
@ -77,11 +79,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
true,
]
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length
)
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expectedExtrudeNodeResult = getNodeFromPath<
VariableDeclarator | CallExpression
@ -112,11 +113,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
const ast = assertParse(code)
// selection
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
true,
]
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
)
const selection: Selection = {
codeRef: codeRefFromRange(segmentRange, ast),
}
@ -260,12 +260,12 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
const ast = assertParse(code)
// selection
const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map(
(selectionSnippet) => [
code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length,
true,
]
const segmentRanges: Array<SourceRange> = selectionSnippets.map(
(selectionSnippet) =>
topLevelRange(
code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length
)
)
// executeAst
@ -596,11 +596,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = assertParse(code)
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
@ -615,11 +614,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = assertParse(code)
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
@ -634,11 +632,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify no edges', () => {
const ast = assertParse(code)
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
@ -660,13 +657,12 @@ describe('Testing button states', () => {
) => {
const ast = assertParse(code)
const range: [number, number, boolean] = segmentSnippet
? [
const range = segmentSnippet
? topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
: [ast.end, ast.end, true] // empty line in the end of the code
code.indexOf(segmentSnippet) + segmentSnippet.length
)
: topLevelRange(ast.end, ast.end) // empty line in the end of the code
const selectionRanges: Selections = {
graphSelections: [

View File

@ -1,4 +1,5 @@
import {
ArtifactGraph,
CallExpression,
Expr,
Identifier,
@ -31,11 +32,7 @@ import {
import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
Artifact,
ArtifactGraph,
getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import {
kclManager,
engineCommandManager,

View File

@ -1,9 +1,8 @@
import { ArtifactGraph } from 'lang/std/artifactGraph'
import { Selections } from 'lib/selections'
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
import { Program } from 'wasm-lib/kcl/bindings/Program'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { PathToNode, VariableDeclarator } from 'lang/wasm'
import { ArtifactGraph, PathToNode, VariableDeclarator } from 'lang/wasm'
import {
getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment,

View File

@ -4,6 +4,7 @@ import {
initPromise,
PathToNode,
Identifier,
topLevelRange,
} from './wasm'
import {
findAllPreviousVariables,
@ -57,7 +58,7 @@ variableBelowShouldNotBeIncluded = 3
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast,
execState.memory,
[rangeStart, rangeStart, true]
topLevelRange(rangeStart, rangeStart)
)
expect(variables).toEqual([
{ key: 'baseThick', value: 1 },
@ -87,7 +88,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe binaryExpression', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('100 + 100') + 2
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -100,7 +104,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe Identifier', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('abc')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('Identifier')
@ -109,7 +116,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe CallExpression', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('def')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('CallExpression')
@ -122,7 +132,7 @@ yo2 = hmm([identifierGuy + 5])`
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('ghi')
const range: [number, number, boolean] = [rangeStart, rangeStart, true]
const range = topLevelRange(rangeStart, rangeStart)
const result = isNodeSafeToReplace(ast, range)
if (err(result)) throw result
expect(result.isSafe).toBe(false)
@ -132,7 +142,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find an UNsafe Identifier, as it is a callee', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('ine([2.8,')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(false)
expect(result.value?.type).toBe('CallExpression')
@ -143,7 +156,10 @@ yo2 = hmm([identifierGuy + 5])`
it("find a safe BinaryExpression that's assigned to a variable", () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('5 + 6') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -156,7 +172,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe BinaryExpression that has a CallExpression within', () => {
const ast = assertParse(code)
const rangeStart = code.indexOf('jkl') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression')
@ -173,7 +192,10 @@ yo2 = hmm([identifierGuy + 5])`
const ast = assertParse(code)
const rangeStart = code.indexOf('identifierGuy') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true])
const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result
expect(result.isSafe).toBe(true)
@ -222,11 +244,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const result = getNodePathFromSourceRange(
ast,
topLevelRange(sourceIndex, sourceIndex)
)
expect(result).toEqual([
['body', ''],
[0, 'index'],
@ -241,11 +262,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const result = getNodePathFromSourceRange(
ast,
topLevelRange(sourceIndex, sourceIndex)
)
const expected = [
['body', ''],
[0, 'index'],
@ -257,18 +277,16 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual(expected)
// expect similar result for start of line
const startSourceIndex = code.indexOf(searchLn)
const startResult = getNodePathFromSourceRange(ast, [
startSourceIndex,
startSourceIndex,
true,
])
const startResult = getNodePathFromSourceRange(
ast,
topLevelRange(startSourceIndex, startSourceIndex)
)
expect(startResult).toEqual([...expected, ['callee', 'CallExpression']])
// expect similar result when whole line is selected
const selectWholeThing = getNodePathFromSourceRange(ast, [
startSourceIndex,
sourceIndex,
true,
])
const selectWholeThing = getNodePathFromSourceRange(
ast,
topLevelRange(startSourceIndex, sourceIndex)
)
expect(selectWholeThing).toEqual(expected)
})
@ -283,11 +301,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const result = getNodePathFromSourceRange(
ast,
topLevelRange(sourceIndex, sourceIndex)
)
expect(result).toEqual([
['body', ''],
[1, 'index'],
@ -313,11 +330,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const result = getNodePathFromSourceRange(
ast,
topLevelRange(sourceIndex, sourceIndex)
)
expect(result).toEqual([
['body', ''],
[1, 'index'],
@ -341,11 +357,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [
sourceIndex,
sourceIndex,
true,
])
const result = getNodePathFromSourceRange(
ast,
topLevelRange(sourceIndex, sourceIndex)
)
expect(result).toEqual([
['body', ''],
[0, 'index'],
@ -375,7 +390,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([100, 101, true], ast),
codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
},
programMemory: execState.memory,
})
@ -395,7 +410,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([100, 101, true], ast),
codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
},
programMemory: execState.memory,
})
@ -409,7 +424,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({
ast,
selection: {
codeRef: codeRefFromRange([10, 11, true], ast),
codeRef: codeRefFromRange(topLevelRange(10, 11), ast),
},
programMemory: execState.memory,
})
@ -431,11 +446,10 @@ describe('Testing findUsesOfTagInPipe', () => {
const lineOfInterest = `198.85], %, $seg01`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const pathToNode = getNodePathFromSourceRange(ast, [
characterIndex,
characterIndex,
true,
])
const pathToNode = getNodePathFromSourceRange(
ast,
topLevelRange(characterIndex, characterIndex)
)
const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(2)
result.forEach((range) => {
@ -448,11 +462,10 @@ describe('Testing findUsesOfTagInPipe', () => {
const lineOfInterest = `line([306.21, 198.82], %)`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const pathToNode = getNodePathFromSourceRange(ast, [
characterIndex,
characterIndex,
true,
])
const pathToNode = getNodePathFromSourceRange(
ast,
topLevelRange(characterIndex, characterIndex)
)
const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(0)
})
@ -498,7 +511,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
},
ast
)
@ -511,7 +527,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
},
ast
)
@ -524,7 +543,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast),
codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
},
ast
)
@ -638,7 +660,7 @@ myNestedVar = [
enter: (node, path) => {
if (
node.type === 'Literal' &&
String(node.value) === literalOfInterest
String((node as any).value.value) === literalOfInterest
) {
pathToNode = path
} else if (
@ -651,11 +673,10 @@ myNestedVar = [
})
const literalIndex = code.indexOf(literalOfInterest)
const pathToNode2 = getNodePathFromSourceRange(ast, [
literalIndex + 2,
literalIndex + 2,
true,
])
const pathToNode2 = getNodePathFromSourceRange(
ast,
topLevelRange(literalIndex + 2, literalIndex + 2)
)
expect(pathToNode).toEqual(pathToNode2)
})
})

View File

@ -2,6 +2,7 @@ import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import {
ArrayExpression,
ArtifactGraph,
BinaryExpression,
CallExpression,
Expr,
@ -16,8 +17,8 @@ import {
sketchFromKclValue,
sketchFromKclValueOptional,
SourceRange,
sourceRangeFromRust,
SyntaxType,
topLevelRange,
VariableDeclaration,
VariableDeclarator,
} from './wasm'
@ -32,7 +33,7 @@ import {
import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
import { codeRefFromRange } from './std/artifactGraph'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -716,16 +717,6 @@ function isTypeInArrayExp(
return node.elements.some((el) => isTypeInValue(el, syntaxType))
}
export function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && Number(val.value) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
export function isLinesParallelAndConstrained(
ast: Program,
artifactGraph: ArtifactGraph,
@ -819,7 +810,7 @@ export function isLinesParallelAndConstrained(
return {
isParallelAndConstrained,
selection: {
codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast),
codeRef: codeRefFromRange(prevSourceRange, ast),
artifact: artifactGraph.get(prevSegment.__geoMeta.id),
},
}
@ -937,7 +928,7 @@ export function findUsesOfTagInPipe(
const tagArgValue =
tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name
if (tagArgValue === tag)
dependentRanges.push([node.start, node.end, true])
dependentRanges.push(topLevelRange(node.start, node.end))
},
})
return dependentRanges

View File

@ -1,559 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = `
Map {
"UUID-0" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
12,
31,
true,
],
},
"id": "UUID",
"pathIds": [
"UUID",
],
"type": "plane",
},
"UUID-1" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
37,
64,
true,
],
},
"id": "UUID",
"planeId": "UUID",
"segIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"solid2dId": "UUID",
"sweepId": "UUID",
"type": "path",
},
"UUID-2" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
70,
86,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-3" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
92,
119,
true,
],
},
"edgeCutId": "UUID",
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-4" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
125,
150,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-5" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
156,
203,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-6" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
209,
217,
true,
],
},
"edgeIds": [],
"id": "UUID",
"pathId": "UUID",
"type": "segment",
},
"UUID-7" => {
"id": "UUID",
"pathId": "UUID",
"type": "solid2D",
},
"UUID-8" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
231,
254,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
"UUID-9" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-10" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [
"UUID",
],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-11" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-12" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-13" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "start",
"sweepId": "UUID",
"type": "cap",
},
"UUID-14" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "end",
"sweepId": "UUID",
"type": "cap",
},
"UUID-15" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-16" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-17" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-18" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-19" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-20" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-21" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-22" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-23" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
260,
299,
true,
],
},
"consumedEdgeId": "UUID",
"edgeIds": [],
"id": "UUID",
"subType": "fillet",
"type": "edgeCut",
},
"UUID-24" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
350,
377,
true,
],
},
"id": "UUID",
"planeId": "UUID",
"segIds": [
"UUID",
"UUID",
"UUID",
"UUID",
],
"solid2dId": "UUID",
"sweepId": "UUID",
"type": "path",
},
"UUID-25" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
383,
398,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-26" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
404,
420,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-27" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
426,
473,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-28" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
479,
487,
true,
],
},
"edgeIds": [],
"id": "UUID",
"pathId": "UUID",
"type": "segment",
},
"UUID-29" => {
"id": "UUID",
"pathId": "UUID",
"type": "solid2D",
},
"UUID-30" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
501,
522,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
"UUID-31" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-32" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-33" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-34" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "end",
"sweepId": "UUID",
"type": "cap",
},
"UUID-35" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-36" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-37" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-38" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-39" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
}
`;

View File

@ -1,960 +0,0 @@
import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ArtifactCommand,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
ResponseMap,
createArtifactGraph,
filterArtifacts,
expandPlane,
expandPath,
expandSweep,
ArtifactGraph,
expandSegment,
getArtifactsToUpdate,
} from './artifactGraph'
import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
import fsp from 'fs/promises'
import fs from 'fs'
import { chromium } from 'playwright'
import * as d3 from 'd3-force'
import path from 'path'
import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/*
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
It's needed for testing the artifactGraph, as it is tied to the websocket commands.
*/
const pathStart = 'src/lang/std/artifactMapCache'
const fullPath = `${pathStart}/artifactMapCache.json`
const exampleCode1 = `sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10.55, 0], %, $seg01)
|> line([0, -10], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)
sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-2, -6], %)
|> line([2, 3], %)
|> line([2, -3], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(5, sketch002)
`
const exampleCodeNo3D = `sketch003 = startSketchOn('YZ')
|> startProfileAt([5.82, 0], %)
|> angledLine([180, 11.54], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
8.21
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch004 = startSketchOn('-XZ')
|> startProfileAt([0, 14.36], %)
|> line([15.49, 0.05], %)
|> tangentialArcTo([0, 0], %)
|> tangentialArcTo([-6.8, 8.17], %)
`
const sketchOnFaceOnFaceEtc = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([4, 8], %)
|> line([5, -8], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(6, sketch001)
sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-0.5, 0.5], %)
|> line([2, 5], %)
|> line([2, -5], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(5, sketch002)
sketch003 = startSketchOn(extrude002, 'END')
|> startProfileAt([1, 1.5], %)
|> line([0.5, 2], %, $seg02)
|> line([1, -2], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(4, sketch003)
sketch004 = startSketchOn(extrude003, seg02)
|> startProfileAt([-3, 14], %)
|> line([0.5, 1], %)
|> line([0.5, -2], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude004 = extrude(3, sketch004)
`
const exampleCodeOffsetPlanes = `
offsetPlane001 = offsetPlane("XY", 20)
offsetPlane002 = offsetPlane("XZ", -50)
offsetPlane003 = offsetPlane("YZ", 10)
sketch002 = startSketchOn(offsetPlane001)
|> startProfileAt([0, 0], %)
|> line([6.78, 15.01], %)
`
// add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests
const codeToWriteCacheFor = {
exampleCode1,
sketchOnFaceOnFaceEtc,
exampleCodeNo3D,
exampleCodeOffsetPlanes,
} as const
type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = {
[key in CodeKey]: {
artifactCommands: ArtifactCommand[]
responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
}
}
beforeAll(async () => {
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
// disableWebRTC: true,
token: VITE_KC_DEV_TOKEN,
// there does seem to be a minimum resolution, not sure what it is but 256 works ok.
width: 256,
height: 256,
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
CodeKey,
string
][]
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
for (const [codeKey, code] of cacheEntries) {
const ast = assertParse(code)
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
artifactCommands: kclManager.execState.artifactCommands,
responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
}
}
const cache = JSON.stringify(cacheToWriteToFileTemp)
await fsp.mkdir(pathStart, { recursive: true })
await fsp.writeFile(fullPath, cache)
resolve(true)
},
})
})
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
})
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it(`there should be one sketch`, () => {
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(sketches).toHaveLength(1)
sketches.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it(`there should be three offsetPlanes`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
expect(offsetPlanes).toHaveLength(3)
offsetPlanes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it(`Only one offset plane should have a path`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
const offsetPlaneWithPaths = offsetPlanes.filter(
(plane) => plane.paths.length
)
expect(offsetPlaneWithPaths).toHaveLength(1)
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes for the extrusion and the sketch on face', () => {
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
(plane) => expandPlane(plane[1], theMap)
)
expect(planes).toHaveLength(1)
planes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it('there should be two paths for the extrusion and the sketch on face', () => {
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(paths).toHaveLength(2)
paths.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
(extrusion) => expandSweep(extrusion[1], theMap)
)
expect(extrusions).toHaveLength(2)
extrusions.forEach((extrusion, index) => {
if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6
// Each face of the triangular prism (5), but without the bottom cap.
// The engine doesn't generate that.
const secondExtrusionIsATriangularPrism = 4
expect(extrusion.surfaces.length).toBe(
!index
? firstExtrusionIsACubeIE6Sides
: secondExtrusionIsATriangularPrism
)
})
})
it('there should be 5 + 4 segments, 4 (+close) from the first extrusion and 3 (+close) from the second', () => {
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
(segment) => expandSegment(segment[1], theMap)
)
expect(segments).toHaveLength(9)
})
it('snapshot of the artifactGraph', () => {
const stableMap = new Map(
[...theMap].map(([, artifact], index): [string, any] => {
const stableValue: any = {}
Object.entries(artifact).forEach(([propName, value]) => {
if (
propName === 'type' ||
propName === 'codeRef' ||
propName === 'subType'
) {
stableValue[propName] = value
return
}
if (Array.isArray(value))
stableValue[propName] = value.map(() => 'UUID')
if (typeof value === 'string' && value)
stableValue[propName] = 'UUID'
})
return [`UUID-${index}`, stableValue]
})
)
expect(stableMap).toMatchSnapshot()
})
it('screenshot graph', async () => {
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png')
}, 20000)
})
describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes, one for each sketch path', () => {
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
(plane) => expandPlane(plane[1], theMap)
)
expect(planes).toHaveLength(2)
planes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it('there should be two paths, one on each plane', () => {
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(paths).toHaveLength(2)
paths.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it(`there should be 1 solid2D, just for the first closed path`, () => {
const solid2Ds = [...filterArtifacts({ types: ['solid2D'] }, theMap)]
expect(solid2Ds).toHaveLength(1)
})
it('there should be no extrusions', () => {
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
(extrusion) => expandSweep(extrusion[1], theMap)
)
expect(extrusions).toHaveLength(0)
})
it('there should be 8 segments, 4 + 1 (close) from the first sketch and 3 from the second', () => {
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
(segment) => expandSegment(segment[1], theMap)
)
expect(segments).toHaveLength(8)
})
it('screenshot graph', async () => {
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 2000, 2000, 'exampleCodeNo3D.png')
}, 20000)
})
})
describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 3000, 3000, 'sketchOnFaceOnFaceEtc.png')
}, 20000)
})
})
function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in
const artifactCommands = parsed[codeKey].artifactCommands
const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return {
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}
}
async function GraphTheGraph(
theMap: ArtifactGraph,
sizeX: number,
sizeY: number,
imageName: string
) {
const nodes: Array<{ id: string; label: string }> = []
const edges: Array<{ source: string; target: string; label: string }> = []
let index = 0
for (const [commandId, artifact] of theMap) {
nodes.push({
id: commandId,
label: `${artifact.type}-${index++}`,
})
Object.entries(artifact).forEach(([propName, value]) => {
if (
propName === 'type' ||
propName === 'codeRef' ||
propName === 'subType' ||
propName === 'id'
)
return
if (Array.isArray(value))
value.forEach((v) => {
v && edges.push({ source: commandId, target: v, label: propName })
})
if (typeof value === 'string' && value)
edges.push({ source: commandId, target: value, label: propName })
})
}
// Create a force simulation to calculate node positions
const simulation = d3
.forceSimulation(nodes as any)
.force(
'link',
d3
.forceLink(edges)
.id((d: any) => d.id)
.distance(100)
)
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(300, 200))
.stop()
// Run the simulation
for (let i = 0; i < 300; ++i) simulation.tick()
// Create traces for Plotly
const nodeTrace = {
x: nodes.map((node: any) => node.x),
y: nodes.map((node: any) => node.y),
text: nodes.map((node) => node.label), // Use the custom label
mode: 'markers+text',
type: 'scatter',
marker: { size: 20, color: 'gray' }, // Nodes in gray
textfont: { size: 14, color: 'black' }, // Labels in black
textposition: 'top center', // Position text on top
}
const edgeTrace = {
x: [],
y: [],
mode: 'lines',
type: 'scatter',
line: { width: 2, color: 'lightgray' }, // Edges in light gray
}
const annotations: any[] = []
edges.forEach((edge) => {
const sourceNode = nodes.find(
(node: any) => node.id === (edge as any).source.id
)
const targetNode = nodes.find(
(node: any) => node.id === (edge as any).target.id
)
// Check if nodes are found
if (!sourceNode || !targetNode) {
throw new Error(
// @ts-ignore
`Node not found: ${!sourceNode ? edge.source.id : edge.target.id}`
)
}
// @ts-ignore
edgeTrace.x.push(sourceNode.x, targetNode.x, null)
// @ts-ignore
edgeTrace.y.push(sourceNode.y, targetNode.y, null)
// Calculate offset for arrowhead
const offsetFactor = 0.9 // Adjust this factor to control the offset distance
// @ts-ignore
const offsetX = (targetNode.x - sourceNode.x) * offsetFactor
// @ts-ignore
const offsetY = (targetNode.y - sourceNode.y) * offsetFactor
// Add arrowhead annotation with offset
annotations.push({
// @ts-ignore
ax: sourceNode.x,
// @ts-ignore
ay: sourceNode.y,
// @ts-ignore
x: targetNode.x - offsetX,
// @ts-ignore
y: targetNode.y - offsetY,
xref: 'x',
yref: 'y',
axref: 'x',
ayref: 'y',
showarrow: true,
arrowhead: 2,
arrowsize: 1,
arrowwidth: 2,
arrowcolor: 'darkgray', // Arrowheads in dark gray
})
// Add edge label annotation closer to the edge tail (25% of the length)
// @ts-ignore
const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25
// @ts-ignore
const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25
annotations.push({
x: labelX,
y: labelY,
xref: 'x',
yref: 'y',
text: edge.label,
showarrow: false,
font: { size: 12, color: 'black' }, // Edge labels in black
align: 'center',
})
})
const data = [edgeTrace, nodeTrace]
const layout = {
// title: 'Force-Directed Graph with Nodes and Edges',
xaxis: { showgrid: false, zeroline: false, showticklabels: false },
yaxis: { showgrid: false, zeroline: false, showticklabels: false },
showlegend: false,
annotations: annotations,
}
// Export to PNG using Playwright
const browser = await chromium.launch()
const page = await browser.newPage()
await page.setContent(`
<html>
<head>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div>
<script>
Plotly.newPlot('plotly-graph', ${JSON.stringify(
data
)}, ${JSON.stringify(layout)})
</script>
</body>
</html>
`)
await page.waitForSelector('#plotly-graph')
const element = await page.$('#plotly-graph')
// @ts-ignore
await element.screenshot({
path: `./e2e/playwright/temp3.png`,
})
await browser.close()
const originalImgPath = path.resolve(
`./src/lang/std/artifactMapGraphs/${imageName}`
)
// chop the top 30 pixels off the image
const originalImgExists = fs.existsSync(originalImgPath)
const originalImg = originalImgExists
? PNG.sync.read(fs.readFileSync(originalImgPath))
: null
// const img1Data = new Uint8Array(img1.data)
// const img1DataChopped = img1Data.slice(30 * img1.width * 4)
// img1.data = Buffer.from(img1DataChopped)
const newImagePath = path.resolve('./e2e/playwright/temp3.png')
const newImage = PNG.sync.read(fs.readFileSync(newImagePath))
const newImageData = new Uint8Array(newImage.data)
const newImageDataChopped = newImageData.slice(30 * newImage.width * 4)
newImage.data = Buffer.from(newImageDataChopped)
const { width, height } = originalImg ?? newImage
const diff = new PNG({ width, height })
const imageSizeDifferent = originalImg?.data.length !== newImage.data.length
let numDiffPixels = 0
if (!imageSizeDifferent) {
numDiffPixels = pixelmatch(
originalImg.data,
newImage.data,
diff.data,
width,
height,
{
threshold: 0.1,
}
)
}
if (numDiffPixels > 10 || imageSizeDifferent) {
console.warn('numDiffPixels', numDiffPixels)
// write file out to final place
fs.writeFileSync(
`src/lang/std/artifactMapGraphs/${imageName}`,
PNG.sync.write(newImage)
)
}
}
describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => {
const { artifactCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1')
const map = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
const artifactCommand = artifactCommands.find(
(a) => a.command.type === type
)
if (!artifactCommand) {
throw new Error(`No artifactCommand found for ${type}`)
}
const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand,
responseMap,
getArtifact,
currentPlaneId,
ast,
execStateArtifacts,
})
return artifactsToUpdate.map(({ artifact }) => artifact)
}
expect(getUpdateObjects('start_path')).toEqual([
{
type: 'path',
segIds: [],
id: expect.any(String),
planeId: 'UUID-1',
sweepId: undefined,
codeRef: {
pathToNode: [['body', '']],
range: [37, 64, true],
},
},
])
expect(getUpdateObjects('extrude')).toEqual([
{
type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String),
id: expect.any(String),
surfaceIds: [],
edgeIds: [],
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'path',
id: expect.any(String),
segIds: expect.any(Array),
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
},
])
expect(getUpdateObjects('extend_path')).toEqual([
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: undefined,
edgeIds: [],
codeRef: {
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
{
type: 'path',
id: expect.any(String),
segIds: expect.any(Array),
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
},
])
expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([
{
type: 'edgeCut',
subType: 'fillet',
id: expect.any(String),
consumedEdgeId: expect.any(String),
edgeIds: [],
surfaceId: undefined,
codeRef: {
range: [260, 299, true],
pathToNode: [['body', '']],
},
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
},
])
expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [156, 203, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [125, 150, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'cap',
subType: 'start',
id: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'cap',
subType: 'end',
id: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
])
})
})

View File

@ -1,17 +1,25 @@
import {
ArtifactCommand,
ExecState,
Artifact,
ArtifactGraph,
ArtifactId,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
PathArtifact,
PlaneArtifact,
WallArtifact,
SegmentArtifact,
Solid2dArtifact as Solid2D,
SweepArtifact,
SweepEdge,
CapArtifact,
EdgeCut,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
interface BaseArtifact {
id: ArtifactId
@ -22,30 +30,12 @@ export interface CodeRef {
pathToNode: PathToNode
}
export interface PlaneArtifact extends BaseArtifact {
type: 'plane'
pathIds: Array<ArtifactId>
codeRef: CodeRef
}
export interface PlaneArtifactRich extends BaseArtifact {
type: 'plane'
paths: Array<PathArtifact>
codeRef: CodeRef
}
export interface PathArtifact extends BaseArtifact {
type: 'path'
planeId: ArtifactId
segIds: Array<ArtifactId>
sweepId?: ArtifactId
solid2dId?: ArtifactId
codeRef: CodeRef
}
interface solid2D extends BaseArtifact {
type: 'solid2D'
pathId: ArtifactId
}
export interface PathArtifactRich extends BaseArtifact {
type: 'path'
/** A path must always lie on a plane */
@ -53,18 +43,10 @@ export interface PathArtifactRich extends BaseArtifact {
/** A path must always contain 0 or more segments */
segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */
sweep?: SweepArtifact
sweep: SweepArtifact | null
codeRef: CodeRef
}
export interface SegmentArtifact extends BaseArtifact {
type: 'segment'
pathId: ArtifactId
surfaceId?: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CodeRef
}
interface SegmentArtifactRich extends BaseArtifact {
type: 'segment'
path: PathArtifact
@ -74,15 +56,6 @@ interface SegmentArtifactRich extends BaseArtifact {
codeRef: CodeRef
}
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
codeRef: CodeRef
}
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
@ -92,58 +65,6 @@ interface SweepArtifactRich extends BaseArtifact {
codeRef: CodeRef
}
interface WallArtifact extends BaseArtifact {
type: 'wall'
segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId
pathIds: Array<ArtifactId>
}
interface CapArtifact extends BaseArtifact {
type: 'cap'
subType: 'start' | 'end'
edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId
pathIds: Array<ArtifactId>
}
interface SweepEdge extends BaseArtifact {
type: 'sweepEdge'
segId: ArtifactId
sweepId: ArtifactId
subType: 'opposite' | 'adjacent'
}
/** A edgeCut is a more generic term for both fillet or chamfer */
interface EdgeCut extends BaseArtifact {
type: 'edgeCut'
subType: 'fillet' | 'chamfer'
consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId>
surfaceId?: ArtifactId
codeRef: CodeRef
}
interface EdgeCutEdge extends BaseArtifact {
type: 'edgeCutEdge'
edgeCutId: ArtifactId
surfaceId: ArtifactId
}
export type Artifact =
| PlaneArtifact
| PathArtifact
| SegmentArtifact
| SweepArtifact
| WallArtifact
| CapArtifact
| SweepEdge
| EdgeCut
| EdgeCutEdge
| solid2D
export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type']
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
@ -152,437 +73,6 @@ export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData
}
/** Creates a graph of artifacts from a list of ordered commands and their responses
* muting the Map should happen entirely this function, other functions called within
* should return data on how to update the map, and not do so directly.
*/
export function createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}: {
artifactCommands: Array<ArtifactCommand>
responseMap: ResponseMap
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}) {
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
for (const artifactCommand of artifactCommands) {
if (artifactCommand.command.type === 'enable_sketch_mode') {
currentPlaneId = artifactCommand.command.entity_id
}
if (artifactCommand.command.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand,
responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
execStateArtifacts,
})
artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact)
})
}
return myMap
}
/** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that
* but maybe types can be improved here.
*/
function mergeArtifacts(
oldArtifact: Artifact | undefined,
newArtifact: Artifact
): Artifact {
// only has string and array of strings
interface GenericArtifact {
[key: string]: string | Array<string>
}
if (!oldArtifact) return newArtifact
// merging artifacts of different types should never happen, but if it does, just return the new artifact
if (oldArtifact.type !== newArtifact.type) return newArtifact
const _oldArtifact = oldArtifact as any as GenericArtifact
const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact
Object.entries(newArtifact as any as GenericArtifact).forEach(
([propName, value]) => {
const otherValue = _oldArtifact[propName]
if (Array.isArray(value) && Array.isArray(otherValue)) {
mergedArtifact[propName] = [...new Set([...otherValue, ...value])]
}
}
)
return mergedArtifact as any as Artifact
}
/**
* Processes a single command and it's response in order to populate the artifact map
* It does not mutate the map directly, but returns an array of artifacts to update
*
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
* can remove this.
*/
export function getArtifactsToUpdate({
artifactCommand,
getArtifact,
responseMap,
currentPlaneId,
ast,
execStateArtifacts,
}: {
artifactCommand: ArtifactCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}): Array<{
id: ArtifactId
artifact: Artifact
}> {
const range = sourceRangeFromRust(artifactCommand.range)
const pathToNode = getNodePathFromSourceRange(ast, range)
const id = artifactCommand.cmdId
const response = responseMap[id]
const cmd = artifactCommand.command
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) {
// If we're calling `make_plane` and the code range doesn't end at `0`
// it's not a default plane, but a custom one from the offsetPlane standard library function
return [
{
id,
artifact: {
type: 'plane',
id,
pathIds: [],
codeRef: { range, pathToNode },
},
},
]
} else if (cmd.type === 'enable_sketch_mode') {
const plane = getArtifact(currentPlaneId)
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
const codeRef =
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
const existingPlane = getArtifact(currentPlaneId)
if (existingPlane?.type === 'wall') {
return [
{
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: existingPlane.segId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
},
},
]
} else {
return [
{
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef },
},
]
}
} else if (cmd.type === 'start_path') {
returnArr.push({
id,
artifact: {
type: 'path',
id,
segIds: [],
planeId: currentPlaneId,
sweepId: undefined,
codeRef: { range, pathToNode },
},
})
const plane = getArtifact(currentPlaneId)
const codeRef =
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
if (plane?.type === 'plane') {
returnArr.push({
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef },
})
}
if (plane?.type === 'wall') {
returnArr.push({
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: plane.segId,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId,
pathIds: [id],
},
})
}
return returnArr
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id
returnArr.push({
id,
artifact: {
type: 'segment',
id,
pathId,
surfaceId: undefined,
edgeIds: [],
codeRef: { range, pathToNode },
},
})
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
artifact: { ...path, segIds: [id] },
})
if (
response?.type === 'modeling' &&
response.data.modeling_response.type === 'close_path'
) {
returnArr.push({
id: response.data.modeling_response.data.face_id,
artifact: {
type: 'solid2D',
id: response.data.modeling_response.data.face_id,
pathId,
},
})
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
artifact: {
...path,
solid2dId: response.data.modeling_response.data.face_id,
},
})
}
return returnArr
} else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: subType,
id,
pathId: cmd.target,
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
const path = getArtifact(cmd.target)
if (path?.type === 'path')
returnArr.push({
id: cmd.target,
artifact: { ...path, sweepId: id },
})
return returnArr
} else if (
cmd.type === 'loft' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'loft'
) {
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: 'loft',
id,
// TODO: make sure to revisit this choice, don't think it matters for now
pathId: cmd.section_ids[0],
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
for (const sectionId of cmd.section_ids) {
const path = getArtifact(sectionId)
if (path?.type === 'path')
returnArr.push({
id: sectionId,
artifact: { ...path, sweepId: id },
})
}
return returnArr
} else if (
cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' &&
response.data.modeling_response.type === 'solid3d_get_extrusion_face_info'
) {
let lastPath: PathArtifact
response.data.modeling_response.data.faces.forEach(
({ curve_id, cap, face_id }) => {
if (cap === 'none' && curve_id && face_id) {
const seg = getArtifact(curve_id)
if (seg?.type !== 'segment') return
const path = getArtifact(seg.pathId)
if (path?.type === 'path' && seg?.type === 'segment') {
lastPath = path
returnArr.push({
id: face_id,
artifact: {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
returnArr.push({
id: curve_id,
artifact: { ...seg, surfaceId: face_id },
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type === 'sweep') {
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
}
}
)
response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => {
if ((cap === 'top' || cap === 'bottom') && face_id) {
const path = lastPath
if (path?.type === 'path') {
returnArr.push({
id: face_id,
artifact: {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
})
return returnArr
} else if (
// is opposite edge
(cmd.type === 'solid3d_get_opposite_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
response.data.modeling_response.data.edge) ||
// or is adjacent edge
(cmd.type === 'solid3d_get_next_adjacent_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type ===
'solid3d_get_next_adjacent_edge' &&
response.data.modeling_response.data.edge)
) {
const wall = getArtifact(cmd.face_id)
if (wall?.type !== 'wall') return returnArr
const sweep = getArtifact(wall.sweepId)
if (sweep?.type !== 'sweep') return returnArr
const path = getArtifact(sweep.pathId)
if (path?.type !== 'path') return returnArr
const segment = getArtifact(cmd.edge_id)
if (segment?.type !== 'segment') return returnArr
return [
{
id: response.data.modeling_response.data.edge,
artifact: {
type: 'sweepEdge',
id: response.data.modeling_response.data.edge,
subType:
cmd.type === 'solid3d_get_next_adjacent_edge'
? 'adjacent'
: 'opposite',
segId: cmd.edge_id,
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
},
},
{
id: cmd.edge_id,
artifact: {
...segment,
edgeIds: [response.data.modeling_response.data.edge],
},
},
{
id: sweep.id,
artifact: {
...sweep,
edgeIds: [response.data.modeling_response.data.edge],
},
},
]
} else if (cmd.type === 'solid3d_fillet_edge') {
returnArr.push({
id,
artifact: {
type: 'edgeCut',
id,
subType: cmd.cut_type,
consumedEdgeId: cmd.edge_id,
edgeIds: [],
surfaceId: undefined,
codeRef: { range, pathToNode },
},
})
const consumedEdge = getArtifact(cmd.edge_id)
if (consumedEdge?.type === 'segment') {
returnArr.push({
id: cmd.edge_id,
artifact: { ...consumedEdge, edgeCutId: id },
})
}
return returnArr
}
return []
}
/** filter map items of a specific type */
export function filterArtifacts<T extends Artifact['type'][]>(
{
@ -676,7 +166,7 @@ export function expandPath(
},
artifactGraph
)
: undefined
: null
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall'] },
artifactGraph
@ -778,11 +268,11 @@ export function getCapCodeRef(
}
export function getSolid2dCodeRef(
solid2D: solid2D,
solid2d: Solid2D,
artifactGraph: ArtifactGraph
): CodeRef | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
{ key: solid2d.pathId, types: ['path'] },
artifactGraph
)
if (err(path)) return path
@ -881,7 +371,7 @@ export function getCodeRefsByArtifactId(
artifactGraph: ArtifactGraph
): Array<CodeRef> | null {
const artifact = artifactGraph.get(id)
if (artifact?.type === 'solid2D') {
if (artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return [codeRef]

View File

@ -1,9 +1,7 @@
import {
ArtifactCommand,
defaultRustSourceRange,
ArtifactGraph,
defaultSourceRange,
ExecState,
Program,
RustSourceRange,
SourceRange,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
@ -17,12 +15,7 @@ import {
darkModeMatcher,
} from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
ArtifactGraph,
EngineCommand,
ResponseMap,
createArtifactGraph,
} from 'lang/std/artifactGraph'
import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast'
@ -36,7 +29,6 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -1022,6 +1014,11 @@ class EngineConnection extends EventTarget {
this.pingPongSpan.pong = new Date()
break
case 'modeling_session_data':
let api_call_id = resp.data?.session?.api_call_id
console.log(`API Call ID: ${api_call_id}`)
break
// Only fires on successful authentication.
case 'ice_server_info':
let ice_servers = resp.data?.ice_servers
@ -1309,8 +1306,8 @@ export enum EngineCommandManagerEvents {
interface PendingMessage {
command: EngineCommand
range: RustSourceRange
idToRangeMap: { [key: string]: RustSourceRange }
range: SourceRange
idToRangeMap: { [key: string]: SourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]>
@ -1994,7 +1991,7 @@ export class EngineCommandManager extends EventTarget {
{
command,
idToRangeMap: {},
range: defaultRustSourceRange(),
range: defaultSourceRange(),
},
true // isSceneCommand
)
@ -2025,9 +2022,9 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined'))
const range: RustSourceRange = JSON.parse(rangeStr)
const range: SourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: RustSourceRange } =
const idToRangeMap: { [key: string]: SourceRange } =
JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
@ -2087,17 +2084,8 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(
ast: Node<Program>,
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({
artifactCommands,
responseMap: this.responseMap,
ast,
execStateArtifacts,
})
updateArtifactGraph(execStateArtifactGraph: ExecState['artifactGraph']) {
this.artifactGraph = execStateArtifactGraph
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null)

View File

@ -11,8 +11,8 @@ import {
assertParse,
recast,
initPromise,
SourceRange,
CallExpression,
topLevelRange,
} from '../wasm'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { enginelessExecutor } from '../../lib/testHelpers'
@ -124,7 +124,10 @@ describe('testing changeSketchArguments', () => {
execState.memory,
{
type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length, true],
sourceRange: topLevelRange(
sourceStart,
sourceStart + lineToChange.length
),
},
{
type: 'straight-segment',
@ -219,11 +222,10 @@ describe('testing addTagForSketchOnFace', () => {
const ast = assertParse(code)
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalLine)
const sourceRange: [number, number, boolean] = [
const sourceRange = topLevelRange(
sourceStart,
sourceStart + originalLine.length,
true,
]
sourceStart + originalLine.length
)
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace(
@ -292,11 +294,10 @@ ${insertCode}
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalChamfer)
const extraChars = originalChamfer.indexOf('chamfer')
const sourceRange: [number, number, boolean] = [
const sourceRange = topLevelRange(
sourceStart + extraChars,
sourceStart + originalChamfer.length - extraChars,
true,
]
sourceStart + originalChamfer.length - extraChars
)
if (err(ast)) throw ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
@ -357,7 +358,6 @@ describe('testing getConstraintInfo', () => {
offset = 0
}, %)
|> tangentialArcTo([3.14, 13.14], %)`
const ast = assertParse(code)
test.each([
[
'line',
@ -366,7 +366,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3',
sourceRange: [78, 79, true],
sourceRange: topLevelRange(78, 79),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -375,7 +375,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '4',
sourceRange: [81, 82, true],
sourceRange: topLevelRange(81, 82),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -389,7 +389,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [118, 122, true],
sourceRange: topLevelRange(118, 122),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -398,7 +398,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: false,
value: '3.14',
sourceRange: [137, 141, true],
sourceRange: topLevelRange(137, 141),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -412,7 +412,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '6.14',
sourceRange: [164, 168, true],
sourceRange: topLevelRange(164, 168),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -421,7 +421,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '3.14',
sourceRange: [170, 174, true],
sourceRange: topLevelRange(170, 174),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -435,7 +435,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLineTo',
sourceRange: [185, 192, true],
sourceRange: topLevelRange(185, 192),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -444,7 +444,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '8',
sourceRange: [193, 194, true],
sourceRange: topLevelRange(193, 194),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -458,7 +458,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLineTo',
sourceRange: [204, 211, true],
sourceRange: topLevelRange(204, 211),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -467,7 +467,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '5',
sourceRange: [212, 213, true],
sourceRange: topLevelRange(212, 213),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -481,7 +481,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLine',
sourceRange: [223, 228, true],
sourceRange: topLevelRange(223, 228),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -490,7 +490,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3.14',
sourceRange: [229, 233, true],
sourceRange: topLevelRange(229, 233),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -504,7 +504,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLine',
sourceRange: [247, 252, true],
sourceRange: topLevelRange(247, 252),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -513,7 +513,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [253, 257, true],
sourceRange: topLevelRange(253, 257),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -527,7 +527,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [301, 305, true],
sourceRange: topLevelRange(301, 305),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -536,7 +536,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [320, 324, true],
sourceRange: topLevelRange(320, 324),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -550,7 +550,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [373, 375, true],
sourceRange: topLevelRange(373, 375),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -559,7 +559,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3',
sourceRange: [390, 391, true],
sourceRange: topLevelRange(390, 391),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -573,7 +573,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '12.14',
sourceRange: [434, 439, true],
sourceRange: topLevelRange(434, 439),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -582,7 +582,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '12',
sourceRange: [450, 452, true],
sourceRange: topLevelRange(450, 452),
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -596,7 +596,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [495, 497, true],
sourceRange: topLevelRange(495, 497),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -605,7 +605,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '10.14',
sourceRange: [508, 513, true],
sourceRange: topLevelRange(508, 513),
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -619,7 +619,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [567, 571, true],
sourceRange: topLevelRange(567, 571),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -628,7 +628,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset',
isConstrained: false,
value: '0',
sourceRange: [608, 609, true],
sourceRange: topLevelRange(608, 609),
argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -637,7 +637,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionTag',
isConstrained: false,
value: 'a',
sourceRange: [592, 593, true],
sourceRange: topLevelRange(592, 593),
argPosition: {
key: 'intersectTag',
type: 'objectProperty',
@ -654,7 +654,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious',
isConstrained: true,
value: 'tangentialArcTo',
sourceRange: [623, 638, true],
sourceRange: topLevelRange(623, 638),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -663,7 +663,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '3.14',
sourceRange: [640, 644, true],
sourceRange: topLevelRange(640, 644),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -672,7 +672,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '13.14',
sourceRange: [646, 651, true],
sourceRange: topLevelRange(646, 651),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -680,11 +680,11 @@ describe('testing getConstraintInfo', () => {
],
],
])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [
const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
code.indexOf(functionName) + functionName.length
)
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>(
@ -717,7 +717,6 @@ describe('testing getConstraintInfo', () => {
offset = 0
}, %)
|> tangentialArcTo([3.14, 13.14], %)`
const ast = assertParse(code)
test.each([
[
`angledLine(`,
@ -726,7 +725,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [112, 116, true],
sourceRange: topLevelRange(112, 116),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -735,7 +734,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: false,
value: '3.14',
sourceRange: [118, 122, true],
sourceRange: topLevelRange(118, 122),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -749,7 +748,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '3.14',
sourceRange: [277, 281, true],
sourceRange: topLevelRange(277, 281),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -758,7 +757,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: false,
value: '3.14',
sourceRange: [283, 287, true],
sourceRange: topLevelRange(283, 287),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -772,7 +771,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [321, 323, true],
sourceRange: topLevelRange(321, 323),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -781,7 +780,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: false,
value: '3',
sourceRange: [325, 326, true],
sourceRange: topLevelRange(325, 326),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -795,7 +794,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '12',
sourceRange: [354, 356, true],
sourceRange: topLevelRange(354, 356),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -804,7 +803,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: false,
value: '12',
sourceRange: [358, 360, true],
sourceRange: topLevelRange(358, 360),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -818,7 +817,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: false,
value: '30',
sourceRange: [388, 390, true],
sourceRange: topLevelRange(388, 390),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -827,7 +826,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: false,
value: '10',
sourceRange: [392, 394, true],
sourceRange: topLevelRange(392, 394),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -835,11 +834,11 @@ describe('testing getConstraintInfo', () => {
],
],
])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [
const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
code.indexOf(functionName) + functionName.length
)
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>(
@ -872,7 +871,6 @@ describe('testing getConstraintInfo', () => {
offset = 0 + 0
}, %)
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
const ast = assertParse(code)
test.each([
[
'line',
@ -881,7 +879,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3 + 0',
sourceRange: [83, 88, true],
sourceRange: topLevelRange(83, 88),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -890,7 +888,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '4 + 0',
sourceRange: [90, 95, true],
sourceRange: topLevelRange(90, 95),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'line',
@ -904,7 +902,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [129, 137, true],
sourceRange: topLevelRange(129, 137),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -913,7 +911,7 @@ describe('testing getConstraintInfo', () => {
type: 'length',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [148, 156, true],
sourceRange: topLevelRange(148, 156),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLine',
@ -927,7 +925,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '6.14 + 0',
sourceRange: [178, 186, true],
sourceRange: topLevelRange(178, 186),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -936,7 +934,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [188, 196, true],
sourceRange: topLevelRange(188, 196),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'lineTo',
@ -950,7 +948,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLineTo',
sourceRange: [209, 216, true],
sourceRange: topLevelRange(209, 216),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -959,7 +957,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '8 + 0',
sourceRange: [217, 222, true],
sourceRange: topLevelRange(217, 222),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo',
@ -973,7 +971,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLineTo',
sourceRange: [234, 241, true],
sourceRange: topLevelRange(234, 241),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -982,7 +980,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '5 + 0',
sourceRange: [242, 247, true],
sourceRange: topLevelRange(242, 247),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo',
@ -996,7 +994,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical',
isConstrained: true,
value: 'yLine',
sourceRange: [259, 264, true],
sourceRange: topLevelRange(259, 264),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -1005,7 +1003,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [265, 273, true],
sourceRange: topLevelRange(265, 273),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'yLine',
@ -1019,7 +1017,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal',
isConstrained: true,
value: 'xLine',
sourceRange: [289, 294, true],
sourceRange: topLevelRange(289, 294),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -1028,7 +1026,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [295, 303, true],
sourceRange: topLevelRange(295, 303),
argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array),
stdLibFnName: 'xLine',
@ -1042,7 +1040,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [345, 353, true],
sourceRange: topLevelRange(345, 353),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -1051,7 +1049,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [364, 372, true],
sourceRange: topLevelRange(364, 372),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength',
@ -1065,7 +1063,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '30 + 0',
sourceRange: [416, 422, true],
sourceRange: topLevelRange(416, 422),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -1074,7 +1072,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative',
isConstrained: true,
value: '3 + 0',
sourceRange: [433, 438, true],
sourceRange: topLevelRange(433, 438),
argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength',
@ -1088,7 +1086,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '12.14 + 0',
sourceRange: [476, 485, true],
sourceRange: topLevelRange(476, 485),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -1097,7 +1095,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '12 + 0',
sourceRange: [492, 498, true],
sourceRange: topLevelRange(492, 498),
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX',
@ -1111,7 +1109,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '30 + 0',
sourceRange: [536, 542, true],
sourceRange: topLevelRange(536, 542),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -1120,7 +1118,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '10.14 + 0',
sourceRange: [549, 558, true],
sourceRange: topLevelRange(549, 558),
argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY',
@ -1134,7 +1132,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [616, 624, true],
sourceRange: topLevelRange(616, 624),
argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1143,7 +1141,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset',
isConstrained: true,
value: '0 + 0',
sourceRange: [671, 676, true],
sourceRange: topLevelRange(671, 676),
argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1152,7 +1150,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionTag',
isConstrained: false,
value: 'a',
sourceRange: [650, 651, true],
sourceRange: topLevelRange(650, 651),
argPosition: { key: 'intersectTag', type: 'objectProperty' },
pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects',
@ -1166,7 +1164,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious',
isConstrained: true,
value: 'tangentialArcTo',
sourceRange: [697, 712, true],
sourceRange: topLevelRange(697, 712),
argPosition: undefined,
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1175,7 +1173,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute',
isConstrained: true,
value: '3.14 + 0',
sourceRange: [714, 722, true],
sourceRange: topLevelRange(714, 722),
argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1184,7 +1182,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute',
isConstrained: true,
value: '13.14 + 0',
sourceRange: [724, 733, true],
sourceRange: topLevelRange(724, 733),
argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo',
@ -1192,11 +1190,11 @@ describe('testing getConstraintInfo', () => {
],
],
])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [
const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName),
code.indexOf(functionName) + functionName.length,
true,
]
code.indexOf(functionName) + functionName.length
)
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>(

View File

@ -12,6 +12,7 @@ import {
VariableDeclaration,
Identifier,
sketchFromKclValue,
topLevelRange,
} from 'lang/wasm'
import {
getNodeFromPath,
@ -222,7 +223,7 @@ const commonConstraintInfoHelper = (
code.slice(input1.start, input1.end),
stdLibFnName,
isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput,
[input1.start, input1.end, true],
topLevelRange(input1.start, input1.end),
pathToFirstArg
)
)
@ -234,7 +235,7 @@ const commonConstraintInfoHelper = (
code.slice(input2.start, input2.end),
stdLibFnName,
isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput,
[input2.start, input2.end, true],
topLevelRange(input2.start, input2.end),
pathToSecondArg
)
)
@ -266,7 +267,7 @@ const horzVertConstraintInfoHelper = (
callee.name,
stdLibFnName,
undefined,
[callee.start, callee.end, true],
topLevelRange(callee.start, callee.end),
pathToCallee
),
constrainInfo(
@ -275,7 +276,7 @@ const horzVertConstraintInfoHelper = (
code.slice(firstArg.start, firstArg.end),
stdLibFnName,
abbreviatedInput,
[firstArg.start, firstArg.end, true],
topLevelRange(firstArg.start, firstArg.end),
pathToFirstArg
),
]
@ -905,7 +906,7 @@ export const tangentialArcTo: SketchLineHelper = {
callee.name,
'tangentialArcTo',
undefined,
[callee.start, callee.end, true],
topLevelRange(callee.start, callee.end),
pathToCallee
),
constrainInfo(
@ -914,7 +915,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[0].start, firstArg.elements[0].end),
'tangentialArcTo',
0,
[firstArg.elements[0].start, firstArg.elements[0].end, true],
topLevelRange(firstArg.elements[0].start, firstArg.elements[0].end),
pathToFirstArg
),
constrainInfo(
@ -923,7 +924,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[1].start, firstArg.elements[1].end),
'tangentialArcTo',
1,
[firstArg.elements[1].start, firstArg.elements[1].end, true],
topLevelRange(firstArg.elements[1].start, firstArg.elements[1].end),
pathToSecondArg
),
]
@ -1052,7 +1053,7 @@ export const circle: SketchLineHelper = {
code.slice(radiusDetails.expr.start, radiusDetails.expr.end),
'circle',
'radius',
[radiusDetails.expr.start, radiusDetails.expr.end, true],
topLevelRange(radiusDetails.expr.start, radiusDetails.expr.end),
pathToRadiusLiteral
),
{
@ -1061,11 +1062,10 @@ export const circle: SketchLineHelper = {
isConstrained: isNotLiteralArrayOrStatic(
centerDetails.expr.elements[0]
),
sourceRange: [
sourceRange: topLevelRange(
centerDetails.expr.elements[0].start,
centerDetails.expr.elements[0].end,
true,
],
centerDetails.expr.elements[0].end
),
pathToNode: pathToXArg,
value: code.slice(
centerDetails.expr.elements[0].start,
@ -1083,11 +1083,10 @@ export const circle: SketchLineHelper = {
isConstrained: isNotLiteralArrayOrStatic(
centerDetails.expr.elements[1]
),
sourceRange: [
sourceRange: topLevelRange(
centerDetails.expr.elements[1].start,
centerDetails.expr.elements[1].end,
true,
],
centerDetails.expr.elements[1].end
),
pathToNode: pathToYArg,
value: code.slice(
centerDetails.expr.elements[1].start,
@ -1763,7 +1762,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(angle.start, angle.end),
'angledLineThatIntersects',
'angle',
[angle.start, angle.end, true],
topLevelRange(angle.start, angle.end),
pathToAngleProp
)
)
@ -1782,7 +1781,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(offset.start, offset.end),
'angledLineThatIntersects',
'offset',
[offset.start, offset.end, true],
topLevelRange(offset.start, offset.end),
pathToOffsetProp
)
)
@ -1801,7 +1800,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(tag.start, tag.end),
'angledLineThatIntersects',
'intersectTag',
[tag.start, tag.end, true],
topLevelRange(tag.start, tag.end),
pathToTagProp
)
returnVal.push(info)

View File

@ -5,6 +5,7 @@ import {
initPromise,
sketchFromKclValue,
SourceRange,
topLevelRange,
} from '../wasm'
import {
ConstraintType,
@ -31,10 +32,10 @@ async function testingSwapSketchFnCall({
constraintType: ConstraintType
}): Promise<{
newCode: string
originalRange: [number, number, boolean]
originalRange: SourceRange
}> {
const startIndex = inputCode.indexOf(callToSwap)
const range: SourceRange = [startIndex, startIndex + callToSwap.length, true]
const range = topLevelRange(startIndex, startIndex + callToSwap.length)
const ast = assertParse(inputCode)
const execState = await enginelessExecutor(ast)
@ -375,7 +376,10 @@ part001 = startSketchOn('XY')
execState.memory.get('part001'),
'part001'
) as Sketch
const _segment = getSketchSegmentFromSourceRange(sg, [index, index, true])
const _segment = getSketchSegmentFromSourceRange(
sg,
topLevelRange(index, index)
)
if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment
expect(segment).toEqual({
@ -390,7 +394,7 @@ part001 = startSketchOn('XY')
const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange(
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
[index, index, true]
topLevelRange(index, index)
)
if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment

View File

@ -9,6 +9,7 @@ import {
Path,
PathToNode,
Expr,
topLevelRange,
} from '../wasm'
import { err } from 'lib/trap'
@ -31,7 +32,7 @@ export function getSketchSegmentFromPathToNode(
const node = nodeMeta.node
if (!node || typeof node.start !== 'number' || !node.end)
return new Error('no node found')
const sourceRange: SourceRange = [node.start, node.end, true]
const sourceRange = topLevelRange(node.start, node.end)
return getSketchSegmentFromSourceRange(sketch, sourceRange)
}
export function getSketchSegmentFromSourceRange(

View File

@ -1,4 +1,11 @@
import { assertParse, Expr, recast, initPromise, Program } from '../wasm'
import {
assertParse,
Expr,
recast,
initPromise,
Program,
topLevelRange,
} from '../wasm'
import {
getConstraintType,
getTransformInfos,
@ -125,7 +132,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number, boolean] = [start, start, true]
const range = topLevelRange(start, start)
return {
codeRef: codeRefFromRange(range, ast),
}
@ -297,7 +304,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start, true], ast),
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
}
})
@ -386,7 +393,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start, true], ast),
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
}
})
@ -446,7 +453,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start, true], ast),
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
}
})
@ -541,7 +548,7 @@ async function helperThing(
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
return {
codeRef: codeRefFromRange([start, start, true], ast),
codeRef: codeRefFromRange(topLevelRange(start, start), ast),
}
})
@ -610,7 +617,7 @@ part001 = startSketchOn('XY')
}
const offsetIndex = index - 7
const expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex, true],
topLevelRange(offsetIndex, offsetIndex),
ast
)
if (err(expectedConstraintLevel)) {

View File

@ -20,12 +20,12 @@ import {
sketchFromKclValue,
Literal,
SourceRange,
LiteralValue,
} from '../wasm'
import {
getNodeFromPath,
getNodeFromPathCurry,
getNodePathFromSourceRange,
isValueZero,
} from '../queryAst'
import {
createArrayExpression,
@ -79,11 +79,32 @@ export type ConstraintType =
| 'setAngleBetween'
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
function asNum(val: LiteralValue): number | Error {
if (typeof val === 'object') return val.value
return REF_NUM_ERR
}
function forceNum(arg: Literal): number {
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
return Number(arg.value)
} else {
return arg.value.value
}
}
function isUndef(val: any): val is undefined {
return typeof val === 'undefined'
}
function isNum(val: any): val is number {
return typeof val === 'number'
function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && forceNum(val) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
function createCallWrapper(
@ -190,7 +211,7 @@ const xyLineSetLength =
: referenceSeg
? segRef
: args[0].expr
const literalARg = getArgLiteralVal(args[0].expr)
const literalARg = asNum(args[0].expr.value)
if (err(literalARg)) return literalARg
return createCallWrapper(xOrY, lineVal, tag, literalARg)
}
@ -211,13 +232,14 @@ const basicAngledLineCreateNode =
referencedSegment: path,
}) => {
const refAng = path ? getAngle(path?.from, path?.to) : 0
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const argValue = asNum(args[0].expr.value)
if (err(argValue)) return argValue
const nonForcedAng =
varValToUse === 'ang'
? inputs[0].expr
: referenceSeg === 'ang'
? getClosesAngleDirection(
args[0].expr.value,
argValue,
refAng,
createSegAngle(referenceSegName)
)
@ -230,8 +252,8 @@ const basicAngledLineCreateNode =
: args[1].expr
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
const literalArg = getArgLiteralVal(
valToForce === 'ang' ? args[0].expr : args[1].expr
const literalArg = asNum(
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
)
if (err(literalArg)) return literalArg
return createCallWrapper(
@ -283,7 +305,7 @@ const getMinAndSegAngVals = (
}
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
Number(arg.value) < 0 ? createUnaryExpression(legLenVal) : legLenVal
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal
const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
@ -322,8 +344,7 @@ const setHorzVertDistanceCreateNode =
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return literalArg
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
@ -352,7 +373,7 @@ const setHorzVertDistanceForAngleLineCreateNode =
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[1].expr)
const literalArg = asNum(args?.[1].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const binExp = createBinaryExpressionWithUnary([
@ -374,8 +395,8 @@ const setAbsDistanceCreateNode =
index = xOrY === 'x' ? 0 : 1
): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
const literalArg = getArgLiteralVal(args?.[index].expr)
if (err(literalArg)) return REF_NUM_ERR
const literalArg = asNum(args?.[index].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
if (isXOrYLine) {
@ -396,8 +417,8 @@ const setAbsDistanceCreateNode =
const setAbsDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
const literalArg = getArgLiteralVal(args?.[1].expr)
if (err(literalArg)) return REF_NUM_ERR
const literalArg = asNum(args?.[1].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
return createCallWrapper(
@ -419,7 +440,7 @@ const setHorVertDistanceForXYLines =
}) => {
const index = xOrY === 'x' ? 0 : 1
const refNum = referencedSegment?.to?.[index]
const literalArg = getArgLiteralVal(args?.[index].expr)
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const makeBinExp = createBinaryExpressionWithUnary([
@ -445,9 +466,9 @@ const setHorzVertDistanceConstraintLineCreateNode =
])
const makeBinExp = (index: 0 | 1) => {
const arg = getArgLiteralVal(args?.[index].expr)
const arg = asNum(args?.[index].expr.value)
const refNum = referencedSegment?.to?.[index]
if (err(arg) || !isNum(refNum)) return REF_NUM_ERR
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR
return createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, isX),
createLiteral(roundOff(arg - refNum, 2)),
@ -468,9 +489,9 @@ const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = args[1].expr.value,
angle = args[0].expr.value
if (!isNum(val) || !isNum(angle)) return REF_NUM_ERR
const val = asNum(args[1].expr.value),
angle = asNum(args[0].expr.value)
if (err(val) || err(angle)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2)
const varNamMap: { [key: number]: string } = {
0: 'ZERO',
@ -498,8 +519,8 @@ const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
inputs,
rawArgs: args,
}) => {
const val = args[1].expr.value
if (!isNum(val)) return REF_NUM_ERR
const val = asNum(args[1].expr.value)
if (err(val)) return val
const valueUsedInTransform = roundOff(val, 2)
return intersectCallWrapper({
fnName: 'angledLineThatIntersects',
@ -524,8 +545,8 @@ const setAngleBetweenCreateNode =
const refAngle = referencedSegment
? getAngle(referencedSegment?.from, referencedSegment?.to)
: 0
const val = args[0].expr.value
if (!isNum(val)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
let firstHalfValue = createSegAngle(referenceSegName)
if (Math.abs(valueUsedInTransform) > 90) {
@ -706,13 +727,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[0].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
tag
)
},
@ -739,13 +758,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthYCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
tag
)
},
@ -763,7 +780,7 @@ const transformMap: TransformMap = {
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = getArgLiteralVal(args[0].expr)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
@ -844,7 +861,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -856,7 +873,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -900,10 +917,11 @@ const transformMap: TransformMap = {
referenceSegName,
getInputOfType(inputs, 'xRelative').expr
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(args[0].expr.value, legAngle), minVal],
[getLegAng(val, legAngle), minVal],
tag
)
},
@ -912,7 +930,7 @@ const transformMap: TransformMap = {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
@ -953,10 +971,11 @@ const transformMap: TransformMap = {
inputs[1].expr,
'legAngY'
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(args[0].expr.value, legAngle), minVal],
[getLegAng(val, legAngle), minVal],
tag
)
},
@ -965,7 +984,7 @@ const transformMap: TransformMap = {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (Number(args[0].expr.value) >= 0)
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
@ -1005,13 +1024,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
@ -1057,13 +1074,11 @@ const transformMap: TransformMap = {
createPipeSubstitution(),
]
)
if (!isNum(args[0].expr.value)) return REF_NUM_ERR
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[
getAngleLengthSign(args[0].expr.value, angleToMatchLengthXCall),
inputs[1].expr,
],
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
@ -1080,7 +1095,7 @@ const transformMap: TransformMap = {
equalLength: {
tooltip: 'xLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = getArgLiteralVal(args[0].expr)
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
const segLen = createSegLen(referenceSegName)
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
@ -1118,7 +1133,7 @@ const transformMap: TransformMap = {
equalLength: {
tooltip: 'yLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = getArgLiteralVal(args[0].expr)
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
let segLen = createSegLen(referenceSegName)
if (argVal < 0) segLen = createUnaryExpression(segLen)
@ -1714,7 +1729,7 @@ export function transformAstSketchLines({
let kclVal = programMemory.get(varName)
let sketch
if (kclVal?.type === 'Solid') {
sketch = kclVal.sketch
sketch = kclVal.value.sketch
} else {
sketch = sketchFromKclValue(kclVal, varName)
if (err(sketch)) {
@ -1823,11 +1838,6 @@ function createLastSeg(isX: boolean): Node<CallExpression> {
])
}
function getArgLiteralVal(arg: Literal): number | Error {
if (!isNum(arg.value)) return REF_NUM_ERR
return arg.value
}
export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange(

View File

@ -5,8 +5,9 @@ import {
Literal,
ArrayExpression,
BinaryExpression,
ArtifactGraph,
} from './wasm'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap(

View File

@ -44,17 +44,30 @@ import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from './queryAst'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
export type { Cap as CapArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { CodeRef } from 'wasm-lib/kcl/bindings/Artifact'
export type { EdgeCut } from 'wasm-lib/kcl/bindings/Artifact'
export type { Path as PathArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Plane as PlaneArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Segment as SegmentArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Solid2d as Solid2dArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Sweep as SweepArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { SweepEdge } from 'wasm-lib/kcl/bindings/Artifact'
export type { Wall as WallArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -76,7 +89,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type SyntaxType =
| 'Program'
@ -105,35 +118,36 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* The first two items are the start and end points (byte offsets from the start of the file).
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
*/
export type SourceRange = [number, number, boolean]
/**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend).
*/
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0]
export function sourceRangeFromRust(s: SourceRange): SourceRange {
return [s[0], s[1], s[2]]
}
/**
* Create a default SourceRange for testing or as a placeholder.
*/
export function defaultSourceRange(): SourceRange {
return [0, 0, true]
return [0, 0, 0]
}
/**
* Create a default RustSourceRange for testing or as a placeholder.
* Create a SourceRange for the top-level module.
*/
export function defaultRustSourceRange(): RustSourceRange {
return [0, 0, 0]
export function topLevelRange(start: number, end: number): SourceRange {
return [start, end, 0]
}
/**
* Returns true if this source range is from the file being executed. Returns
* false if it's from a file that was imported.
*/
export function isTopLevelModule(range: SourceRange): boolean {
return range[2] === 0
}
export const wasmUrl = () => {
@ -234,7 +248,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
}
}
@ -258,8 +273,9 @@ export const isPathToNodeNumber = (
export interface ExecState {
memory: ProgramMemory
operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
artifacts: { [key in ArtifactId]?: RustArtifact }
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
}
/**
@ -272,18 +288,53 @@ export function emptyExecState(): ExecState {
operations: [],
artifacts: {},
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
}
}
function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
function execStateFromRust(
execOutcome: RustExecOutcome,
program: Node<Program>
): ExecState {
const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph)
// We haven't ported pathToNode logic to Rust yet, so we need to fill it in.
for (const [id, artifact] of artifactGraph) {
if (!artifact) continue
if (!('codeRef' in artifact)) continue
const pathToNode = getNodePathFromSourceRange(
program,
sourceRangeFromRust(artifact.codeRef.range)
)
artifact.codeRef.pathToNode = pathToNode
}
return {
memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
artifactGraph,
}
}
export type ArtifactGraph = Map<ArtifactId, Artifact>
function rustArtifactGraphToMap(
rustArtifactGraph: RustArtifactGraph
): ArtifactGraph {
const map = new Map<ArtifactId, Artifact>()
for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) {
if (!artifact) continue
map.set(id, artifact)
}
return map
}
export function defaultArtifactGraph(): ArtifactGraph {
return new Map()
}
interface Memory {
[key: string]: KclValue | undefined
}
@ -488,7 +539,8 @@ export function sketchFromKclValueOptional(
): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Solid') return obj.sketch
if (obj?.type === 'Sketch') return obj.value
if (obj?.type === 'Solid') return obj.value.sketch
if (!varName) {
varName = 'a KCL value'
}
@ -543,7 +595,7 @@ export const executor = async (
engineCommandManager,
fileSystemManager
)
return execStateFromRust(execOutcome)
return execStateFromRust(execOutcome, node)
} catch (e: any) {
console.log(e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
@ -552,7 +604,8 @@ export const executor = async (
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations,
parsed.artifactCommands
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
)
return Promise.reject(kclError)
@ -613,7 +666,8 @@ export const modifyAstForSketch = async (
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
console.log(kclError)
@ -683,7 +737,8 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
}
}

40
src/lib/base64.test.ts Normal file
View File

@ -0,0 +1,40 @@
import { expect } from 'vitest'
import { base64ToString, stringToBase64 } from './base64'
describe('base64 encoding', () => {
test('to base64, simple code', async () => {
const code = `extrusionDistance = 12`
// Generated by online tool
const expectedBase64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
test(`to base64, code with UTF-8 characters`, async () => {
// example adapted from MDN docs: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const code = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
// Generated by online tool
const expectedBase64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
// The following are simply the reverse of the above tests
test('from base64, simple code', async () => {
const base64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const expectedCode = `extrusionDistance = 12`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
test(`from base64, code with UTF-8 characters`, async () => {
const base64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const expectedCode = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
})

29
src/lib/base64.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* Converts a string to a base64 string, preserving the UTF-8 encoding
*/
export function stringToBase64(str: string) {
return bytesToBase64(new TextEncoder().encode(str))
}
/**
* Converts a base64 string to a string, preserving the UTF-8 encoding
*/
export function base64ToString(base64: string) {
return new TextDecoder().decode(base64ToBytes(base64))
}
/**
* From the MDN Web Docs
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
*/
function base64ToBytes(base64: string) {
const binString = atob(base64)
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
}
function bytesToBase64(bytes: Uint8Array) {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('')
return btoa(binString)
}

View File

@ -9,7 +9,11 @@ import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
import { loftValidator, revolveAxisValidator } from './validators'
import {
loftValidator,
revolveAxisValidator,
shellValidator,
} from './validators'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
@ -276,7 +280,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
selection: {
inputType: 'selection',
selectionTypes: ['solid2D', 'segment'],
selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection
required: true,
skip: true,
@ -308,7 +312,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
profile: {
inputType: 'selection',
selectionTypes: ['solid2D'],
selectionTypes: ['solid2d'],
required: true,
skip: true,
multiple: false,
@ -333,7 +337,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
selection: {
inputType: 'selection',
selectionTypes: ['solid2D'],
selectionTypes: ['solid2d'],
multiple: true,
required: true,
skip: false,
@ -351,12 +355,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
skip: false,
validation: shellValidator,
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
// TODO: add dry-run validation on thickness param
},
},
},
@ -368,7 +373,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
selection: {
inputType: 'selection',
selectionTypes: ['solid2D', 'segment'],
selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection
required: true,
skip: true,
@ -573,7 +578,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selection: {
inputType: 'selection',
selectionTypes: [
'solid2D',
'solid2d',
'segment',
'sweepEdge',
'cap',

View File

@ -1,5 +1,8 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine'
export type ProjectsCommandSchema = {
@ -17,6 +20,13 @@ export type ProjectsCommandSchema = {
oldName: string
newName: string
}
'Import file from URL': {
name: string
code?: string
units: UnitLength_type
method: 'newProject' | 'existingProject'
projectName?: string
}
}
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
@ -26,6 +36,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Open project': {
icon: 'arrowRight',
description: 'Open a project',
status: isDesktop() ? 'active' : 'inactive',
args: {
name: {
inputType: 'options',
@ -42,6 +53,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Create project': {
icon: 'folderPlus',
description: 'Create a project',
status: isDesktop() ? 'active' : 'inactive',
args: {
name: {
inputType: 'string',
@ -53,6 +65,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
'Delete project': {
icon: 'close',
description: 'Delete a project',
status: isDesktop() ? 'active' : 'inactive',
needsReview: true,
reviewMessage: ({ argumentsToSubmit }) =>
CommandBarOverwriteWarning({
@ -75,6 +88,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
icon: 'folder',
description: 'Rename a project',
needsReview: true,
status: isDesktop() ? 'active' : 'inactive',
args: {
oldName: {
inputType: 'options',
@ -92,4 +106,80 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
},
},
},
'Import file from URL': {
icon: 'file',
description: 'Create a file',
needsReview: true,
status: 'active',
args: {
method: {
inputType: 'options',
required: true,
skip: true,
options: isDesktop()
? [
{ name: 'New project', value: 'newProject' },
{ name: 'Existing project', value: 'existingProject' },
]
: [{ name: 'Overwrite', value: 'existingProject' }],
valueSummary(value) {
return isDesktop()
? value === 'newProject'
? 'New project'
: 'Existing project'
: 'Overwrite'
},
},
// TODO: We can't get the currently-opened project to auto-populate here because
// it's not available on projectMachine, but lower in fileMachine. Unify these.
projectName: {
inputType: 'options',
required: (commandsContext) =>
isDesktop() &&
commandsContext.argumentsToSubmit.method === 'existingProject',
skip: true,
options: (_, context) =>
context?.projects.map((p) => ({
name: p.name!,
value: p.name!,
})) || [],
},
name: {
inputType: 'string',
required: isDesktop(),
skip: true,
},
code: {
inputType: 'text',
required: true,
skip: true,
valueSummary(value) {
const lineCount = value?.trim().split('\n').length
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
},
},
units: {
inputType: 'options',
required: false,
skip: true,
options: baseUnitsUnion.map((unit) => ({
name: baseUnitLabels[unit],
value: unit,
})),
},
},
reviewMessage(commandBarContext) {
return isDesktop()
? `Will add the contents from URL to a new ${
commandBarContext.argumentsToSubmit.method === 'newProject'
? 'project with file main.kcl'
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
} named "${
commandBarContext.argumentsToSubmit.name
}", and set default units to "${
commandBarContext.argumentsToSubmit.units
}".`
: `Will overwrite the contents of the current file with the contents from the URL.`
},
},
}

View File

@ -116,16 +116,16 @@ export const loftValidator = async ({
}
const { selection } = data
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) {
return 'Unable to loft, some selection are not solid2Ds'
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2ds'
}
const sectionIds = data.selection.graphSelections.flatMap((s) =>
s.artifact?.type === 'solid2D' ? s.artifact.pathId : []
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
)
if (sectionIds.length < 2) {
return 'Unable to loft, selection contains less than two solid2Ds'
return 'Unable to loft, selection contains less than two solid2ds'
}
const loftCommand = async () => {
@ -153,3 +153,57 @@ export const loftValidator = async ({
return 'Unable to loft with selected sketches'
}
}
export const shellValidator = async ({
data,
}: {
data: { selection: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.selection)) {
return 'Unable to shell, selections are missing'
}
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
const face_ids = data.selection.graphSelections.flatMap((s) =>
s.artifact ? s.artifact.id : []
)
// We don't have the concept of solid3ds in TS yet.
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
const object_id = engineCommandManager.artifactGraph
.values()
.find((v) => v.type === 'sweep')?.pathId
if (!object_id) {
return "Unable to shell, couldn't find the solid"
}
const shellCommand = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
const cmdArgs = {
face_ids,
object_id,
hollow: DEFAULT_HOLLOW,
shell_thickness: DEFAULT_THICKNESS,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'solid3d_shell_face',
...cmdArgs,
},
})
}
const attemptShell = await dryRunWrapper(shellCommand)
if (attemptShell?.success) {
return true
}
return 'Unable to shell with the provided selection'
}

View File

@ -69,6 +69,7 @@ export const KCL_DEFAULT_DEGREE = `360`
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
export const DEFAULT_HOST = 'https://api.zoo.dev'
export const PROD_APP_URL = 'https://app.zoo.dev'
export const SETTINGS_FILE_NAME = 'settings.toml'
export const TOKEN_FILE_NAME = 'token.txt'
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
@ -110,6 +111,9 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
localFallback: '/kcl-samples-manifest-fallback.json',
} as const
/** URL parameter to create a file */
export const CREATE_FILE_URL_PARAM = 'create-file'
/** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
@ -139,3 +143,12 @@ export const VIEW_NAMES_SEMANTIC = {
} as const
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
/** Custom URL protocol our desktop registers */
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
/**
* A query parameter that triggers a modal
* to "open in desktop app" when present in the URL
*/
export const ASK_TO_OPEN_QUERY_PARAM = 'ask-open-desktop'

58
src/lib/desktopFS.test.ts Normal file
View File

@ -0,0 +1,58 @@
import { getUniqueProjectName } from './desktopFS'
import { FileEntry } from './project'
/** Create a dummy project */
function project(name: string, children?: FileEntry[]): FileEntry {
return {
name,
children: children || [
{ name: 'main.kcl', children: null, path: 'main.kcl' },
],
path: `/projects/${name}`,
}
}
describe(`Getting unique project names`, () => {
it(`should return the same name if no conflicts`, () => {
const projectName = 'new-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe(projectName)
})
it(`should return a unique name if there is a conflict`, () => {
const projectName = 'existing-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-1')
})
it(`should increment an ending index until a unique one is found`, () => {
const projectName = 'existing-project-1'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-3')
})
it(`should prefer the formatting of the index identifier if present`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
it(`be able to get an incrementing index regardless of padding zeroes`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-01'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
})

View File

@ -54,8 +54,10 @@ export function getNextProjectIndex(
const matches = projects.map((project) => project.name?.match(regex))
const indices = matches
.filter(Boolean)
.map((match) => match![1])
.map(Number)
.map((match) => (match !== null ? match[1] : '-1'))
.map((maybeMatchIndex) => {
return parseInt(maybeMatchIndex || '0', 10)
})
const maxIndex = Math.max(...indices, -1)
return maxIndex + 1
}
@ -83,6 +85,33 @@ export function doesProjectNameNeedInterpolated(projectName: string) {
return projectName.includes(INDEX_IDENTIFIER)
}
/**
* Given a target name, which may include our magic index interpolation string,
* and a list of projects, return a unique name that doesn't conflict with any
* of the existing projects, incrementing any ending number if necessary.
* @param name
* @param projects
* @returns
*/
export function getUniqueProjectName(name: string, projects: FileEntry[]) {
// The name may have our magic index interpolation string in it
const needsInterpolation = doesProjectNameNeedInterpolated(name)
if (needsInterpolation) {
const nextIndex = getNextProjectIndex(name, projects)
return interpolateProjectNameWithIndex(name, nextIndex)
} else {
let newName = name
while (projects.some((project) => project.name === newName)) {
const nameEndsWithNumber = newName.match(/\d+$/)
newName = nameEndsWithNumber
? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`)
: `${name}-1`
}
return newName
}
}
function escapeRegExpChars(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

View File

@ -1,12 +1,14 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes'
import { kclManager } from './singletons'
import { codeManager, kclManager } from './singletons'
import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { IndexLoaderData } from './types'
interface OnSubmitProps {
sampleName: string
@ -15,10 +17,21 @@ interface OnSubmitProps {
method: 'overwrite' | 'newFile'
}
export function kclCommands(
onSubmit: (p: OnSubmitProps) => Promise<void>,
providedOptions: CommandArgumentOption<string>[]
): Command[] {
interface KclCommandConfig {
// TODO: find a different approach that doesn't require
// special props for a single command
specialPropsForSampleCommand: {
onSubmit: (p: OnSubmitProps) => Promise<void>
providedOptions: CommandArgumentOption<string>[]
}
projectData: IndexLoaderData
authToken: string
settings: {
defaultUnit: UnitLength_type
}
}
export function kclCommands(commandProps: KclCommandConfig): Command[] {
return [
{
name: 'format-code',
@ -107,7 +120,9 @@ export function kclCommands(
)
.then((props) => {
if (props?.code) {
onSubmit(props).catch(reportError)
commandProps.specialPropsForSampleCommand
.onSubmit(props)
.catch(reportError)
}
})
.catch(reportError)
@ -149,9 +164,25 @@ export function kclCommands(
}
return value
},
options: providedOptions,
options: commandProps.specialPropsForSampleCommand.providedOptions,
},
},
},
{
name: 'share-file-link',
displayName: 'Share file',
description: 'Create a link that contains a copy of the current file.',
groupId: 'code',
needsReview: false,
icon: 'link',
onSubmit: () => {
copyFileShareLink({
token: commandProps.authToken,
code: codeManager.code,
name: commandProps.projectData.project?.name || '',
units: commandProps.settings.defaultUnit,
}).catch(reportRejection)
},
},
]
}

16
src/lib/links.test.ts Normal file
View File

@ -0,0 +1,16 @@
import { createCreateFileUrl } from './links'
describe(`link creation tests`, () => {
test(`createCreateFileUrl happy path`, async () => {
const code = `extrusionDistance = 12`
const name = `test`
const units = `mm`
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, units })
expect(result.toString()).toBe(expectedLink)
})
})

100
src/lib/links.ts Normal file
View File

@ -0,0 +1,100 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import {
ASK_TO_OPEN_QUERY_PARAM,
CREATE_FILE_URL_PARAM,
PROD_APP_URL,
} from './constants'
import { stringToBase64 } from './base64'
import { DEV, VITE_KC_API_BASE_URL } from 'env'
import toast from 'react-hot-toast'
import { err } from './trap'
export interface FileLinkParams {
code: string
name: string
units: UnitLength_type
}
export async function copyFileShareLink(
args: FileLinkParams & { token: string }
) {
const token = args.token
if (!token) {
toast.error('You need to be signed in to share a file.', {
duration: 5000,
})
return
}
const shareUrl = createCreateFileUrl(args)
const shortlink = await createShortlink(token, shareUrl.toString())
if (err(shortlink)) {
toast.error(shortlink.message, {
duration: 5000,
})
return
}
await globalThis.navigator.clipboard.writeText(shortlink.url)
toast.success(
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
{
duration: 5000,
}
)
}
/**
* Creates a URL with the necessary query parameters to trigger
* the "Import file from URL" command in the app.
*
* With the additional step of asking the user if they want to
* open the URL in the desktop app.
*/
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
// Use the dev server if we are in development mode
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true),
name,
units,
code: stringToBase64(code),
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
})
const createFileUrl = new URL(`?${searchParams.toString()}`, origin)
return createFileUrl
}
/**
* Given a file's code, name, and units, creates shareable link to the
* web app with a query parameter that triggers a modal to "open in desktop app".
* That modal is defined in the `OpenInDesktopAppHandler` component.
* TODO: update the return type to use TS library after its updated
*/
export async function createShortlink(
token: string,
url: string
): Promise<Error | { key: string; url: string }> {
/**
* We don't use our `withBaseURL` function here because
* there is no URL shortener service in the dev API.
*/
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
method: 'POST',
headers: {
'Content-type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
url,
// In future we can support org-scoped and password-protected shortlinks here
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
}),
})
if (!response.ok) {
const error = await response.json()
return new Error(`Failed to create shortlink: ${error.message}`)
} else {
return response.json()
}
}

View File

@ -1,4 +1,4 @@
import { defaultRustSourceRange } from 'lang/wasm'
import { defaultSourceRange } from 'lang/wasm'
import { filterOperations } from './operations'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
@ -8,7 +8,7 @@ function stdlib(name: string): Operation {
name,
unlabeledArg: null,
labeledArgs: {},
sourceRange: defaultRustSourceRange(),
sourceRange: defaultSourceRange(),
isError: false,
}
}
@ -17,10 +17,10 @@ function userCall(name: string): Operation {
return {
type: 'UserDefinedFunctionCall',
name,
functionSourceRange: defaultRustSourceRange(),
functionSourceRange: defaultSourceRange(),
unlabeledArg: null,
labeledArgs: {},
sourceRange: defaultRustSourceRange(),
sourceRange: defaultSourceRange(),
}
}
function userReturn(): Operation {

View File

@ -3,8 +3,8 @@ import { VITE_KC_API_BASE_URL } from 'env'
import crossPlatformFetch from './crossPlatformFetch'
import { err, reportRejection } from './trap'
import { Selections } from './selections'
import { ArtifactGraph, getArtifactOfTypes } from 'lang/std/artifactGraph'
import { SourceRange } from 'lang/wasm'
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { ArtifactGraph, SourceRange, topLevelRange } from 'lang/wasm'
import toast from 'react-hot-toast'
import { codeManager, editorManager, kclManager } from './singletons'
import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad'
@ -334,7 +334,7 @@ const reBuildNewCodeWithRanges = (
} else if (change.added && !change.removed) {
const start = newCodeWithRanges.length
const end = start + change.value.length
insertRanges.push([start, end, true])
insertRanges.push(topLevelRange(start, end))
newCodeWithRanges += change.value
}
}

View File

@ -114,7 +114,7 @@ export const fileLoader: LoaderFunction = async (
return redirect(
`${PATHS.FILE}/${encodeURIComponent(
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
)}`
)}${new URL(routerData.request.url).search || ''}`
)
}
@ -188,11 +188,14 @@ export const fileLoader: LoaderFunction = async (
// Loads the settings and by extension the projects in the default directory
// and returns them to the Home route, along with any errors that occurred
export const homeLoader: LoaderFunction = async (): Promise<
HomeLoaderData | Response
> => {
export const homeLoader: LoaderFunction = async ({
request,
}): Promise<HomeLoaderData | Response> => {
const url = new URL(request.url)
if (!isDesktop()) {
return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
return redirect(
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
}
return {}
}

View File

@ -10,6 +10,7 @@ import {
SourceRange,
Expr,
defaultSourceRange,
topLevelRange,
} from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { isNonNullable, uuidv4 } from 'lib/utils'
@ -63,7 +64,7 @@ type Selection__old =
| 'line-end'
| 'line-mid'
| 'extrude-wall'
| 'solid2D'
| 'solid2d'
| 'start-cap'
| 'end-cap'
| 'point'
@ -103,13 +104,13 @@ function convertSelectionToOld(selection: Selection): Selection__old | null {
// return {} as Selection__old
// TODO implementation
const _artifact = selection.artifact
if (_artifact?.type === 'solid2D') {
if (_artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return { range: codeRef.range, type: 'solid2D' }
return { range: codeRef.range, type: 'solid2d' }
}
if (_artifact?.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
@ -269,7 +270,7 @@ export function getEventForSegmentSelection(
selectionType: 'singleCodeCursor',
selection: {
codeRef: {
range: [node.node.start, node.node.end, true],
range: topLevelRange(node.node.start, node.node.end),
pathToNode: group.userData.pathToNode,
},
},
@ -381,10 +382,13 @@ export function processCodeMirrorRanges({
if (!isChange) return null
const codeBasedSelections: Selections['graphSelections'] =
codeMirrorRanges.map(({ from, to }) => {
const pathToNode = getNodePathFromSourceRange(ast, [from, to, true])
const pathToNode = getNodePathFromSourceRange(
ast,
topLevelRange(from, to)
)
return {
codeRef: {
range: [from, to, true],
range: topLevelRange(from, to),
pathToNode,
},
}
@ -447,7 +451,10 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
if (err(nodeMeta)) return
const node = nodeMeta.node
const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection?.codeRef?.range, [node.start, node.end, true])
return isOverlap(
selection?.codeRef?.range,
topLevelRange(node.start, node.end)
)
})
const color = groupHasCursor
@ -575,7 +582,7 @@ export function getSelectionTypeDisplayText(
([type, count]) =>
`${count} ${type
.replace('wall', 'face')
.replace('solid2D', 'face')
.replace('solid2d', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
)
.toArray()
@ -650,7 +657,7 @@ export function codeToIdSelections(
const artifact = engineCommandManager.artifactGraph.get(
entry.artifact.solid2dId || ''
)
if (artifact?.type !== 'solid2D') {
if (artifact?.type !== 'solid2d') {
bestCandidate = {
artifact: entry.artifact,
selection,
@ -873,7 +880,7 @@ export function updateSelections(
return {
artifact: artifact,
codeRef: {
range: [node.start, node.end, true],
range: topLevelRange(node.start, node.end),
pathToNode: pathToNode,
},
}
@ -887,7 +894,7 @@ export function updateSelections(
if (err(node)) return node
pathToNodeBasedSelections.push({
codeRef: {
range: [node.node.start, node.node.end, true],
range: topLevelRange(node.node.start, node.node.end),
pathToNode: pathToNode,
},
})

View File

@ -195,7 +195,7 @@ export async function submitAndAwaitTextToKcl({
.toLowerCase()}${FILE_EXT}`
if (isDesktop()) {
// We have to pre-emptively run our unique file name logic,
// We have to preemptively run our unique file name logic,
// so that we can pass the unique file name to the toast,
// and by extension the file-deletion-on-reject logic.
newFileName = getNextFileName({

View File

@ -280,7 +280,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Offset plane',
description: 'Create a plane parallel to an existing plane.',
links: [],
links: [
{
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/offsetPlane',
},
],
},
{
id: 'plane-points',
@ -305,7 +310,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Text-to-CAD',
description: 'Generate geometry from a text prompt.',
links: [],
links: [
{
label: 'API docs',
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
},
],
},
{
id: 'prompt-to-edit',

View File

@ -6,16 +6,16 @@ import {
hasLeadingZero,
hasDigitsLeftOfDecimal,
} from './utils'
import { SourceRange } from '../lang/wasm'
import { SourceRange, topLevelRange } from '../lang/wasm'
describe('testing isOverlapping', () => {
testBothOrders([0, 3, true], [3, 10, true])
testBothOrders([0, 5, true], [3, 4, true])
testBothOrders([0, 5, true], [5, 10, true])
testBothOrders([0, 5, true], [6, 10, true], false)
testBothOrders([0, 5, true], [-1, 1, true])
testBothOrders([0, 5, true], [-1, 0, true])
testBothOrders([0, 5, true], [-2, -1, true], false)
testBothOrders(topLevelRange(0, 3), topLevelRange(3, 10))
testBothOrders(topLevelRange(0, 5), topLevelRange(3, 4))
testBothOrders(topLevelRange(0, 5), topLevelRange(5, 10))
testBothOrders(topLevelRange(0, 5), topLevelRange(6, 10), false)
testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 1))
testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 0))
testBothOrders(topLevelRange(0, 5), topLevelRange(-2, -1), false)
})
function testBothOrders(a: SourceRange, b: SourceRange, result = true) {

View File

@ -25,6 +25,10 @@ export const projectsMachine = setup({
type: 'Delete project'
data: ProjectsCommandSchema['Delete project']
}
| {
type: 'Import file from URL'
data: ProjectsCommandSchema['Import file from URL']
}
| { type: 'navigate'; data: { name: string } }
| {
type: 'xstate.done.actor.read-projects'
@ -42,6 +46,10 @@ export const projectsMachine = setup({
type: 'xstate.done.actor.rename-project'
output: { message: string; oldName: string; newName: string }
}
| {
type: 'xstate.done.actor.create-file'
output: { message: string; projectName: string; fileName: string }
}
| { type: 'assign'; data: { [key: string]: any } },
input: {} as {
projects: Project[]
@ -60,6 +68,7 @@ export const projectsMachine = setup({
toastError: () => {},
navigateToProject: () => {},
navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
},
actors: {
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
@ -90,12 +99,22 @@ export const projectsMachine = setup({
name: '',
})
),
createFile: fromPromise(
(_: {
input: ProjectsCommandSchema['Import file from URL'] & {
projects: Project[]
}
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
),
},
guards: {
'Has at least 1 project': () => false,
'Has at least 1 project': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-projects') return false
return event.output.length ? event.output.length >= 1 : false
},
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBULE6VwYvgeIJ38sAIT25td27KpdzG7yZeho5slHVmMc1IY3HLfnkrNZt2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFGVv11ZFsnm0kdN8QFJVjlUPZ9co+3-2C0KEqdrXsJMJ8ryc86iUyYiCvxFNzldb3jHtjTsf8EPj1ssQchZlR8jR5EmDRrIGZcuF7maF1aZtslx29IWqtiwPDSz1LxDxepnlOfYDghp03eyOpngzXuYdHNPHozgJ26B9IXR2cTvP0BVkSRXKqgLtyq8OfQcXOwlubMIA */
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */
id: 'Home machine',
initial: 'Reading projects',
@ -111,6 +130,8 @@ export const projectsMachine = setup({
})),
target: '.Reading projects',
},
'Import file from URL': '.Creating file',
},
states: {
'Has no projects': {
@ -155,7 +176,10 @@ export const projectsMachine = setup({
id: 'create-project',
src: 'createProject',
input: ({ event, context }) => {
if (event.type !== 'Create project') {
if (
event.type !== 'Create project' &&
event.type !== 'Import file from URL'
) {
return {
name: '',
projects: context.projects,
@ -272,5 +296,39 @@ export const projectsMachine = setup({
],
},
},
'Creating file': {
invoke: {
id: 'create-file',
src: 'createFile',
input: ({ event, context }) => {
if (event.type !== 'Import file from URL') {
return {
code: '',
name: '',
units: 'mm',
method: 'existingProject',
projects: context.projects,
}
}
return {
code: event.data.code || '',
name: event.data.name,
units: event.data.units,
method: event.data.method,
projectName: event.data.projectName,
projects: context.projects,
}
},
onDone: {
target: 'Reading projects',
actions: ['navigateToFile', 'toastSuccess'],
},
onError: {
target: 'Reading projects',
actions: 'toastError',
},
},
},
},
})

View File

@ -21,6 +21,7 @@ import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os'
import { reportRejection } from 'lib/trap'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import argvFromYargs from './commandLineArgs'
import * as packageJSON from '../package.json'
@ -42,15 +43,13 @@ if (!process.env.NODE_ENV)
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
'wss://api.dev.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.dev.zoo.dev'
process.env.VITE_KC_SITE_BASE_URL ??= 'https://dev.zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
/// Register our application to handle all "electron-fiddle://" protocols.
/// Register our application to handle all "zoo-studio:" protocols.
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
@ -65,7 +64,7 @@ if (process.defaultApp) {
// Must be done before ready event.
registerStartupListeners()
const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
let newWindow
if (reuse) {
@ -90,32 +89,54 @@ const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
})
}
const pathIsCustomProtocolLink =
pathToOpen?.startsWith(ZOO_STUDIO_PROTOCOL) ?? false
// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
const filteredPath = pathToOpen
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
: ''
const fullHashBasedUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/${filteredPath}`
newWindow.loadURL(fullHashBasedUrl).catch(reportRejection)
} else {
getProjectPathAtStartup(filePath)
.then(async (projectPath) => {
const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
if (projectPath === null) {
await newWindow.loadFile(startIndex)
return
}
console.log('Loading file', projectPath)
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
await newWindow.loadFile(startIndex, {
hash: fullUrl,
if (pathIsCustomProtocolLink && pathToOpen) {
// We're trying to open a custom protocol link
const filteredPath = pathToOpen
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
: ''
const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
newWindow
.loadFile(startIndex, {
hash: filteredPath,
})
})
.catch(reportRejection)
.catch(reportRejection)
} else {
// otherwise we're trying to open a local file from the command line
getProjectPathAtStartup(pathToOpen)
.then(async (projectPath) => {
const startIndex = path.join(
__dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
if (projectPath === null) {
await newWindow.loadFile(startIndex)
return
}
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
await newWindow.loadFile(startIndex, {
hash: fullUrl,
})
})
.catch(reportRejection)
}
}
// Open the DevTools.

View File

@ -25,6 +25,7 @@ import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { useProjectsContext } from 'hooks/useProjectsContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.
@ -34,6 +35,18 @@ const Home = () => {
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Import file from URL',
argDefaultValues,
},
})
})
useRefreshSettings(PATHS.HOME + 'SETTINGS')
const navigate = useNavigate()
const {
@ -148,7 +161,7 @@ const Home = () => {
}}
data-testid="home-new-file"
>
New project
Create project
</ActionButton>
</div>
<div className="flex gap-2 items-center">

View File

@ -1382,12 +1382,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "iai"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1739,7 +1733,6 @@ dependencies = [
"gltf-json",
"handlebars",
"http 1.2.0",
"iai",
"image",
"indexmap 2.7.0",
"insta",

View File

@ -17,6 +17,7 @@ use kittycad_modeling_cmds::{
websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse},
};
use tokio::sync::RwLock;
use uuid::Uuid;
const CPP_PREFIX: &str = "const double scaleFactor = 100;\n";
const NEED_PLANES: bool = true;
@ -369,6 +370,10 @@ impl kcl_lib::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
IndexMap::new()
}
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
Vec::new()
}

View File

@ -113,7 +113,6 @@ base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.3.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
@ -129,10 +128,6 @@ workspace = true
name = "compiler_benchmark_criterion"
harness = false
[[bench]]
name = "compiler_benchmark_iai"
harness = false
[[bench]]
name = "digest_benchmark"
harness = false
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "executor_benchmark_criterion"
harness = false
[[bench]]
name = "executor_benchmark_iai"
harness = false

View File

@ -1,35 +0,0 @@
use iai::black_box;
pub fn parse(program: &str) {
black_box(kcl_lib::Program::parse(program).unwrap());
}
fn parse_kitt() {
parse(KITT_PROGRAM)
}
fn parse_pipes() {
parse(PIPES_PROGRAM)
}
fn parse_cube() {
parse(CUBE_PROGRAM)
}
fn parse_math() {
parse(MATH_PROGRAM)
}
fn parse_lsystem() {
parse(LSYSTEM_PROGRAM)
}
iai::main! {
parse_kitt,
parse_pipes,
parse_cube,
parse_math,
parse_lsystem,
}
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

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