Compare commits

...

64 Commits

Author SHA1 Message Date
7d7b176bb7 Cut release v0.15.5 (#1632) 2024-03-06 12:41:09 +11:00
9aada41a0d Show all CAD files in FileTree (#1642) 2024-03-05 20:37:48 -05:00
23971465ce More fixes to export e2e test (#1646)
* change to download listener

* ultra snapshot test

* clean up

* Revert "ultra snapshot test"

This reverts commit 2d2a585a17.
2024-03-06 01:08:15 +00:00
23e294930b Clean up possibly dead code (#1032)
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 12:53:29 -08:00
22cc4c9a98 fix error sourcce range for kcl stdlib (#1641)
fix error

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-03-05 12:46:05 -08:00
fe6478f568 Bump serde_json from 1.0.113 to 1.0.114 in /src/wasm-lib (#1622)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114)

---
updated-dependencies:
- dependency-name: serde_json
  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>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 20:38:28 +00:00
1989734c3b Bump openapitor from 8db292e to 6f38abe in /src/wasm-lib (#1636)
Bumps [openapitor](https://github.com/KittyCAD/kittycad.rs) from `8db292e` to `6f38abe`.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](8db292eaa7...6f38abe149)

---
updated-dependencies:
- dependency-name: openapitor
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 20:38:10 +00:00
f36984f52a Bump mio from 0.8.9 to 0.8.11 in /src-tauri (#1630)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.9 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.9...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 20:23:42 +00:00
5437538892 Bump mio from 0.8.9 to 0.8.11 in /src/wasm-lib (#1629)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.9 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.9...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 20:23:16 +00:00
97bd60ae87 Bump js-sys from 0.3.68 to 0.3.69 in /src/wasm-lib (#1635)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.68 to 0.3.69.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  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>
2024-03-05 20:22:54 +00:00
9116d79c50 Bump tauri-plugin-fs-extra from ed682dd to 19aa220 in /src-tauri (#1634)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `ed682dd` to `19aa220`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](ed682dd96e...19aa220411)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs-extra
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 20:22:42 +00:00
b3b5dff60f Bump kittycad from 0.2.58 to 0.2.59 in /src-tauri (#1633)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.58 to 0.2.59.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.58...v0.2.59)

---
updated-dependencies:
- dependency-name: kittycad
  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>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-03-05 20:22:25 +00:00
55f842d3bd Bump tauri from 1.5.4 to 1.6.0 in /src-tauri (#1446)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 1.5.4 to 1.6.0.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v1.5.4...tauri-v1.6.0)

---
updated-dependencies:
- dependency-name: tauri
  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>
2024-03-05 12:14:30 -08:00
778478757e Fillets (#1401)
* add fillet

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

* updates

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

* update tests

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

* fixes

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

* get end cap info

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

* tryu

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

* next-adjacent

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

* fix js tests

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

* works

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

* updates

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

* u[pdates

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

* updates

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

* updates

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

* move back to functions

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-03-05 11:52:45 -08:00
bc303fbaab try and make test more robust (#1638)
* try and make test more robust

* unused import

* add logging

* fix bug in export test

* Revert "unused import"

This reverts commit 0fb7090eca.

* revert more

* more fixes

* fix

* ultra snapshot test

* Revert "ultra snapshot test"

This reverts commit 17a883727e.
2024-03-05 15:42:27 +00:00
d422f09045 Minor readme typo 2024-03-05 04:56:29 -05:00
adcf80331a Bump KCVM (#1631) 2024-03-04 17:51:03 -08:00
4fbd7ace98 Increase playwright timeout on a per test basis (#1627)
try individual timeout
2024-03-05 11:52:43 +11:00
0df858b9ca update discord announce under 2000 char limit (#1628)
* update discord announce under 2000 char limit

* updating URL handling
2024-03-04 14:34:57 -07:00
c6f080c440 Add export to cmd bar (#1593)
* Add new exportFile icon

* Isolate exportFromEngine command

* Naive initial export command

* Update types to accept functions for arg defaultValue, required, and options

* Make existing helper functions and configs work with new types

* Make UI components work with new types
support resolving function values and conditional logic

* Add full export command to command bar

* Replace ExportButton with thin wrapper on cmd bar command

* fmt

* Fix stale tests and bugs found by good tests

* fmt

* Update src/components/CommandBar/CommandArgOptionInput.tsx

* Update snapshot tests and onboarding wording

* Move the panel open click into doExport

* Don't need to input storage step in export tests anymore

* Remove console logs, fmt, select options if we need to

* Increase test timeout

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-03-05 08:06:43 +11:00
c1a14a107a undo test stuff (#1619)
* another bad change

* undo
2024-03-04 14:18:25 +11:00
3c721f2b29 make close segment visually distinct (#1620) 2024-03-04 14:18:08 +11:00
61e2a1eddc remove playwright parallel, but run macos and ubuntu at the same time (#1617)
* remove playwright parallel, but run macos and ubuntu at the same time

* better logging for cam test

* skip a test for safari

* remove steps
2024-03-04 12:18:56 +11:00
6406e27794 bad change 2024-03-04 10:44:45 +11:00
1e382a76dd clean up old snapshot images (#1618) 2024-03-04 10:41:25 +11:00
06cdaa9ae8 overhaul cam move test 2024-03-04 10:37:11 +11:00
85c30be333 Remove unnecessary import (#1611) 2024-03-03 15:18:28 -06:00
4d4a1d66e8 snap to profile start (#1616)
* fix camera set from debug panel

* add snap to for closing sketch

* clean up file that shouldn't have been committed
2024-03-04 08:14:37 +11:00
223b5952aa Refactor mouse event args (#1613)
* refactor mouse event interfaces

Importantly returning multiple intersections from raycastRing, but other clean up

* refactor enter exit args interface

* type tweak
2024-03-03 16:23:16 +11:00
fedffbb384 Grackle: stdlib LineTo function (#1601)
* Bump execution plan

* Grackle: lineTo stdlib function (#1605)

* Remove test JSON output
2024-03-02 18:39:31 -06:00
ed4e3df3b2 unused vars cleanup (#1608) 2024-03-02 20:30:24 +11:00
18d200e790 add test: Can edit segments by dragging their handles (#1607) 2024-03-02 20:08:13 +11:00
0c50a5996d show selected color for start selected at (#1606) 2024-03-02 19:00:24 +11:00
73bca2dcfc fully remove show (#1592)
* fully remove show

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

* updates

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

* updates

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

* fmt

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

* updates

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

* rm

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

* updates

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

* fix tests

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-03-01 17:16:18 -08:00
c6a50a3cdf try parallel plawright (#1579)
* parallel plawright

* test robustness tweak

* change to only double speed
2024-03-02 11:25:50 +11:00
b81c9d04cc make kcl std lib first class (#1603)
* make kcl std lib first class

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

* fix tests

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

* u[dates

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-03-01 14:23:30 -08:00
9d8a7064da make startProfileAt UI editable (#1599)
make startProfileAt editable
2024-03-02 08:48:30 +11:00
b0e6140e9f Implement dual camera sync direction (#1597)
* implement dual camer sync direction

The existance of the client side scene requires two cameras to stay in sync, really these need to be a master-slave relationship, intitial this was implemented with the client side scene taking the lead and sending updates to the server using the  endpoint (as it didn't require an new endpoints), but even though we added a sequence property to this endpoint and sent it over udp, it was still an abuse of this endpoint as the engine didn't have this endpoint setup with a fload of messages and low-latency in mind.

Now we have migrated back to sending mouse events to the engine instead, but with the engine replying with camera details on drag_end etc so that we can keep the client camera in sync.

The client side camera still does take the master role in sketch mode as it makes sense to keep the low latency benfits of the local camera for the locallay rendered assets in sketch mode, moving the camera in this mode already did hide the engine camera while the camera is moving so as to avoid ghoasting so this works well.

The camera controls now work by syncing in either direction depending on what's appropiate

* fmt

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

* update default plane extrude numbers

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

* trigger-ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-02 08:20:50 +11:00
f9df7ff885 import docs (#1602)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-03-01 12:19:24 -08:00
aec9637d7a EngineConnection should fail fast if socket closes (#1600)
* EngineConnection should fail fast if socket closes

* Fix clippy lint
2024-03-01 14:43:11 -05:00
e4c5fad8c7 failing auto complete test (#1578) 2024-03-01 08:22:04 -08:00
cc0d601294 enable concurrency for playwright action (#1598) 2024-03-01 07:08:02 -08:00
69cefafc19 Bump image from 0.24.8 to 0.24.9 in /src/wasm-lib (#1584)
Bumps [image](https://github.com/image-rs/image) from 0.24.8 to 0.24.9.
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.8...v0.24.9)

---
updated-dependencies:
- dependency-name: image
  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>
2024-02-29 19:57:37 -08:00
b187ca3422 Bump kittycad from 0.2.54 to 0.2.58 in /src/wasm-lib (#1583)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.54 to 0.2.58.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.54...v0.2.58)

---
updated-dependencies:
- dependency-name: kittycad
  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>
2024-02-29 19:56:45 -08:00
1edadcaa0f Bump kittycad from 0.2.53 to 0.2.58 in /src-tauri (#1581)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.53 to 0.2.58.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.53...v0.2.58)

---
updated-dependencies:
- dependency-name: kittycad
  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>
2024-02-29 19:56:09 -08:00
95c0ded8cf Refactor: move point-parsing into its own function (#1590)
This will be reused in future stdlib functions.

Also, add a field for argument number to the "invalid argument type" error message.
2024-02-29 17:55:34 -06:00
0ebb4e2cad one more sentry (#1591)
Update KclSingleton.tsx
2024-02-29 14:56:57 -08:00
f3e0939057 Cut release v0.15.4 (#1587) 2024-03-01 09:50:28 +11:00
f5e233d8a0 Finish removing Sentry (#1588)
Finish removing Sentry

Following Frank's PR in 8f5034f997, I'm
sending up a PR to finish pulling Sentry out of the codebase, rather
than just disabling it via configuration.

F

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2024-02-29 16:41:20 -05:00
1cab3e628f client side sketch scene not respecting base-unit-scale (#1576)
* client side sketch scene not respecting base-unit-scale

* test tweak

* remove dead code

* fix test

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

* test fix up

* trigger ci

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

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-01 06:55:49 +11:00
2ca6ba52b6 Bump serde_json from 1.0.113 to 1.0.114 in /src-tauri (#1463)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114)

---
updated-dependencies:
- dependency-name: serde_json
  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>
2024-02-29 11:35:35 -08:00
f741ea2e09 Bump serde from 1.0.196 to 1.0.197 in /src-tauri (#1462)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.196 to 1.0.197.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.196...v1.0.197)

---
updated-dependencies:
- dependency-name: serde
  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>
2024-02-29 11:34:43 -08:00
9f2a7781fc Bump anyhow from 1.0.79 to 1.0.80 in /src-tauri (#1465)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.79 to 1.0.80.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.79...1.0.80)

---
updated-dependencies:
- dependency-name: anyhow
  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>
2024-02-29 11:34:30 -08:00
990f2b4154 Vector for tracking cargo tests (#1580)
* try and log to vector

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

* iupdates

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

* try and log to vector

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

* try and log to vector

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

* ud[ates

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

* ud[ates

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

* ud[ates

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

* ud[ates

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

* ud[ates

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

* ud[ates

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

* ud[ates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-29 11:14:01 -08:00
0af0f15281 Bump clap from 4.5.0 to 4.5.1 in /src/wasm-lib (#1448)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.0...clap_complete-v4.5.1)

---
updated-dependencies:
- dependency-name: clap
  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>
2024-02-29 00:53:57 -08:00
b558548b94 Bump google-github-actions/auth from 2.1.1 to 2.1.2 (#1521)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  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>
2024-02-29 00:52:34 -08:00
29e0f9a270 Bump kittycad-execution-plan from 9cb86ba to 29086e1 in /src/wasm-lib (#1570)
Bump kittycad-execution-plan in /src/wasm-lib

Bumps [kittycad-execution-plan](https://github.com/KittyCAD/modeling-api) from `9cb86ba` to `29086e1`.
- [Commits](9cb86ba54e...29086e1079)

---
updated-dependencies:
- dependency-name: kittycad-execution-plan
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-29 00:52:18 -08:00
9385c32cfb Bump kittycad-modeling-session from 9cb86ba to 29086e1 in /src/wasm-lib (#1568)
Bump kittycad-modeling-session in /src/wasm-lib

Bumps [kittycad-modeling-session](https://github.com/KittyCAD/modeling-api) from `9cb86ba` to `29086e1`.
- [Commits](9cb86ba54e...29086e1079)

---
updated-dependencies:
- dependency-name: kittycad-modeling-session
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-29 00:51:56 -08:00
ce3fb5c353 Bump tauri-plugin-fs-extra from 01211ff to ed682dd in /src-tauri (#1567)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `01211ff` to `ed682dd`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](01211ff075...ed682dd96e)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs-extra
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-29 00:51:24 -08:00
f920490518 Bump syn from 2.0.49 to 2.0.52 in /src/wasm-lib (#1563)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.49 to 2.0.52.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.49...2.0.52)

---
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>
2024-02-29 00:51:03 -08:00
d681e667ee Hide cam when moving (#1577)
hide cam when moving
2024-02-29 19:25:48 +11:00
5c6515a60e Fix autocomplete in comment (#1575) 2024-02-28 23:24:11 -08:00
eb8a33312d fix trailing comma (#1574)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-28 22:24:11 -08:00
d351b3bbe4 fix recast (#1571)
* fix recast

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

* updates

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-28 21:19:10 -08:00
125 changed files with 13842 additions and 3211 deletions

View File

@ -3,4 +3,3 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_SENTRY_DSN=

View File

@ -3,4 +3,3 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=

View File

@ -40,6 +40,20 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest - uses: taiki-e/install-action@nextest
- name: Rust Cache - name: Rust Cache
@ -48,7 +62,7 @@ jobs:
shell: bash shell: bash
run: |- run: |-
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo nextest run --workspace --no-fail-fast -P ci cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env: env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000 RUST_MIN_STACK: 10485760000

View File

@ -336,7 +336,7 @@ jobs:
cat last_download.json cat last_download.json
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.1' uses: 'google-github-actions/auth@v2.1.2'
with: with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}' credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'

View File

@ -4,6 +4,11 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
@ -80,7 +85,6 @@ jobs:
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14
needs: playwright-ubuntu
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

21
.github/workflows/vector.toml vendored Normal file
View File

@ -0,0 +1,21 @@
[sources.github-actions-file]
type = "file"
data_dir = "/tmp/vector"
include = ["/tmp/github-actions.log"]
# Modify the logs to include the action name.
[transforms.add-action-name]
type = "remap"
inputs = [ "github-actions-file" ]
source = '''
.action = "GITHUB_WORKFLOW"
.repo = "GITHUB_REPOSITORY"
.sha = "GITHUB_SHA"
.ref = "GITHUB_REF_NAME"
'''
[sinks.axiom]
type = "axiom"
inputs = ["add-action-name"]
token = "GH_ACTIONS_AXIOM_TOKEN"
dataset = "github-actions"

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ src/wasm-lib/bindings
src/wasm-lib/kcl/bindings src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info src/wasm-lib/lcov.info
src/wasm-lib/grackle/test_json_output
e2e/playwright/playwright-secrets.env e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png e2e/playwright/temp1.png

View File

@ -141,7 +141,7 @@ run `./make-release.sh` for a patch update
run `./make-release.sh "minor"` for minor run `./make-release.sh "minor"` for minor
run `./make-release.sh "major"` for major run `./make-release.sh "major"` for major
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and past in the following The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and paste in the following
```typescript ```typescript
console.log( console.log(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,8 @@ import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme' import { Themes } from '../../src/lib/theme'
import { roundOff } from 'lib/utils'
import { platform } from 'node:os'
/* /*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -15,9 +17,9 @@ document.addEventListener('mousemove', (e) =>
*/ */
const commonPoints = { const commonPoints = {
startAt: '[26.38, -35.59]', startAt: '[9.06, -12.22]',
num1: 26.63, num1: 9.14,
num2: 53.01, num2: 18.2,
} }
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
@ -65,10 +67,8 @@ test('Basic sketch', async ({ page }) => {
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await u.doAndWaitForImageDiff( await page.getByRole('button', { name: 'Start Sketch' }).click()
() => page.getByRole('button', { name: 'Start Sketch' }).click(), await page.waitForTimeout(100)
200
)
// select a plane // select a plane
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
@ -90,7 +90,6 @@ test('Basic sketch', async ({ page }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num = 26.63
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
@ -136,6 +135,126 @@ test('Basic sketch', async ({ page }) => {
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
test('Can moving camera', async ({ page, context }) => {
test.skip(process.platform === 'darwin', 'Can moving camera')
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async (
mouseActions: any,
xyz: [number, number, number],
cnt = 0
) => {
// hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together.
await u.updateCamPosition(camPos)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300)
await u.openAndClearDebugPanel()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
let shouldRetry = false
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 303)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 })
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-10, -85, -85])
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
// zoom
await u.doAndWaitForImageDiff(async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel()
}, 300)
// zoom with scroll
await u.openAndClearDebugPanel()
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [1, -94, -94])
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => { test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
@ -284,10 +403,9 @@ test('Can create sketches on all planes and their back sides', async ({
}) => { }) => {
await u.openDebugPanel() await u.openDebugPanel()
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.updateCamPosition(viewCmd)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y) await page.mouse.click(clickCoords.x, clickCoords.y)
@ -315,7 +433,7 @@ test('Can create sketches on all planes and their back sides', async ({
const codeTemplate = ( const codeTemplate = (
plane = 'XY' plane = 'XY'
) => `const part001 = startSketchOn('${plane}') ) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([32.13, -43.34], %)` |> startProfileAt([1.14, -1.54], %)`
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
expectedCode: codeTemplate('XY'), expectedCode: codeTemplate('XY'),
@ -325,7 +443,7 @@ test('Can create sketches on all planes and their back sides', async ({
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
expectedCode: codeTemplate('YZ'), expectedCode: codeTemplate('YZ'),
clickCoords: { x: 700, y: 300 }, // green plane clickCoords: { x: 700, y: 250 }, // green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camPos,
@ -386,12 +504,16 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type('(5, %)') // finish line with comment
await page.keyboard.type('(5, %) // lin')
await page.waitForTimeout(100)
// there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY') .toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %) |> startProfileAt([0,0], %)
|> xLine(5, %)`) |> xLine(5, %) // lin`)
}) })
// Onboarding tests // Onboarding tests
@ -621,12 +743,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
const themeOption = page.getByRole('option', { name: 'Set Theme' }) const themeOption = page.getByRole('option', { name: 'Set Theme' })
await expect(themeOption).toBeVisible() await expect(themeOption).toBeVisible()
await themeOption.click() await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option') const themeInput = page.getByPlaceholder('system')
await expect(themeInput).toBeVisible() await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused() await expect(themeInput).toBeFocused()
// Select dark theme // Select dark theme
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute( await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
'data-headlessui-state', 'data-headlessui-state',
'active' 'active'
@ -699,6 +821,8 @@ test('Can extrude from the command bar', async ({ page, context }) => {
).toBeDisabled() ).toBeDisabled()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
// Check that the code was updated // Check that the code was updated
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test // Unfortunately this indentation seems to matter for the test
@ -793,7 +917,7 @@ test('Can add multiple sketches', async ({ page }) => {
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[26.23, -35.39]' const startAt2 = '[0.93,-1.25]'
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -807,7 +931,7 @@ const part002 = startSketchOn('XY')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num2 = 26.48 const num2 = 0.94
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
@ -825,7 +949,7 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${num2}], %)`.replace(/\s/g, '') |> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
) )
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect( await expect(
@ -835,8 +959,8 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${num2}], %) |> line([0, ${roundOff(num2 - 0.01)}], %)
|> line([-52.71, 0], %)`.replace(/\s/g, '') |> line([-1.87, 0], %)`.replace(/\s/g, '')
) )
}) })
@ -930,7 +1054,7 @@ fn yohey = (pos) => {
|> line([-15.79, 17.08], %) |> line([-15.79, 17.08], %)
return '' return ''
} }
yohey([15.79, -34.6]) yohey([15.79, -34.6])
` `
) )
@ -992,7 +1116,6 @@ test('Deselecting line tool should mean nothing happens on click', async ({
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -1050,3 +1173,160 @@ test('Deselecting line tool should mean nothing happens on click', async ({
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
}) })
test('Can edit segments by dragging their handles', async ({
page,
context,
}) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const startPX = [652, 418]
const lineEndPX = [794, 416]
const arcEndPX = [893, 318]
const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100)
let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 }
// drag startProfieAt handle
await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.mouse.move(lineEndPX[0] + dragPX, lineEndPX[1] - dragPX)
await page.mouse.down()
await page.mouse.move(
lineEndPX[0] + dragPX * 2,
lineEndPX[1] - dragPX * 2,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
await page.mouse.move(arcEndPX[0], arcEndPX[1])
await page.mouse.down()
await page.mouse.move(arcEndPX[0] + dragPX, arcEndPX[1] - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([7.01, -11.79], %)
|> line([14.69, 2.73], %)
|> tangentialArcTo([27.6, -3.25], %)`)
})
test('Snap to close works (at any scale)', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
const doSnapAtDifferentScales = async (
camPos: [number, number, number],
expectedCode: string
) => {
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.updateCamPosition(camPos)
await u.closeDebugPanel()
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('XZ')`
)
let prevContent = await page.locator('.cm-content').innerText()
const pointA = [700, 200]
const pointB = [900, 200]
const pointC = [900, 400]
// draw three lines
await page.mouse.click(pointA[0], pointA[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointB[0], pointB[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.click(pointC[0], pointC[1])
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointA[0] - 12, pointA[1] + 12)
const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7]
await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 })
await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1])
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode()
}
const codeTemplate = (
scale = 1,
fudge = 0
) => `const part001 = startSketchOn('XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %)
|> line([0, -${roundOff(scale * 175.37) + fudge}], %)
|> close(%)`
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
})

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test' import { test, expect, Download } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
@ -29,96 +29,7 @@ test.beforeEach(async ({ context, page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60000) test.setTimeout(60_000)
const commonPoints = {
startAt: '[26.38, -35.59]',
num1: 26.63,
num2: 53.01,
}
test('change camera, show planes', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
const camPos: [number, number, number] = [0, 85, 85]
await u.updateCamPosition(camPos)
// rotate
await u.closeDebugPanel()
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 300)
await page.mouse.up({ button: 'right' })
await u.openDebugPanel()
await page.waitForTimeout(500)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
// pan
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.updateCamPosition(camPos)
await u.clearCommandLogs()
await u.closeDebugPanel()
// zoom
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('exports of each format should work', async ({ page, context }) => { test('exports of each format should work', async ({ page, context }) => {
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
@ -179,8 +90,6 @@ const part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByRole('button', { name: APP_NAME }).click()
interface Paths { interface Paths {
modelPath: string modelPath: string
imagePath: string imagePath: string
@ -189,19 +98,50 @@ const part001 = startSketchOn('-XZ')
const doExport = async ( const doExport = async (
output: Models['OutputFormat_type'] output: Models['OutputFormat_type']
): Promise<Paths> => { ): Promise<Paths> => {
await page.getByRole('button', { name: 'Export Model' }).click() await page.getByRole('button', { name: APP_NAME }).click()
await expect(
const exportSelect = page.getByTestId('export-type') page.getByRole('button', { name: 'Export Part' })
await exportSelect.selectOption({ label: output.type }) ).toBeVisible()
await page.getByRole('button', { name: 'Export Part' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar
await page.getByRole('option', { name: output.type, exact: false }).click()
await page.locator('#arg-form').waitFor({ state: 'detached' })
if ('storage' in output) { if ('storage' in output) {
const storageSelect = page.getByTestId('export-storage') await page.getByTestId('arg-name-storage').waitFor({ timeout: 1000 })
await storageSelect.selectOption({ label: output.storage }) await page.getByRole('button', { name: 'storage', exact: false }).click()
await page
.getByRole('option', { name: output.storage, exact: false })
.click()
await page.locator('#arg-form').waitFor({ state: 'detached' })
}
await expect(page.getByText('Confirm Export')).toBeVisible()
const getPromiseAndResolve = () => {
let resolve: any = () => {}
const promise = new Promise<Download>((r) => {
resolve = r
})
return [promise, resolve]
} }
const downloadPromise = page.waitForEvent('download') const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
await page.getByRole('button', { name: 'Export', exact: true }).click() const [downloadPromise2, downloadResolve2] = getPromiseAndResolve()
const download = await downloadPromise let downloadCnt = 0
page.on('download', async (download) => {
if (downloadCnt === 0) {
downloadResolve1(download)
} else if (downloadCnt === 1) {
downloadResolve2(download)
}
downloadCnt++
})
await page.getByRole('button', { name: 'Submit command' }).click()
// Handle download
const download = await downloadPromise1
const downloadLocationer = (extra = '', isImage = false) => const downloadLocationer = (extra = '', isImage = false) =>
`./e2e/playwright/export-snapshots/${output.type}-${ `./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : '' 'storage' in output ? output.storage : ''
@ -211,7 +151,7 @@ const part001 = startSketchOn('-XZ')
if (output.type === 'gltf' && output.storage === 'standard') { if (output.type === 'gltf' && output.storage === 'standard') {
// wait for second download // wait for second download
const download2 = await page.waitForEvent('download') const download2 = await downloadPromise2
await download.saveAs(downloadLocation) await download.saveAs(downloadLocation)
await download2.saveAs(downloadLocation2) await download2.saveAs(downloadLocation2)
@ -384,13 +324,13 @@ test('extrude on each default plane should be stable', async ({
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([0.70, 0.44], %) |> startProfileAt([7.00, 4.40], %)
|> line([0.66, -0.02], %) |> line([6.60, -0.20], %)
|> line([0.28, 0.50], %) |> line([2.80, 5.00], %)
|> line([-0.56, 0.44], %) |> line([-5.60, 4.40], %)
|> line([-0.54, -0.38], %) |> line([-5.40, -3.80], %)
|> close(%) |> close(%)
|> extrude(1.00, %) |> extrude(10.00, %)
` `
await context.addInitScript(async (code) => { await context.addInitScript(async (code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
@ -435,7 +375,23 @@ test('extrude on each default plane should be stable', async ({
await runSnapshotsForOtherPlanes('-YZ') await runSnapshotsForOtherPlanes('-YZ')
}) })
test('Draft segments should look right', async ({ page }) => { test('Draft segments should look right', async ({ page, context }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -468,7 +424,7 @@ test('Draft segments should look right', async ({ page }) => {
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -482,8 +438,8 @@ test('Draft segments should look right', async ({ page }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt([9.06, -12.22], %)
|> line([${commonPoints.num1}, 0], %)`) |> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -493,3 +449,202 @@ test('Draft segments should look right', async ({ page }) => {
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test('Client side scene scale should match engine scale inch', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('Client side scene scale should match engine scale mm', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'mm',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'metric',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.33], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.15.3", "version": "0.15.5",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.10.2", "@codemirror/autocomplete": "^6.10.2",
@ -15,7 +15,6 @@
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0", "@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.77.0",
"@tauri-apps/api": "^1.5.1", "@tauri-apps/api": "^1.5.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",

View File

@ -1,16 +1,34 @@
import requests import re
import os import os
import requests
webhook_url = os.getenv('DISCORD_WEBHOOK_URL') webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
release_version = os.getenv('RELEASE_VERSION') release_version = os.getenv('RELEASE_VERSION')
release_body = os.getenv('RELEASE_BODY') release_body = os.getenv('RELEASE_BODY')
# message to send to Discord # Regular expression to match URLs
url_pattern = r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)'
# Function to encase URLs in <>
def encase_urls_with_angle_brackets(match):
url = match.group(0)
return f'<{url}>'
# Replace all URLs in the release_body with their <> enclosed version
modified_release_body = re.sub(url_pattern, encase_urls_with_angle_brackets, release_body)
# Ensure the modified_release_body does not exceed Discord's character limit
max_length = 500 # Adjust as needed
if len(modified_release_body) > max_length:
modified_release_body = modified_release_body[:max_length].rsplit(' ', 1)[0] # Avoid cutting off in the middle of a word
modified_release_body += "... for full changelog, check out the link above."
# Message to send to Discord
data = { data = {
"content": "content":
f''' f'''
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download **{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
{release_body} {modified_release_body}
''', ''',
"username": "Modeling App Release Updates", "username": "Modeling App Release Updates",
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png" "avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
@ -23,4 +41,7 @@ response = requests.post(webhook_url, json=data)
if response.status_code == 204: if response.status_code == 204:
print("Successfully sent the message to Discord.") print("Successfully sent the message to Discord.")
else: else:
print("Failed to send the message to Discord.") print(f"Failed to send the message to Discord. Status code: {response.status_code}, Response: {response.text}")
print(modified_release_body)
print(data["content"])

41
src-tauri/Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.79" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]] [[package]]
name = "app" name = "app"
@ -1664,9 +1664,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.53" version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4" checksum = "4080db4364c103601db486e4a8aa889ea56c011991e4c454373d8050a165d3da"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1934,9 +1934,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.9" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
@ -3235,9 +3235,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.196" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3253,9 +3253,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.196" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3275,10 +3275,11 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.113" version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
dependencies = [ dependencies = [
"indexmap 2.0.0",
"itoa 1.0.6", "itoa 1.0.6",
"ryu", "ryu",
"serde", "serde",
@ -3760,14 +3761,15 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.5.4" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" checksum = "0da520ff07c0745199204f7a7a62a8c6ee1666313b792b051ca170eca04649aa"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"cocoa", "cocoa",
"dirs-next", "dirs-next",
"dunce",
"embed_plist", "embed_plist",
"encoding_rs", "encoding_rs",
"flate2", "flate2",
@ -3778,6 +3780,7 @@ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"http", "http",
"ignore", "ignore",
"indexmap 1.9.3",
"objc", "objc",
"once_cell", "once_cell",
"open", "open",
@ -3872,7 +3875,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs-extra" name = "tauri-plugin-fs-extra"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#19aa2204115e7304681cb40faf7512aba525bc5e"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -3904,9 +3907,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "0.14.3" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" checksum = "067c56fc153b3caf406d7cd6de4486c80d1d66c0f414f39e94cb2f5543f6445f"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"gtk", "gtk",
@ -3924,9 +3927,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "1.5.2" version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21"
dependencies = [ dependencies = [
"brotli", "brotli",
"ctor", "ctor",

View File

@ -16,11 +16,11 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.53" kittycad = "0.2.59"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.5.4", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] } tauri = { version = "1.6.0", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.36.0", features = ["time"] } tokio = { version = "1.36.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"

View File

@ -7,7 +7,6 @@ use std::io::Read;
use anyhow::Result; use anyhow::Result;
use oauth2::TokenResponse; use oauth2::TokenResponse;
use std::process::Command;
use tauri::{InvokeError, Manager}; use tauri::{InvokeError, Manager};
const DEFAULT_HOST: &str = "https://api.kittycad.io"; const DEFAULT_HOST: &str = "https://api.kittycad.io";

View File

@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "zoo-modeling-app", "productName": "zoo-modeling-app",
"version": "0.15.3" "version": "0.15.5"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -3,15 +3,8 @@ import {
createBrowserRouter, createBrowserRouter,
Outlet, Outlet,
redirect, redirect,
useLocation,
RouterProvider, RouterProvider,
} from 'react-router-dom' } from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage' import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings' import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding' import Onboarding, { onboardingRoutes } from './routes/Onboarding'
@ -38,8 +31,6 @@ import { ContextFrom } from 'xstate'
import CommandBarProvider, { import CommandBarProvider, {
CommandBar, CommandBar,
} from 'components/CommandBar/CommandBar' } from 'components/CommandBar/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider' import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSingleton' import { KclContextProvider, kclManager } from 'lang/KclSingleton'
import FileMachineProvider from 'components/FileMachineProvider' import FileMachineProvider from 'components/FileMachineProvider'
@ -48,38 +39,6 @@ import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types' import { IndexLoaderData, HomeLoaderData } from 'lib/types'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
dsn: VITE_KC_SENTRY_DSN,
// TODO(paultag): pass in the right env here.
// environment: "production",
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
// TODO: Add in kittycad.io endpoints
tracePropagationTargets: ['localhost'],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
export const BROWSER_FILE_NAME = 'new' export const BROWSER_FILE_NAME = 'new'
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0] type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]

View File

@ -16,7 +16,11 @@ import {
SKETCH_LAYER, SKETCH_LAYER,
ZOOM_MAGIC_NUMBER, ZOOM_MAGIC_NUMBER,
} from './sceneInfra' } from './sceneInfra'
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection' import {
EngineCommand,
Subscription,
engineCommandManager,
} from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
@ -28,6 +32,12 @@ const FRAMES_TO_ANIMATE_IN = 30
const tempQuaternion = new Quaternion() // just used for maths const tempQuaternion = new Quaternion() // just used for maths
type interactionType = 'pan' | 'rotate' | 'zoom'
const throttledEngCmd = throttle((cmd: EngineCommand) => {
engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15)
interface ThreeCamValues { interface ThreeCamValues {
position: Vector3 position: Vector3
quaternion: Quaternion quaternion: Quaternion
@ -110,10 +120,11 @@ const throttledUpdateEngineFov = throttle(
lastCmdDelay lastCmdDelay
) as any as number ) as any as number
}, },
1000 / 15 1000 / 30
) )
export class CameraControls { export class CameraControls {
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
camera: PerspectiveCamera | OrthographicCamera camera: PerspectiveCamera | OrthographicCamera
target: Vector3 target: Vector3
domElement: HTMLCanvasElement domElement: HTMLCanvasElement
@ -151,6 +162,17 @@ export class CameraControls {
get isPerspective() { get isPerspective() {
return this.camera instanceof PerspectiveCamera return this.camera instanceof PerspectiveCamera
} }
private debounceTimer = 0
handleStart = () => {
if (this.debounceTimer) clearTimeout(this.debounceTimer)
this._isCamMovingCallback(true, false)
}
handleEnd = () => {
this.debounceTimer = setTimeout(() => {
this._isCamMovingCallback(false, false)
}, 400) as any as number
}
// reacts hooks into some of this singleton's properties // reacts hooks into some of this singleton's properties
reactCameraProperties: ReactCameraProperties = { reactCameraProperties: ReactCameraProperties = {
@ -187,6 +209,7 @@ export class CameraControls {
this.camera.zoom = camProps.zoom || 1 this.camera.zoom = camProps.zoom || 1
} }
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
console.log('doing this thing', camProps)
this.update(true) this.update(true)
} }
@ -209,6 +232,46 @@ export class CameraControls {
this.onWindowResize() this.onWindowResize()
this.update() this.update()
this._usePerspectiveCamera()
const cb: Subscription<
'default_camera_zoom' | 'camera_drag_end' | 'default_camera_get_settings'
>['callback'] = ({ data, type }) => {
const camSettings = data.settings
this.camera.position.set(
camSettings.pos.x,
camSettings.pos.y,
camSettings.pos.z
)
this.target.set(
camSettings.center.x,
camSettings.center.y,
camSettings.center.z
)
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
this.camera.fov = camSettings.fov_y
} else if (
this.camera instanceof OrthographicCamera &&
camSettings.ortho_scale
) {
this.camera.zoom = camSettings.ortho_scale
}
this.onCameraChange()
}
setTimeout(() => {
engineCommandManager.subscribeTo({
event: 'camera_drag_end',
callback: cb,
})
engineCommandManager.subscribeTo({
event: 'default_camera_zoom',
callback: cb,
})
engineCommandManager.subscribeTo({
event: 'default_camera_get_settings',
callback: cb,
})
})
} }
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void = private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
@ -242,6 +305,21 @@ export class CameraControls {
onMouseDown = (event: MouseEvent) => { onMouseDown = (event: MouseEvent) => {
this.isDragging = true this.isDragging = true
this.mouseDownPosition.set(event.clientX, event.clientY) this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
this.handleStart()
if (this.syncDirection === 'engineToClient') {
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
}
} }
onMouseMove = (event: MouseEvent) => { onMouseMove = (event: MouseEvent) => {
@ -252,36 +330,34 @@ export class CameraControls {
.sub(this.mouseDownPosition) .sub(this.mouseDownPosition)
this.mouseDownPosition.copy(this.mouseNewPosition) this.mouseDownPosition.copy(this.mouseNewPosition)
let state: 'pan' | 'rotate' | 'zoom' = 'pan' const interaction = this.getInteractionType(event)
if (interaction === 'none') return
if (this.interactionGuards.pan.callback(event as any)) { if (this.syncDirection === 'engineToClient') {
if (this.enablePan === false) return throttledEngCmd({
// handleMouseDownPan(event) type: 'modeling_cmd_req',
state = 'pan' cmd: {
} else if (this.interactionGuards.rotate.callback(event as any)) { type: 'camera_drag_move',
if (this.enableRotate === false) return interaction,
// handleMouseDownRotate(event) window: { x: event.clientX, y: event.clientY },
state = 'rotate' },
} else if (this.interactionGuards.zoom.dragCallback(event as any)) { cmd_id: uuidv4(),
if (this.enableZoom === false) return })
// handleMouseDownDolly(event)
state = 'zoom'
} else {
return return
} }
// Implement camera movement logic here based on deltaMove // Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target: // For example, for rotating the camera around the target:
if (state === 'rotate') { if (interaction === 'rotate') {
this.pendingRotation = this.pendingRotation this.pendingRotation = this.pendingRotation
? this.pendingRotation ? this.pendingRotation
: new Vector2() : new Vector2()
this.pendingRotation.x += deltaMove.x this.pendingRotation.x += deltaMove.x
this.pendingRotation.y += deltaMove.y this.pendingRotation.y += deltaMove.y
} else if (state === 'zoom') { } else if (interaction === 'zoom') {
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + deltaMove.y * 0.01 this.pendingZoom *= 1 + deltaMove.y * 0.01
} else if (state === 'pan') { } else if (interaction === 'pan') {
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2() this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
let distance = this.camera.position.distanceTo(this.target) let distance = this.camera.position.distanceTo(this.target)
if (this.camera instanceof OrthographicCamera) { if (this.camera instanceof OrthographicCamera) {
@ -297,15 +373,52 @@ export class CameraControls {
onMouseUp = (event: MouseEvent) => { onMouseUp = (event: MouseEvent) => {
this.isDragging = false this.isDragging = false
this.handleEnd()
if (this.syncDirection === 'engineToClient') {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction,
window: { x: event.clientX, y: event.clientY },
},
cmd_id: uuidv4(),
})
}
} }
onMouseWheel = (event: WheelEvent) => { onMouseWheel = (event: WheelEvent) => {
// Assume trackpad if the deltas are small and integers // Assume trackpad if the deltas are small and integers
this.handleStart()
if (this.syncDirection === 'engineToClient') {
const interactions = this.interactionGuards.zoom.scrollCallback(
event as any
)
if (!interactions) {
this.handleEnd()
return
}
throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: -event.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
this.handleEnd()
return
}
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0 const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed) this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
this.handleEnd()
} }
useOrthographicCamera = () => { useOrthographicCamera = () => {
@ -358,7 +471,7 @@ export class CameraControls {
return this.camera return this.camera
} }
usePerspectiveCamera = () => { _usePerspectiveCamera = () => {
const { x: px, y: py, z: pz } = this.camera.position const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const zoom = this.camera.zoom const zoom = this.camera.zoom
@ -374,14 +487,17 @@ export class CameraControls {
) )
direction.normalize() direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance) this.camera.position.copy(this.target).addScaledVector(direction, distance)
}
usePerspectiveCamera = () => {
this._usePerspectiveCamera()
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'default_camera_set_perspective', type: 'default_camera_set_perspective',
parameters: { parameters: {
fov_y: this.camera.fov, fov_y:
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov), ...calculateNearFarFromFOV(this.lastPerspectiveFov),
}, },
}, },
@ -454,7 +570,7 @@ export class CameraControls {
update = (forceUpdate = false) => { update = (forceUpdate = false) => {
// If there are any changes that need to be applied to the camera, apply them here. // If there are any changes that need to be applied to the camera, apply them here.
let didChange = forceUpdate let didChange = false
if (this.pendingRotation) { if (this.pendingRotation) {
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y) this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
this.pendingRotation = null // Clear the pending rotation after applying it this.pendingRotation = null // Clear the pending rotation after applying it
@ -506,8 +622,8 @@ export class CameraControls {
// Update the camera's matrices // Update the camera's matrices
this.camera.updateMatrixWorld() this.camera.updateMatrixWorld()
if (didChange) { if (didChange || forceUpdate) {
this.onCameraChange() this.onCameraChange(forceUpdate)
} }
// damping would be implemented here in update if we choose to add it. // damping would be implemented here in update if we choose to add it.
@ -618,6 +734,10 @@ export class CameraControls {
duration = 500, duration = 500,
toOrthographic = true toOrthographic = true
): Promise<void> { ): Promise<void> {
if (this.syncDirection === 'engineToClient')
console.warn(
'tweenCameraToQuaternion not design to work with engineToClient syncDirection.'
)
const isVertical = isQuaternionVertical(targetQuaternion) const isVertical = isQuaternionVertical(targetQuaternion)
let remainingDuration = duration let remainingDuration = duration
if (isVertical) { if (isVertical) {
@ -700,6 +820,10 @@ export class CameraControls {
animateToOrthographic = () => animateToOrthographic = () =>
new Promise((resolve) => { new Promise((resolve) => {
if (this.syncDirection === 'engineToClient')
console.warn(
'animate To Orthographic not design to work with engineToClient syncDirection.'
)
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
let currentFov = this.lastPerspectiveFov let currentFov = this.lastPerspectiveFov
this.fovBeforeOrtho = currentFov this.fovBeforeOrtho = currentFov
@ -733,6 +857,10 @@ export class CameraControls {
}) })
animateToPerspective = () => animateToPerspective = () =>
new Promise((resolve) => { new Promise((resolve) => {
if (this.syncDirection === 'engineToClient')
console.warn(
'animate To Perspective not design to work with engineToClient syncDirection.'
)
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
// Immediately set the camera to perspective with a very low FOV // Immediately set the camera to perspective with a very low FOV
const targetFov = this.fovBeforeOrtho // Target FOV for perspective const targetFov = this.fovBeforeOrtho // Target FOV for perspective
@ -771,7 +899,7 @@ export class CameraControls {
this.reactCameraPropertiesCallback(a) this.reactCameraPropertiesCallback(a)
}, 200) }, 200)
onCameraChange = () => { onCameraChange = (forceUpdate = false) => {
const distance = this.target.distanceTo(this.camera.position) const distance = this.target.distanceTo(this.camera.position)
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) { if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
this.camera.far = distance * 2 this.camera.far = distance * 2
@ -779,13 +907,14 @@ export class CameraControls {
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
throttledUpdateEngineCamera({ if (this.syncDirection === 'clientToEngine' || forceUpdate)
quaternion: this.camera.quaternion, throttledUpdateEngineCamera({
position: this.camera.position, quaternion: this.camera.quaternion,
zoom: this.camera.zoom, position: this.camera.position,
isPerspective: this.isPerspective, zoom: this.camera.zoom,
target: this.target, isPerspective: this.isPerspective,
}) target: this.target,
})
this.deferReactUpdate({ this.deferReactUpdate({
type: this.isPerspective ? 'perspective' : 'orthographic', type: this.isPerspective ? 'perspective' : 'orthographic',
[this.isPerspective ? 'fov' : 'zoom']: [this.isPerspective ? 'fov' : 'zoom']:
@ -806,9 +935,18 @@ export class CameraControls {
}) })
Object.values(this._camChangeCallbacks).forEach((cb) => cb()) Object.values(this._camChangeCallbacks).forEach((cb) => cb())
} }
getInteractionType = (event: any) =>
_getInteractionType(
this.interactionGuards,
event,
this.enablePan,
this.enableRotate,
this.enableZoom
)
} }
// currently duplicated, delete one // Pure function helpers
function calculateNearFarFromFOV(fov: number) { function calculateNearFarFromFOV(fov: number) {
const nearFarRatio = (fov - 3) / (45 - 3) const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1) // const z_near = 0.1 + nearFarRatio * (5 - 0.1)
@ -816,7 +954,6 @@ function calculateNearFarFromFOV(fov: number) {
return { z_near: 0.1, z_far } return { z_near: 0.1, z_far }
} }
// currently duplicated, delete one
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
target, target,
position, position,
@ -857,8 +994,6 @@ function convertThreeCamValuesToEngineCam({
} }
} }
// Pure function helpers
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion { function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
// Direction from position to target, normalized. // Direction from position to target, normalized.
let direction = new Vector3().subVectors(target, position).normalize() let direction = new Vector3().subVectors(target, position).normalize()
@ -877,3 +1012,17 @@ function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
return quaternion return quaternion
} }
function _getInteractionType(
interactionGuards: MouseGuard,
event: any,
enablePan: boolean,
enableRotate: boolean,
enableZoom: boolean
): interactionType | 'none' {
let state: interactionType | 'none' = 'none'
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
return state
}

View File

@ -3,10 +3,13 @@ import {
DoubleSide, DoubleSide,
ExtrudeGeometry, ExtrudeGeometry,
Group, Group,
Intersection,
LineCurve3, LineCurve3,
Matrix4, Matrix4,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Object3D,
Object3DEventMap,
OrthographicCamera, OrthographicCamera,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
@ -24,6 +27,7 @@ import {
defaultPlaneColor, defaultPlaneColor,
getSceneScale, getSceneScale,
INTERSECTION_PLANE_LAYER, INTERSECTION_PLANE_LAYER,
OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE, RAYCASTABLE_PLANE,
sceneInfra, sceneInfra,
SKETCH_GROUP_SEGMENTS, SKETCH_GROUP_SEGMENTS,
@ -56,6 +60,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { import {
createArcGeometry, createArcGeometry,
dashedStraight, dashedStraight,
profileStart,
straightSegment, straightSegment,
tangentialArcToSegment, tangentialArcToSegment,
} from './segments' } from './segments'
@ -63,7 +68,7 @@ import {
addCloseToPipe, addCloseToPipe,
addNewSketchLn, addNewSketchLn,
changeSketchArguments, changeSketchArguments,
compareVec2Epsilon2, updateStartProfileAtArgs,
} from 'lang/std/sketch' } from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils' import { isReducedMotion, throttle } from 'lib/utils'
import { import {
@ -85,6 +90,7 @@ export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body' export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH = export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed' 'tangential-arc-to-segment-body-dashed'
export const PROFILE_START = 'profile-start'
// This singleton Class is responsible for all of the things the user sees and interacts with. // This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements. // That mostly mean sketch elements.
@ -106,9 +112,10 @@ class SceneEntities {
Object.values(this.activeSegments).forEach((segment) => { Object.values(this.activeSegments).forEach((segment) => {
const factor = const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera (sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, segment) : perspScale(sceneInfra.camControls.camera, segment)) /
sceneInfra._baseUnitMultiplier
if ( if (
segment.userData.from && segment.userData.from &&
segment.userData.to && segment.userData.to &&
@ -136,6 +143,9 @@ class SceneEntities {
scale: factor, scale: factor,
}) })
} }
if (segment.name === PROFILE_START) {
segment.scale.set(factor, factor, factor)
}
}) })
if (this.axisGroup) { if (this.axisGroup) {
const factor = const factor =
@ -143,9 +153,9 @@ class SceneEntities {
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, this.axisGroup) : perspScale(sceneInfra.camControls.camera, this.axisGroup)
const x = this.axisGroup.getObjectByName(X_AXIS) const x = this.axisGroup.getObjectByName(X_AXIS)
x?.scale.set(1, factor, 1) x?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
const y = this.axisGroup.getObjectByName(Y_AXIS) const y = this.axisGroup.getObjectByName(Y_AXIS)
y?.scale.set(factor, 1, 1) y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
} }
} }
@ -169,6 +179,7 @@ class SceneEntities {
this.scene.add(this.intersectionPlane) this.scene.add(this.intersectionPlane)
} }
createSketchAxis(sketchPathToNode: PathToNode) { createSketchAxis(sketchPathToNode: PathToNode) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055 const baseXColor = 0x000055
const baseYColor = 0x550000 const baseYColor = 0x550000
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01) const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
@ -208,6 +219,14 @@ class SceneEntities {
sceneInfra.camControls.target sceneInfra.camControls.target
) )
gridHelper.scale.set(sceneScale, sceneScale, sceneScale) gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
xAxisMesh?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
yAxisMesh?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper) this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
this.currentSketchQuaternion && this.currentSketchQuaternion &&
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion) this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
@ -279,17 +298,55 @@ class SceneEntities {
) )
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor = const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera (sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, dummy) : perspScale(sceneInfra.camControls.camera, dummy)) /
sceneInfra._baseUnitMultiplier
const segPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sketchGroup.start.__geoMeta.sourceRange
)
const _profileStart = profileStart({
from: sketchGroup.start.from,
id: sketchGroup.start.__geoMeta.id,
pathToNode: segPathToNode,
scale: factor,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
group.add(_profileStart)
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
sketchGroup.value.forEach((segment, index) => { sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange( let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast, kclManager.ast,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
) )
if (draftSegment && (sketchGroup.value[index - 1] || sketchGroup.start)) {
const previousSegment =
sketchGroup.value[index - 1] || sketchGroup.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
kclManager.ast,
previousSegment.__geoMeta.sourceRange
)
const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange(
truncatedAst,
segment.__geoMeta.sourceRange
)
segPathToNode[1][0] = bodyIndex
}
const isDraftSegment = const isDraftSegment =
draftSegment && index === sketchGroup.value.length - 1 draftSegment && index === sketchGroup.value.length - 1
let seg let seg
const callExpName = getNodeFromPath<CallExpression>(
kclManager.ast,
segPathToNode,
'CallExpression'
)?.node?.callee?.name
if (segment.type === 'TangentialArcTo') { if (segment.type === 'TangentialArcTo') {
seg = tangentialArcToSegment({ seg = tangentialArcToSegment({
prevSegment: sketchGroup.value[index - 1], prevSegment: sketchGroup.value[index - 1],
@ -308,6 +365,7 @@ class SceneEntities {
pathToNode: segPathToNode, pathToNode: segPathToNode,
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
callExpName,
}) })
} }
seg.layers.set(SKETCH_LAYER) seg.layers.set(SKETCH_LAYER)
@ -329,17 +387,19 @@ class SceneEntities {
this.scene.add(group) this.scene.add(group)
if (!draftSegment) { if (!draftSegment) {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: (args) => { onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
if (args.event.which !== 1) return if (mouseEvent.which !== 1) return
this.onDragSegment({ this.onDragSegment({
...args, object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
sketchPathToNode, sketchPathToNode,
}) })
}, },
onMove: () => {}, onMove: () => {},
onClick: (args) => { onClick: (args) => {
if (args?.event.which !== 1) return if (args?.mouseEvent.which !== 1) return
if (!args || !args.object) { if (!args || !args.selected) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Set selection', type: 'Set selection',
data: { data: {
@ -348,73 +408,32 @@ class SceneEntities {
}) })
return return
} }
const { object } = args const { selected } = args
const event = getEventForSegmentSelection(object) const event = getEventForSegmentSelection(selected)
if (!event) return if (!event) return
sceneInfra.modelingSend(event) sceneInfra.modelingSend(event)
}, },
onMouseEnter: ({ object }) => { ...mouseEnterLeaveCallbacks(),
// TODO change the color of the segment to yellow?
// Give a few pixels grace around each of the segments
// for hover.
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
const obj = object as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(object)
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(object, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ object }) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(object)
const isSelected = parent?.userData?.isSelected
colorSegment(object, isSelected ? 0x0000ff : 0xffffff)
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
const obj = object as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}) })
} else { } else {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: () => {},
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.event.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersection2d } = args const { intersectionPoint } = args
if (!intersection2d) return let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const firstSeg = sketchGroup.value[0]
const isClosingSketch = compareVec2Epsilon2(
firstSeg.from,
[intersection2d.x, intersection2d.y],
1
)
let modifiedAst let modifiedAst
if (isClosingSketch) { if (profileStart) {
// TODO close needs a better UX
modifiedAst = addCloseToPipe({ modifiedAst = addCloseToPipe({
node: kclManager.ast, node: kclManager.ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}) })
} else { } else if (intersection2d) {
const lastSegment = sketchGroup.value.slice(-1)[0] const lastSegment = sketchGroup.value.slice(-1)[0]
modifiedAst = addNewSketchLn({ modifiedAst = addNewSketchLn({
node: kclManager.ast, node: kclManager.ast,
@ -427,6 +446,9 @@ class SceneEntities {
: 'line', : 'line',
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}).modifiedAst }).modifiedAst
} else {
// return early as we didn't modify the ast
return
} }
kclManager.executeAstMock(modifiedAst, { updates: 'code' }) kclManager.executeAstMock(modifiedAst, { updates: 'code' })
@ -435,8 +457,9 @@ class SceneEntities {
}, },
onMove: (args) => { onMove: (args) => {
this.onDragSegment({ this.onDragSegment({
...args, intersection2d: args.intersectionPoint.twoD,
object: Object.values(this.activeSegments).slice(-1)[0], object: Object.values(this.activeSegments).slice(-1)[0],
intersects: args.intersects,
sketchPathToNode, sketchPathToNode,
draftInfo: { draftInfo: {
draftSegment, draftSegment,
@ -446,6 +469,7 @@ class SceneEntities {
}, },
}) })
}, },
...mouseEnterLeaveCallbacks(),
}) })
} }
sceneInfra.camControls.enableRotate = false sceneInfra.camControls.enableRotate = false
@ -482,17 +506,15 @@ class SceneEntities {
) )
onDragSegment({ onDragSegment({
object, object,
event, intersection2d: _intersection2d,
intersectPoint,
intersection2d,
sketchPathToNode, sketchPathToNode,
draftInfo, draftInfo,
intersects,
}: { }: {
object: any object: any
event: any
intersectPoint: Vector3
intersection2d: Vector2 intersection2d: Vector2
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: { draftInfo?: {
draftSegment: DraftSegment draftSegment: DraftSegment
truncatedAst: Program truncatedAst: Program
@ -500,7 +522,20 @@ class SceneEntities {
variableDeclarationName: string variableDeclarationName: string
} }
}) { }) {
const group = getParentGroup(object) const profileStart =
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const intersection2d = profileStart
? new Vector2(profileStart.position.x, profileStart.position.y)
: _intersection2d
const group = getParentGroup(object, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (!group) return if (!group) return
const pathToNode: PathToNode = JSON.parse( const pathToNode: PathToNode = JSON.parse(
JSON.stringify(group.userData.pathToNode) JSON.stringify(group.userData.pathToNode)
@ -524,13 +559,28 @@ class SceneEntities {
).node ).node
if (node.type !== 'CallExpression') return if (node.type !== 'CallExpression') return
const modded = changeSketchArguments( let modded: {
modifiedAst, modifiedAst: Program
kclManager.programMemory, pathToNode: PathToNode
[node.start, node.end], }
to, if (group.name === PROFILE_START) {
from modded = updateStartProfileAtArgs({
) node: modifiedAst,
pathToNode,
to,
from,
previousProgramMemory: kclManager.programMemory,
})
} else {
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
[node.start, node.end],
to,
from
)
}
modifiedAst = modded.modifiedAst modifiedAst = modded.modifiedAst
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
draftInfo draftInfo
@ -549,10 +599,16 @@ class SceneEntities {
programMemoryOverride, programMemoryOverride,
}) })
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[variableDeclarationName] const sketchGroup = programMemory.root[
.value as Path[] variableDeclarationName
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
sketchGroup.forEach((segment, index) => {
const updateSegment = (
segment: Path | SketchGroup['start'],
index: number
) => {
const segPathToNode = getNodePathFromSourceRange( const segPathToNode = getNodePathFromSourceRange(
modifiedAst, modifiedAst,
segment.__geoMeta.sourceRange segment.__geoMeta.sourceRange
@ -567,12 +623,13 @@ class SceneEntities {
// const prevSegment = sketchGroup.slice(index - 1)[0] // const prevSegment = sketchGroup.slice(index - 1)[0]
const type = group?.userData?.type const type = group?.userData?.type
const factor = const factor =
sceneInfra.camControls.camera instanceof OrthographicCamera (sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, group) : perspScale(sceneInfra.camControls.camera, group)) /
sceneInfra._baseUnitMultiplier
if (type === TANGENTIAL_ARC_TO_SEGMENT) { if (type === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({ this.updateTangentialArcToSegment({
prevSegment: sketchGroup[index - 1], prevSegment: sgPaths[index - 1],
from: segment.from, from: segment.from,
to: segment.to, to: segment.to,
group: group, group: group,
@ -585,8 +642,13 @@ class SceneEntities {
group: group, group: group,
scale: factor, scale: factor,
}) })
} else if (type === PROFILE_START) {
group.position.set(segment.from[0], segment.from[1], 0)
group.scale.set(factor, factor, factor)
} }
}) }
updateSegment(sketchGroup.start, 0)
sgPaths.forEach(updateSegment)
})() })()
} }
@ -606,9 +668,7 @@ class SceneEntities {
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
const arrowGroup = group.children.find( const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
(child) => child.userData.type === ARROWHEAD
) as Group
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -683,20 +743,20 @@ class SceneEntities {
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, -0.08 * scale) shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line shape.lineTo(0, 0.08 * scale) // The width of the line
const arrowGroup = group.children.find( const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
(child) => child.userData.type === ARROWHEAD
) as Group
arrowGroup.position.set(to[0], to[1], 0) if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3() const dir = new Vector3()
.subVectors( .subVectors(
new Vector3(to[0], to[1], 0), new Vector3(to[0], to[1], 0),
new Vector3(from[0], from[1], 0) new Vector3(from[0], from[1], 0)
) )
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
}
const straightSegmentBody = group.children.find( const straightSegmentBody = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY (child) => child.userData.type === STRAIGHT_SEGMENT_BODY
@ -781,22 +841,24 @@ class SceneEntities {
} }
setupDefaultPlaneHover() { setupDefaultPlaneHover() {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onMouseEnter: ({ object }) => { onMouseEnter: ({ selected }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return if (!(selected instanceof Mesh && selected.parent)) return
const type: DefaultPlane = object.userData.type if (selected.parent.userData.type !== DEFAULT_PLANES) return
object.material.color = defaultPlaneColor(type, 0.5, 1) const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type, 0.5, 1)
}, },
onMouseLeave: ({ object }) => { onMouseLeave: ({ selected }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return if (!(selected instanceof Mesh && selected.parent)) return
const type: DefaultPlane = object.userData.type if (selected.parent.userData.type !== DEFAULT_PLANES) return
object.material.color = defaultPlaneColor(type) const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type)
}, },
onClick: (args) => { onClick: (args) => {
if (!args || !args.object) return if (!args || !args.intersects?.[0]) return
if (args.event.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersection } = args const { intersects } = args
const type = intersection.object.name || '' const type = intersects?.[0].object.name || ''
const posNorm = Number(intersection.normal?.z) > 0 const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY' let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1] let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
if (type === YZ_PLANE) { if (type === YZ_PLANE) {
@ -968,9 +1030,9 @@ export function quaternionFromSketchGroup(
} }
function colorSegment(object: any, color: number) { function colorSegment(object: any, color: number) {
const arrowHead = getParentGroup(object, [ARROWHEAD]) const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (arrowHead) { if (segmentHead) {
arrowHead.traverse((child) => { segmentHead.traverse((child) => {
if (child instanceof Mesh) { if (child instanceof Mesh) {
child.material.color.set(color) child.material.color.set(color)
} }
@ -1026,3 +1088,53 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2]) ? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z) : new Vector3(a.x, a.y, a.z)
} }
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -18,7 +18,6 @@ import {
Intersection, Intersection,
Object3D, Object3D,
Object3DEventMap, Object3DEventMap,
BoxGeometry,
} from 'three' } from 'three'
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -49,31 +48,36 @@ export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments' export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead' export const ARROWHEAD = 'arrowhead'
interface BaseCallbackArgs2 { export interface OnMouseEnterLeaveArgs {
object: any selected: Object3D<Object3DEventMap>
event: any mouseEvent: MouseEvent
}
interface BaseCallbackArgs {
event: any
}
interface OnDragCallbackArgs extends BaseCallbackArgs {
object: any
intersection2d: Vector2
intersectPoint: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
}
interface OnClickCallbackArgs extends BaseCallbackArgs {
intersection2d?: Vector2
intersectPoint: Vector3
intersection: Intersection<Object3D<Object3DEventMap>>
object?: any
} }
interface onMoveCallbackArgs { interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
event: any intersectionPoint: {
intersection2d: Vector2 twoD: Vector2
intersectPoint: Vector3 threeD: Vector3
intersection: Intersection<Object3D<Object3DEventMap>> }
intersects: Intersection<Object3D<Object3DEventMap>>[]
}
interface OnClickCallbackArgs {
mouseEvent: MouseEvent
intersectionPoint?: {
twoD: Vector2
threeD: Vector3
}
intersects: Intersection<Object3D<Object3DEventMap>>[]
selected?: Object3D<Object3DEventMap>
}
interface OnMoveCallbackArgs {
mouseEvent: MouseEvent
intersectionPoint: {
twoD: Vector2
threeD: Vector3
}
intersects: Intersection<Object3D<Object3DEventMap>>[]
selected?: Object3D<Object3DEventMap>
} }
// This singleton class is responsible for all of the under the hood setup for the client side scene. // This singleton class is responsible for all of the under the hood setup for the client side scene.
@ -88,17 +92,19 @@ class SceneInfra {
fov = 45 fov = 45
fovBeforeAnimate = 45 fovBeforeAnimate = 45
isFovAnimationInProgress = false isFovAnimationInProgress = false
_baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {} onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {} onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {} onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {} onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
setCallbacks = (callbacks: { setCallbacks = (callbacks: {
onDrag?: (arg: OnDragCallbackArgs) => void onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: onMoveCallbackArgs) => void onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg?: OnClickCallbackArgs) => void onClick?: (arg?: OnClickCallbackArgs) => void
onMouseEnter?: (arg: BaseCallbackArgs2) => void onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: BaseCallbackArgs2) => void onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => { }) => {
this.onDragCallback = callbacks.onDrag || this.onDragCallback this.onDragCallback = callbacks.onDrag || this.onDragCallback
this.onMoveCallback = callbacks.onMove || this.onMoveCallback this.onMoveCallback = callbacks.onMove || this.onMoveCallback
@ -107,6 +113,15 @@ class SceneInfra {
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
this.selected = null // following selections between callbacks being set is too tricky this.selected = null // following selections between callbacks being set is too tricky
} }
set baseUnit(unit: BaseUnit) {
this._baseUnit = unit
this._baseUnitMultiplier = baseUnitTomm(unit)
this.scene.scale.set(
this._baseUnitMultiplier,
this._baseUnitMultiplier,
this._baseUnitMultiplier
)
}
resetMouseListeners = () => { resetMouseListeners = () => {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: () => {}, onDrag: () => {},
@ -132,10 +147,9 @@ class SceneInfra {
currentMouseVector = new Vector2() currentMouseVector = new Vector2()
selected: { selected: {
mouseDownVector: Vector2 mouseDownVector: Vector2
object: any object: Object3D<Object3DEventMap>
hasBeenDragged: boolean hasBeenDragged: boolean
} | null = null } | null = null
selectedObject: null | any = null
mouseDownVector: null | Vector2 = null mouseDownVector: null | Vector2 = null
constructor() { constructor() {
@ -202,7 +216,12 @@ class SceneInfra {
const axisGroup = this.scene const axisGroup = this.scene
.getObjectByName(AXIS_GROUP) .getObjectByName(AXIS_GROUP)
?.getObjectByName('gridHelper') ?.getObjectByName('gridHelper')
planesGroup && planesGroup.scale.set(scale, scale, scale) planesGroup &&
planesGroup.scale.set(
scale / sceneInfra._baseUnitMultiplier,
scale / sceneInfra._baseUnitMultiplier,
scale / sceneInfra._baseUnitMultiplier
)
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale) axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
} }
@ -227,8 +246,8 @@ class SceneInfra {
// Dispose of any other resources like geometries, materials, textures // Dispose of any other resources like geometries, materials, textures
} }
getPlaneIntersectPoint = (): { getPlaneIntersectPoint = (): {
intersection2d?: Vector2 twoD?: Vector2
intersectPoint: Vector3 threeD?: Vector3
intersection: Intersection<Object3D<Object3DEventMap>> intersection: Intersection<Object3D<Object3DEventMap>>
} | null => { } | null => {
this.planeRaycaster.setFromCamera( this.planeRaycaster.setFromCamera(
@ -239,23 +258,11 @@ class SceneInfra {
this.scene.children, this.scene.children,
true true
) )
if ( const recastablePlaneIntersect = planeIntersects.find(
planeIntersects.length > 0 && (intersect) => intersect.object.name === RAYCASTABLE_PLANE
planeIntersects[0].object.userData.type !== RAYCASTABLE_PLANE
) {
const intersect = planeIntersects[0]
return {
intersectPoint: intersect.point,
intersection: intersect,
}
}
if (
!(
planeIntersects.length > 0 &&
planeIntersects[0].object.userData.type === RAYCASTABLE_PLANE
)
) )
return null if (!planeIntersects.length) return null
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
const planePosition = planeIntersects[0].object.position const planePosition = planeIntersects[0].object.position
const inversePlaneQuaternion = planeIntersects[0].object.quaternion const inversePlaneQuaternion = planeIntersects[0].object.quaternion
.clone() .clone()
@ -270,16 +277,21 @@ class SceneInfra {
} }
return { return {
intersection2d: new Vector2(transformedPoint.x, transformedPoint.y), // z should be 0 twoD: new Vector2(
intersectPoint, transformedPoint.x / this._baseUnitMultiplier,
transformedPoint.y / this._baseUnitMultiplier
), // z should be 0
threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
intersection: planeIntersects[0], intersection: planeIntersects[0],
} }
} }
onMouseMove = (event: MouseEvent) => { onMouseMove = (mouseEvent: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1 this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1 this.currentMouseVector.y =
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
const planeIntersectPoint = this.getPlaneIntersectPoint() const planeIntersectPoint = this.getPlaneIntersectPoint()
const intersects = this.raycastRing()
if (this.selected) { if (this.selected) {
const hasBeenDragged = !compareVec2Epsilon2( const hasBeenDragged = !compareVec2Epsilon2(
@ -295,47 +307,56 @@ class SceneInfra {
if ( if (
hasBeenDragged && hasBeenDragged &&
planeIntersectPoint && planeIntersectPoint &&
planeIntersectPoint.intersection2d planeIntersectPoint.twoD &&
planeIntersectPoint.threeD
) { ) {
// // console.log('onDrag', this.selected) // // console.log('onDrag', this.selected)
this.onDragCallback({ this.onDragCallback({
object: this.selected.object, mouseEvent,
event, intersectionPoint: {
intersection2d: planeIntersectPoint.intersection2d, twoD: planeIntersectPoint.twoD,
...planeIntersectPoint, threeD: planeIntersectPoint.threeD,
},
intersects,
selected: this.selected.object,
}) })
} }
} else if (planeIntersectPoint && planeIntersectPoint.intersection2d) { } else if (
planeIntersectPoint &&
planeIntersectPoint.twoD &&
planeIntersectPoint.threeD
) {
this.onMoveCallback({ this.onMoveCallback({
event, mouseEvent,
intersection2d: planeIntersectPoint.intersection2d, intersectionPoint: {
...planeIntersectPoint, twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
}) })
} }
const intersect = this.raycastRing() if (intersects[0]) {
const firstIntersectObject = intersects[0].object
if (intersect) {
const firstIntersectObject = intersect.object
if (this.hoveredObject !== firstIntersectObject) { if (this.hoveredObject !== firstIntersectObject) {
if (this.hoveredObject) { if (this.hoveredObject) {
this.onMouseLeave({ this.onMouseLeave({
object: this.hoveredObject, selected: this.hoveredObject,
event, mouseEvent: mouseEvent,
}) })
} }
this.hoveredObject = firstIntersectObject this.hoveredObject = firstIntersectObject
this.onMouseEnter({ this.onMouseEnter({
object: this.hoveredObject, selected: this.hoveredObject,
event, mouseEvent: mouseEvent,
}) })
} }
} else { } else {
if (this.hoveredObject) { if (this.hoveredObject) {
this.onMouseLeave({ this.onMouseLeave({
object: this.hoveredObject, selected: this.hoveredObject,
event, mouseEvent: mouseEvent,
}) })
this.hoveredObject = null this.hoveredObject = null
} }
@ -345,41 +366,38 @@ class SceneInfra {
raycastRing = ( raycastRing = (
pixelRadius = 8, pixelRadius = 8,
rayRingCount = 32 rayRingCount = 32
): Intersection<Object3D<Object3DEventMap>> | undefined => { ): Intersection<Object3D<Object3DEventMap>>[] => {
const mouseDownVector = this.currentMouseVector.clone() const mouseDownVector = this.currentMouseVector.clone()
let closestIntersection: const intersectionsMap = new Map<
| Intersection<Object3D<Object3DEventMap>> Object3D,
| undefined = undefined Intersection<Object3D<Object3DEventMap>>
let closestDistance = Infinity >()
const updateClosestIntersection = ( const updateIntersectionsMap = (
intersections: Intersection<Object3D<Object3DEventMap>>[] intersections: Intersection<Object3D<Object3DEventMap>>[]
) => { ) => {
let intersection = null intersections.forEach((intersection) => {
for (let i = 0; i < intersections.length; i++) { if (intersection.object.type !== 'GridHelper') {
if (intersections[i].object.type !== 'GridHelper') { const existingIntersection = intersectionsMap.get(intersection.object)
intersection = intersections[i] if (
break !existingIntersection ||
existingIntersection.distance > intersection.distance
) {
intersectionsMap.set(intersection.object, intersection)
}
} }
} })
if (!intersection) return
if (intersection.distance < closestDistance) {
closestDistance = intersection.distance
closestIntersection = intersection
}
} }
// Check the center point // Check the center point
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera) this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
updateClosestIntersection( updateIntersectionsMap(
this.raycaster.intersectObjects(this.scene.children, true) this.raycaster.intersectObjects(this.scene.children, true)
) )
// Check the ring points // Check the ring points
for (let i = 0; i < rayRingCount; i++) { for (let i = 0; i < rayRingCount; i++) {
const angle = (i / rayRingCount) * Math.PI * 2 const angle = (i / rayRingCount) * Math.PI * 2
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2 const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2 const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
const ringVector = new Vector2( const ringVector = new Vector2(
@ -387,11 +405,15 @@ class SceneInfra {
mouseDownVector.y - offsetY mouseDownVector.y - offsetY
) )
this.raycaster.setFromCamera(ringVector, this.camControls.camera) this.raycaster.setFromCamera(ringVector, this.camControls.camera)
updateClosestIntersection( updateIntersectionsMap(
this.raycaster.intersectObjects(this.scene.children, true) this.raycaster.intersectObjects(this.scene.children, true)
) )
} }
return closestIntersection
// Convert the map values to an array and sort by distance
return Array.from(intersectionsMap.values()).sort(
(a, b) => a.distance - b.distance
)
} }
onMouseDown = (event: MouseEvent) => { onMouseDown = (event: MouseEvent) => {
@ -399,45 +421,60 @@ class SceneInfra {
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1 this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
const mouseDownVector = this.currentMouseVector.clone() const mouseDownVector = this.currentMouseVector.clone()
const intersect = this.raycastRing() const intersect = this.raycastRing()[0]
if (intersect) { if (intersect) {
const intersectParent = intersect?.object?.parent as Group const intersectParent = intersect?.object?.parent as Group
this.selected = intersectParent.isGroup this.selected = intersectParent.isGroup
? { ? {
mouseDownVector, mouseDownVector,
object: intersect?.object, object: intersect.object,
hasBeenDragged: false, hasBeenDragged: false,
} }
: null : null
} }
} }
onMouseUp = (event: MouseEvent) => { onMouseUp = (mouseEvent: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1 this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1 this.currentMouseVector.y =
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
const planeIntersectPoint = this.getPlaneIntersectPoint() const planeIntersectPoint = this.getPlaneIntersectPoint()
const intersects = this.raycastRing()
if (this.selected) { if (this.selected) {
if (this.selected.hasBeenDragged) { if (this.selected.hasBeenDragged) {
// this is where we could fire a onDragEnd event // this is where we could fire a onDragEnd event
// console.log('onDragEnd', this.selected) // console.log('onDragEnd', this.selected)
} else if (planeIntersectPoint) { } else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
// fire onClick event as there was no drags // fire onClick event as there was no drags
this.onClickCallback({ this.onClickCallback({
object: this.selected?.object, mouseEvent,
event, intersectionPoint: {
...planeIntersectPoint, twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
selected: this.selected.object,
})
} else if (planeIntersectPoint) {
this.onClickCallback({
mouseEvent,
intersects,
}) })
} else { } else {
this.onClickCallback() this.onClickCallback()
} }
// Clear the selected state whether it was dragged or not // Clear the selected state whether it was dragged or not
this.selected = null this.selected = null
} else if (planeIntersectPoint) { } else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
this.onClickCallback({ this.onClickCallback({
event, mouseEvent,
...planeIntersectPoint, intersectionPoint: {
twoD: planeIntersectPoint.twoD,
threeD: planeIntersectPoint.threeD,
},
intersects,
}) })
} else { } else {
this.onClickCallback() this.onClickCallback()
@ -483,7 +520,11 @@ class SceneInfra {
this.camControls.camera, this.camControls.camera,
this.camControls.target this.camControls.target
) )
planesGroup.scale.set(sceneScale, sceneScale, sceneScale) planesGroup.scale.set(
sceneScale / sceneInfra._baseUnitMultiplier,
sceneScale / sceneInfra._baseUnitMultiplier,
sceneScale / sceneInfra._baseUnitMultiplier
)
this.scene.add(planesGroup) this.scene.add(planesGroup)
} }
removeDefaultPlanes() { removeDefaultPlanes() {

View File

@ -1,5 +1,6 @@
import { Coords2d } from 'lang/std/sketch' import { Coords2d } from 'lang/std/sketch'
import { import {
BoxGeometry,
BufferGeometry, BufferGeometry,
CatmullRomCurve3, CatmullRomCurve3,
ConeGeometry, ConeGeometry,
@ -19,6 +20,7 @@ import {
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import { import {
PROFILE_START,
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH, STRAIGHT_SEGMENT_DASH,
@ -29,6 +31,38 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './sceneInfra' import { ARROWHEAD } from './sceneInfra'
export function profileStart({
from,
id,
pathToNode,
scale = 1,
}: {
from: Coords2d
id: string
pathToNode: PathToNode
scale?: number
}) {
const group = new Group()
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
group.add(mesh)
group.userData = {
type: PROFILE_START,
id,
from,
pathToNode,
isSelected: false,
}
group.name = PROFILE_START
group.position.set(from[0], from[1], 0)
group.scale.set(scale, scale, scale)
return group
}
export function straightSegment({ export function straightSegment({
from, from,
to, to,
@ -36,6 +70,7 @@ export function straightSegment({
pathToNode, pathToNode,
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
callExpName,
}: { }: {
from: Coords2d from: Coords2d
to: Coords2d to: Coords2d
@ -43,6 +78,7 @@ export function straightSegment({
pathToNode: PathToNode pathToNode: PathToNode
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
callExpName: string
}): Group { }): Group {
const group = new Group() const group = new Group()
@ -66,7 +102,8 @@ export function straightSegment({
}) })
} }
const body = new MeshBasicMaterial({ color: 0xffffff }) const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff
const body = new MeshBasicMaterial({ color: baseColor })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment mesh.userData.type = isDraftSegment
? STRAIGHT_SEGMENT_DASH ? STRAIGHT_SEGMENT_DASH
@ -80,7 +117,10 @@ export function straightSegment({
to, to,
pathToNode, pathToNode,
isSelected: false, isSelected: false,
callExpName,
baseColor,
} }
group.name = STRAIGHT_SEGMENT
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -89,7 +129,8 @@ export function straightSegment({
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
group.add(mesh, arrowGroup) group.add(mesh)
if (callExpName !== 'close') group.add(arrowGroup)
return group return group
} }
@ -169,6 +210,7 @@ export function tangentialArcToSegment({
pathToNode, pathToNode,
isSelected: false, isSelected: false,
} }
group.name = TANGENTIAL_ARC_TO_SEGMENT
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)

View File

@ -1,8 +1,8 @@
import { Combobox } from '@headlessui/react' import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgumentOption } from 'lib/commandTypes' import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
function CommandArgOptionInput({ function CommandArgOptionInput({
options, options,
@ -11,51 +11,89 @@ function CommandArgOptionInput({
onSubmit, onSubmit,
placeholder, placeholder,
}: { }: {
options: CommandArgumentOption<unknown>[] options: (CommandArgument<unknown> & { inputType: 'options' })['options']
argName: string argName: string
stepBack: () => void stepBack: () => void
onSubmit: (data: unknown) => void onSubmit: (data: unknown) => void
placeholder?: string placeholder?: string
}) { }) {
const { commandBarSend, commandBarState } = useCommandsContext() const { commandBarSend, commandBarState } = useCommandsContext()
const resolvedOptions = useMemo(
() =>
typeof options === 'function'
? options(commandBarState.context)
: options,
[argName, options, commandBarState.context]
)
// The initial current option is either an already-input value or the configured default
const currentOption = useMemo(
() =>
resolvedOptions.find(
(o) => o.value === commandBarState.context.argumentsToSubmit[argName]
) || resolvedOptions.find((o) => o.isCurrent),
[commandBarState.context.argumentsToSubmit, argName, resolvedOptions]
)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null) const formRef = useRef<HTMLFormElement>(null)
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>( const [selectedOption, setSelectedOption] = useState<
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value || CommandArgumentOption<unknown>
commandBarState.context.argumentsToSubmit[argName] || >(currentOption || resolvedOptions[0])
options[0].value const initialQuery = useMemo(() => '', [options, argName])
const [query, setQuery] = useState(initialQuery)
const [filteredOptions, setFilteredOptions] =
useState<typeof resolvedOptions>()
// Create a new Fuse instance when the options change
const fuse = useMemo(
() =>
new Fuse(resolvedOptions, {
keys: ['name', 'description'],
threshold: 0.3,
}),
[argName, resolvedOptions]
) )
const [query, setQuery] = useState('')
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
const fuse = new Fuse(options, { // Reset the query and selected option when the argName changes
keys: ['name', 'description'], useEffect(() => {
threshold: 0.3, setQuery(initialQuery)
}) setSelectedOption(currentOption || resolvedOptions[0])
}, [argName])
// Auto focus and select the input when the component mounts
useEffect(() => { useEffect(() => {
inputRef.current?.focus() inputRef.current?.focus()
inputRef.current?.select() inputRef.current?.select()
}, [inputRef]) }, [inputRef])
// Filter the options based on the query,
// resetting the query when the options change
useEffect(() => { useEffect(() => {
const results = fuse.search(query).map((result) => result.item) const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options) setFilteredOptions(query.length > 0 ? results : resolvedOptions)
}, [query]) }, [query, resolvedOptions, fuse])
function handleSelectOption(option: CommandArgumentOption<unknown>) { function handleSelectOption(option: CommandArgumentOption<unknown>) {
setArgValue(option) // We deal with the whole option object internally
setSelectedOption(option)
// But we only submit the value
onSubmit(option.value) onSubmit(option.value)
} }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault() e.preventDefault()
onSubmit(argValue)
// We submit the value of the selected option, not the whole object
onSubmit(selectedOption.value)
} }
return ( return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}> <form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<Combobox value={argValue} onChange={handleSelectOption} name="options"> <Combobox
value={selectedOption}
onChange={handleSelectOption}
name="options"
>
<div className="flex items-center mx-4 mt-4 mb-2"> <div className="flex items-center mx-4 mt-4 mb-2">
<label <label
htmlFor="option-input" htmlFor="option-input"
@ -75,10 +113,12 @@ function CommandArgOptionInput({
stepBack() stepBack()
} }
}} }}
value={query}
placeholder={ placeholder={
(argValue as CommandArgumentOption<unknown>)?.name || currentOption?.name ||
placeholder || placeholder ||
'Select an option for ' + argName argName ||
'Select an option'
} }
autoCapitalize="off" autoCapitalize="off"
autoComplete="off" autoComplete="off"
@ -98,7 +138,7 @@ function CommandArgOptionInput({
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90" className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
> >
<p className="flex-grow">{option.name} </p> <p className="flex-grow">{option.name} </p>
{'isCurrent' in option && option.isCurrent && ( {option.value === currentOption?.value && (
<small className="text-chalkboard-70 dark:text-chalkboard-50"> <small className="text-chalkboard-70 dark:text-chalkboard-50">
current current
</small> </small>

View File

@ -29,12 +29,6 @@ export const CommandBarProvider = ({
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, { const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
devTools: true, devTools: true,
guards: { guards: {
'Arguments are ready': (context, _) => {
return context.selectedCommand?.args
? context.argumentsToSubmit.length ===
Object.keys(context.selectedCommand.args)?.length
: false
},
'Command has no arguments': (context, _event) => { 'Command has no arguments': (context, _event) => {
return ( return (
!context.selectedCommand?.args || !context.selectedCommand?.args ||
@ -81,7 +75,12 @@ export const CommandBar = () => {
function stepBack() { function stepBack() {
if (!currentArgument) { if (!currentArgument) {
if (commandBarState.matches('Review')) { if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {}) const entries = Object.entries(selectedCommand?.args || {}).filter(
([_, argConfig]) =>
typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required
)
const currentArgName = entries[entries.length - 1][0] const currentArgName = entries[entries.length - 1][0]
const currentArg = { const currentArg = {
@ -89,19 +88,12 @@ export const CommandBar = () => {
...entries[entries.length - 1][1], ...entries[entries.length - 1][1],
} }
if (commandBarState.matches('Review')) { commandBarSend({
commandBarSend({ type: 'Edit argument',
type: 'Edit argument', data: {
data: { arg: currentArg,
arg: currentArg, },
}, })
})
} else {
commandBarSend({
type: 'Remove argument',
data: { [currentArgName]: currentArg },
})
}
} else { } else {
commandBarSend({ type: 'Deselect command' }) commandBarSend({ type: 'Deselect command' })
} }
@ -124,11 +116,6 @@ export const CommandBar = () => {
} }
} }
useEffect(
() => console.log(commandBarState.context.argumentsToSubmit),
[commandBarState.context.argumentsToSubmit]
)
return ( return (
<Transition.Root <Transition.Root
show={!commandBarState.matches('Closed') || false} show={!commandBarState.matches('Closed') || false}
@ -159,6 +146,7 @@ export const CommandBar = () => {
<WrapperComponent.Panel <WrapperComponent.Panel
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70" className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div" as="div"
data-testid="command-bar"
> >
{commandBarState.matches('Selecting command') ? ( {commandBarState.matches('Selecting command') ? (
<CommandComboBox options={commands} /> <CommandComboBox options={commands} />

View File

@ -1,6 +1,6 @@
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon' import { CustomIcon } from '../CustomIcon'
import React, { ReactNode, useState } from 'react' import React, { useState } from 'react'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections' import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -76,72 +76,87 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
)} )}
{selectedCommand?.name} {selectedCommand?.name}
</p> </p>
{Object.entries(selectedCommand?.args || {}).map( {Object.entries(selectedCommand?.args || {})
([argName, arg], i) => ( .filter(([_, argConfig]) =>
<button typeof argConfig.required === 'function'
disabled={!isReviewing && currentArgument?.name === argName} ? argConfig.required(commandBarState.context)
onClick={() => { : argConfig.required
commandBarSend({
type: isReviewing
? 'Edit argument'
: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
}}
key={argName}
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
argName === currentArgument?.name
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
}`}
>
<span className="capitalize">{argName}</span>
{argumentsToSubmit[argName] ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(
argumentsToSubmit[argName] as Selections
)
) : arg.inputType === 'kcl' ? (
roundOff(
Number(
(argumentsToSubmit[argName] as KclCommandValue)
.valueCalculated
),
4
)
) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argumentsToSubmit[argName])
) : (
<em>{argumentsToSubmit[argName] as ReactNode}</em>
)
) : null}
{showShortcuts && (
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
<span className="sr-only">Hotkey: </span>
{i + 1}
</small>
)}
{arg.inputType === 'kcl' &&
!!argumentsToSubmit[argName] &&
'variableName' in
(argumentsToSubmit[argName] as KclCommandValue) && (
<>
<CustomIcon name="make-variable" className="w-4 h-4" />
<Tooltip position="blockEnd">
New variable:{' '}
{
(
argumentsToSubmit[
argName
] as KclExpressionWithVariable
).variableName
}
</Tooltip>
</>
)}
</button>
) )
)} .map(([argName, arg], i) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? (argumentsToSubmit[argName] as Function)(
commandBarState.context
)
: argumentsToSubmit[argName]) || ''
return (
<button
disabled={!isReviewing && currentArgument?.name === argName}
onClick={() => {
commandBarSend({
type: isReviewing
? 'Edit argument'
: 'Change current argument',
data: { arg: { ...arg, name: argName } },
})
}}
key={argName}
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
argName === currentArgument?.name
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
}`}
>
<span
data-testid={`arg-name-${argName.toLowerCase()}`}
className="capitalize"
>
{argName}
</span>
{argValue ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections)
) : arg.inputType === 'kcl' ? (
roundOff(
Number((argValue as KclCommandValue).valueCalculated),
4
)
) : typeof argValue === 'object' ? (
JSON.stringify(argValue)
) : (
<em>{argValue}</em>
)
) : null}
{showShortcuts && (
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
<span className="sr-only">Hotkey: </span>
{i + 1}
</small>
)}
{arg.inputType === 'kcl' &&
!!argValue &&
'variableName' in (argValue as KclCommandValue) && (
<>
<CustomIcon
name="make-variable"
className="w-4 h-4"
/>
<Tooltip position="blockEnd">
New variable:{' '}
{
(
argumentsToSubmit[
argName
] as KclExpressionWithVariable
).variableName
}
</Tooltip>
</>
)}
</button>
)
})}
</div> </div>
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />} {isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
</div> </div>

View File

@ -48,7 +48,8 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
if (!arg) return if (!arg) return
}) })
function submitCommand() { function submitCommand(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
commandBarSend({ commandBarSend({
type: 'Submit command', type: 'Submit command',
data: argumentsToSubmit, data: argumentsToSubmit,

View File

@ -29,7 +29,7 @@ function CommandBarSelectionInput({
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.actor, selectionSelector) const selection = useSelector(arg.machineActor, selectionSelector)
const [selectionsByType, setSelectionsByType] = useState< const [selectionsByType, setSelectionsByType] = useState<
'none' | ResolvedSelectionType[] 'none' | ResolvedSelectionType[]
>( >(

View File

@ -9,6 +9,7 @@ export type CustomIconName =
| 'clipboardCheckmark' | 'clipboardCheckmark'
| 'close' | 'close'
| 'equal' | 'equal'
| 'exportFile'
| 'extrude' | 'extrude'
| 'file' | 'file'
| 'filePlus' | 'filePlus'
@ -17,6 +18,7 @@ export type CustomIconName =
| 'gear' | 'gear'
| 'horizontal' | 'horizontal'
| 'horizontalDash' | 'horizontalDash'
| 'kcl'
| 'line' | 'line'
| 'make-variable' | 'make-variable'
| 'move' | 'move'
@ -194,6 +196,22 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'exportFile':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3124L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3124L16.3904 14.8124L16.6403 14.5L16.3904 14.1877Z"
fill="currentColor"
/>
</svg>
)
case 'extrude': case 'extrude':
return ( return (
<svg <svg
@ -322,6 +340,22 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'kcl':
return (
<svg
{...props}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
fill="currentColor"
/>
</svg>
)
case 'line': case 'line':
return ( return (
<svg <svg

View File

@ -1,238 +0,0 @@
import { v4 as uuidv4 } from 'uuid'
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from './ActionButton'
import Modal from 'react-modal'
import React from 'react'
import { useFormik } from 'formik'
import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>
export interface ExportButtonProps extends React.PropsWithChildren {
className?: {
button?: string
icon?: string
bg?: string
}
}
export const ExportButton = ({ children, className }: ExportButtonProps) => {
const [modalIsOpen, setIsOpen] = React.useState(false)
const {
settings: {
state: {
context: { baseUnit },
},
},
} = useGlobalStateContext()
const defaultType = 'gltf'
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
const defaultStorage = 'embedded'
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
function openModal() {
setIsOpen(true)
}
function closeModal() {
setIsOpen(false)
}
// Default to gltf and embedded.
const initialValues: OutputFormat = {
type: defaultType,
storage: defaultStorage,
presentation: 'pretty',
}
const formik = useFormik({
initialValues,
onSubmit: (values: OutputFormat) => {
// Set the default coords.
if (
values.type === 'obj' ||
values.type === 'ply' ||
values.type === 'step' ||
values.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
values.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
values.type === 'obj' ||
values.type === 'stl' ||
values.type === 'ply'
) {
values.units = baseUnit
}
if (
values.type === 'ply' ||
values.type === 'stl' ||
values.type === 'gltf'
) {
// Set the storage type.
values.storage = storage
}
if (values.type === 'ply' || values.type === 'stl') {
values.selection = { type: 'default_scene' }
}
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
// By default let's leave this blank to export the whole scene.
// In the future we might want to let the user choose which entities
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format: values,
source_unit: baseUnit,
},
cmd_id: uuidv4(),
})
closeModal()
},
})
return (
<>
<ActionButton
onClick={openModal}
Element="button"
icon={{
icon: faFileExport,
className: 'p-1',
size: 'sm',
iconClassName: className?.icon,
bgClassName: className?.bg,
}}
className={className?.button}
>
{children || 'Export'}
</ActionButton>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Export"
overlayClassName="z-40 fixed inset-0 grid place-items-center"
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
>
<h1 className="text-2xl font-bold">Export your design</h1>
<form onSubmit={formik.handleSubmit}>
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
<label htmlFor="type" className="flex-1">
<p className="mb-2">Type</p>
<select
id="type"
name="type"
data-testid="export-type"
onChange={(e) => {
setType(e.target.value as OutputTypeKey)
if (e.target.value === 'gltf') {
// Set default to embedded.
setStorage('embedded')
} else if (e.target.value === 'ply') {
// Set default to ascii.
setStorage('ascii')
} else if (e.target.value === 'stl') {
// Set default to ascii.
setStorage('ascii')
}
formik.handleChange(e)
}}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
<option value="gltf">gltf</option>
<option value="obj">obj</option>
<option value="ply">ply</option>
<option value="step">step</option>
<option value="stl">stl</option>
</select>
</label>
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
<label htmlFor="storage" className="flex-1">
<p className="mb-2">Storage</p>
<select
id="storage"
name="storage"
data-testid="export-storage"
onChange={(e) => {
setStorage(e.target.value as StorageUnion)
formik.handleChange(e)
}}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
{type === 'gltf' && (
<>
<option value="embedded">embedded</option>
<option value="binary">binary</option>
<option value="standard">standard</option>
</>
)}
{type === 'stl' && (
<>
<option value="ascii">ascii</option>
<option value="binary">binary</option>
</>
)}
{type === 'ply' && (
<>
<option value="ascii">ascii</option>
<option value="binary_little_endian">
binary_little_endian
</option>
<option value="binary_big_endian">
binary_big_endian
</option>
</>
)}
</select>
</label>
)}
</div>
<div className="flex justify-between mt-6">
<ActionButton
Element="button"
onClick={closeModal}
icon={{
icon: faXmark,
className: 'p-1',
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="hover:border-destroy-40"
>
Close
</ActionButton>
<ActionButton
Element="button"
type="submit"
icon={{ icon: faFileExport, className: 'p-1' }}
>
Export
</ActionButton>
</div>
</form>
</Modal>
</>
)
}

View File

@ -50,10 +50,7 @@ export const FileMachineProvider = ({
selectedDirectory: project, selectedDirectory: project,
}, },
actions: { actions: {
navigateToFile: ( navigateToFile: (context, event) => {
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine>
) => {
if (event.data && 'name' in event.data) { if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' }) commandBarSend({ type: 'Close' })
navigate( navigate(
@ -77,10 +74,7 @@ export const FileMachineProvider = ({
children: newFiles, children: newFiles,
} }
}, },
createFile: async ( createFile: async (context, event) => {
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Create file'>
) => {
let name = event.data.name.trim() || DEFAULT_FILE_NAME let name = event.data.name.trim() || DEFAULT_FILE_NAME
if (event.data.makeDir) { if (event.data.makeDir) {

View File

@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'
import { Dispatch, useRef, useState } from 'react' import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react' import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -11,7 +11,10 @@ import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './FileTree.module.css' import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS' import { FILE_EXT, sortProject } from 'lib/tauriFS'
import { CustomIcon } from './CustomIcon'
import { kclManager } from 'lang/KclSingleton'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -157,13 +160,23 @@ const FileTreeItem = ({
// Show the renaming form // Show the renaming form
setIsRenaming(true) setIsRenaming(true)
} else if (e.code === 'Space') { } else if (e.code === 'Space') {
openFile() handleDoubleClick()
} }
} }
function openFile() { function handleDoubleClick() {
if (fileOrDir.children !== undefined) return // Don't open directories if (fileOrDir.children !== undefined) return // Don't open directories
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
// Import non-kcl files
kclManager.setCodeAndExecute(
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
kclManager.code
)
} else {
// Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
}
closePanel() closePanel()
} }
@ -180,11 +193,12 @@ const FileTreeItem = ({
<button <button
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit" className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
onDoubleClick={openFile} onDoubleClick={handleDoubleClick}
onClick={(e) => e.currentTarget.focus()} onClick={(e) => e.currentTarget.focus()}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
> >
<KclIcon <CustomIcon
name={fileOrDir.name?.endsWith(FILE_EXT) ? 'kcl' : 'file'}
className={ className={
'inline-block w-3 ' + 'inline-block w-3 ' +
(isCurrentFile (isCurrentFile
@ -313,9 +327,15 @@ export const FileTree = ({
closePanel, closePanel,
}: FileTreeProps) => { }: FileTreeProps) => {
const { send, context } = useFileContext() const { send, context } = useFileContext()
const docuemntHasFocus = useDocumentHasFocus()
useHotkeys('meta + n', createFile) useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder) useHotkeys('meta + shift + n', createFolder)
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [docuemntHasFocus])
async function createFile() { async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } }) send({ type: 'Create file', data: { name: '', makeDir: false } })
} }
@ -381,21 +401,3 @@ export const FileTree = ({
</div> </div>
) )
} }
function KclIcon({ className = '' }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -18,6 +18,7 @@ import {
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { sceneInfra } from 'clientSideScene/sceneInfra'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -56,27 +57,30 @@ export const GlobalStateProvider = ({
> >
) )
const [settingsState, settingsSend] = useMachine(settingsMachine, { const [settingsState, settingsSend, settingsActor] = useMachine(
context: persistedSettings, settingsMachine,
actions: { {
toastSuccess: (context, event) => { context: persistedSettings,
const truncatedNewValue = actions: {
'data' in event && event.data instanceof Object toastSuccess: (context, event) => {
? (context[Object.keys(event.data)[0] as keyof typeof context] const truncatedNewValue =
.toString() 'data' in event && event.data instanceof Object
.substring(0, 28) as any) ? (String(
: undefined context[Object.keys(event.data)[0] as keyof typeof context]
toast.success( ).substring(0, 28) as any)
event.type + : undefined
(truncatedNewValue toast.success(
? ` to "${truncatedNewValue}${ event.type +
truncatedNewValue.length === 28 ? '...' : '' (truncatedNewValue
}"` ? ` to "${truncatedNewValue}${
: '') truncatedNewValue.length === 28 ? '...' : ''
) }"`
: '')
)
},
}, },
}, }
}) )
settingsStateRef = settingsState.context settingsStateRef = settingsState.context
useStateMachineCommands({ useStateMachineCommands({
@ -84,6 +88,7 @@ export const GlobalStateProvider = ({
state: settingsState, state: settingsState,
send: settingsSend, send: settingsSend,
commandBarConfig: settingsCommandBarConfig, commandBarConfig: settingsCommandBarConfig,
actor: settingsActor,
}) })
// Listen for changes to the system theme and update the app theme accordingly // Listen for changes to the system theme and update the app theme accordingly
@ -97,13 +102,14 @@ export const GlobalStateProvider = ({
if (settingsState.context.theme !== 'system') return if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light) setThemeClass(e.matches ? Themes.Dark : Themes.Light)
} }
sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm'
matcher.addEventListener('change', listener) matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener) return () => matcher.removeEventListener('change', listener)
}, [settingsState.context]) }, [settingsState.context])
// Auth machine setup // Auth machine setup
const [authState, authSend] = useMachine(authMachine, { const [authState, authSend, authActor] = useMachine(authMachine, {
actions: { actions: {
goToSignInPage: () => { goToSignInPage: () => {
navigate(paths.SIGN_IN) navigate(paths.SIGN_IN)
@ -123,6 +129,7 @@ export const GlobalStateProvider = ({
state: authState, state: authState,
send: authSend, send: authSend,
commandBarConfig: authCommandBarConfig, commandBarConfig: authCommandBarConfig,
actor: authActor,
}) })
return ( return (

View File

@ -25,8 +25,7 @@ describe('processMemory', () => {
|> lineTo([-3.35, 0.17], %) |> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %) |> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %) // |> rx(90, %)`
show(theExtrude, theSketch)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, { const programMemory = await enginelessExecutor(ast, {
root: {}, root: {},

View File

@ -38,6 +38,10 @@ import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst' import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { isSingleCursorInPipe } from 'lang/queryAst' import { isSingleCursorInPipe } from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -54,7 +58,12 @@ export const ModelingMachineProvider = ({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { auth } = useGlobalStateContext() const {
auth,
settings: {
context: { baseUnit },
},
} = useGlobalStateContext()
const { code } = useKclContext() const { code } = useKclContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
@ -170,6 +179,56 @@ export const ModelingMachineProvider = ({
} }
return { selectionRangeTypeMap } return { selectionRangeTypeMap }
}), }),
'Engine export': (_, event) => {
if (event.type !== 'Export' || TEST) return
const format = {
...event.data,
} as Partial<Models['OutputFormat_type']>
// Set all the un-configurable defaults here.
if (format.type === 'gltf') {
format.presentation = 'pretty'
}
if (
format.type === 'obj' ||
format.type === 'ply' ||
format.type === 'step' ||
format.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
format.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
if (
format.type === 'obj' ||
format.type === 'stl' ||
format.type === 'ply'
) {
format.units = baseUnit
}
if (format.type === 'ply' || format.type === 'stl') {
format.selection = { type: 'default_scene' }
}
exportFromEngine({
source_unit: baseUnit,
format: format as Models['OutputFormat_type'],
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
},
}, },
guards: { guards: {
'has valid extrude selection': ({ selectionRanges }) => { 'has valid extrude selection': ({ selectionRanges }) => {
@ -192,6 +251,8 @@ export const ModelingMachineProvider = ({
selectionRanges selectionRanges
) )
}, },
'Has exportable geometry': () =>
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
}, },
services: { services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => { 'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {

View File

@ -5,7 +5,6 @@ import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { GlobalStateProvider } from './GlobalStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { vi } from 'vitest' import { vi } from 'vitest'
import { ExportButtonProps } from './ExportButton'
const now = new Date() const now = new Date()
const projectWellFormed = { const projectWellFormed = {
@ -38,15 +37,6 @@ const projectWellFormed = {
}, },
} satisfies ProjectWithEntryPointMetadata } satisfies ProjectWithEntryPointMetadata
const mockExportButton = vi.fn()
vi.mock('/src/components/ExportButton', () => ({
// engineCommandManager method call in ExportButton causes vitest to hang
ExportButton: (props: ExportButtonProps) => {
mockExportButton(props)
return <button>Fake export button</button>
},
}))
describe('ProjectSidebarMenu tests', () => { describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => { test('Renders the project name', () => {
render( render(

View File

@ -5,12 +5,12 @@ import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'
import { Fragment } from 'react' import { Fragment } from 'react'
import { FileTree } from './FileTree' import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -21,6 +21,8 @@ const ProjectSidebarMenu = ({
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) => { }) => {
const { commandBarSend } = useCommandsContext()
return renderAsLink ? ( return renderAsLink ? (
<Link <Link
to={paths.HOME} to={paths.HOME}
@ -112,13 +114,19 @@ const ProjectSidebarMenu = ({
<div className="flex-1 overflow-hidden" /> <div className="flex-1 overflow-hidden" />
)} )}
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90"> <div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
<ExportButton <ActionButton
className={{ Element="button"
button: 'border-transparent dark:border-transparent', icon={{ icon: 'exportFile', className: 'p-1' }}
}} className="border-transparent dark:border-transparent"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Export', ownerMachine: 'modeling' },
})
}
> >
Export Model Export Part
</ExportButton> </ActionButton>
{isTauri() && ( {isTauri() && (
<ActionButton <ActionButton
Element="link" Element="link"

View File

@ -1,15 +1,8 @@
import { import { MouseEventHandler, useEffect, useRef, useState } from 'react'
MouseEventHandler,
WheelEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { getNormalisedCoordinates, throttle } from '../lib/utils' import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
@ -36,7 +29,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
@ -68,19 +60,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
magnitude: e.deltaY * 0.4,
},
cmd_id: uuidv4(),
})
}, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
clientX, clientX,
clientY, clientY,
@ -159,14 +138,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
muted muted
autoPlay autoPlay
controls={false} controls={false}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)} onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`} className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
disablePictureInPicture disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/> />
<ClientSideScene cameraControls={settings.context.cameraControls} /> <ClientSideScene cameraControls={settings.context?.cameraControls} />
{!isNetworkOkay && !isLoading && ( {!isNetworkOkay && !isLoading && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>

View File

@ -108,8 +108,8 @@ export const TextEditor = ({
state, state,
} = useModelingContext() } = useModelingContext()
const { settings: { context: { textWrapping } = {} } = {}, auth } = const { settings, auth } = useGlobalStateContext()
useGlobalStateContext() const textWrapping = settings.context?.textWrapping ?? 'On'
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { const {
context: { project }, context: { project },

View File

@ -4,6 +4,7 @@ import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol' import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util' import { offsetToPos } from 'editor/plugins/lsp/util'
import { LanguageServerOptions } from 'editor/plugins/lsp' import { LanguageServerOptions } from 'editor/plugins/lsp'
import { syntaxTree } from '@codemirror/language'
import { import {
LanguageServerPlugin, LanguageServerPlugin,
documentUri, documentUri,
@ -40,6 +41,14 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
if (plugin == null) return null if (plugin == null) return null
const { state, pos, explicit } = context const { state, pos, explicit } = context
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
if (
nodeBefore.name === 'BlockComment' ||
nodeBefore.name === 'LineComment'
)
return null
const line = state.doc.lineAt(pos) const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined let trigChar: string | undefined
@ -60,6 +69,7 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
) { ) {
return null return null
} }
return await plugin.requestCompletion( return await plugin.requestCompletion(
context, context,
offsetToPos(state.doc, pos), offsetToPos(state.doc, pos),

View File

@ -7,6 +7,5 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS .VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
export const TEST = import.meta.env.TEST export const TEST = import.meta.env.TEST
export const DEV = import.meta.env.DEV export const DEV = import.meta.env.DEV

View File

@ -0,0 +1,31 @@
// Based on https://learnersbucket.com/examples/interview/usehasfocus-hook-in-react/
import { useState, useEffect } from 'react'
export const useDocumentHasFocus = () => {
// get the initial state
const [focus, setFocus] = useState(document.hasFocus())
useEffect(() => {
// helper functions to update the status
const onFocus = () => setFocus(true)
const onBlur = () => setFocus(false)
// assign the listener
// update the status on the event
if (globalThis.window && typeof globalThis.window !== 'undefined') {
globalThis.window.addEventListener('focus', onFocus)
globalThis.window.addEventListener('blur', onBlur)
}
// remove the listener
return () => {
if (globalThis.window && typeof globalThis.window !== 'undefined') {
globalThis.window.removeEventListener('focus', onFocus)
globalThis.window.removeEventListener('blur', onBlur)
}
}
}, [])
// return the status
return focus
}

View File

@ -28,7 +28,7 @@ interface UseStateMachineCommandsArgs<
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor?: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: CommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void

View File

@ -93,7 +93,7 @@ class KclManager {
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
this._params.id && this._params.id &&
writeTextFile(this._params.id, code).catch((err) => { writeTextFile(this._params.id, code).catch((err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) // TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')
}) })

View File

@ -11,59 +11,53 @@ const mySketch001 = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)`
show(mySketch001)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const shown = programMemory?.return?.map( const sketch001 = programMemory?.root?.mySketch001
// @ts-ignore expect(sketch001).toEqual({
(a) => programMemory?.root?.[a.name] type: 'SketchGroup',
) on: expect.any(Object),
expect(shown).toEqual([ start: {
{ to: [0, 0],
type: 'SketchGroup', from: [0, 0],
on: expect.any(Object), name: '',
start: { __geoMeta: {
to: [0, 0], id: expect.any(String),
from: [0, 0], sourceRange: [46, 71],
},
},
value: [
{
type: 'ToPoint',
name: '', name: '',
to: [-1.59, -1.54],
from: [0, 0],
__geoMeta: { __geoMeta: {
sourceRange: [77, 102],
id: expect.any(String), id: expect.any(String),
sourceRange: [46, 71],
}, },
}, },
value: [ {
{ type: 'ToPoint',
type: 'ToPoint', to: [0.46, -5.82],
name: '', from: [-1.59, -1.54],
to: [-1.59, -1.54], name: '',
from: [0, 0], __geoMeta: {
__geoMeta: { sourceRange: [108, 132],
sourceRange: [77, 102], id: expect.any(String),
id: expect.any(String),
},
}, },
{ },
type: 'ToPoint', ],
to: [0.46, -5.82], position: [0, 0, 0],
from: [-1.59, -1.54], rotation: [0, 0, 0, 1],
name: '', xAxis: { x: 1, y: 0, z: 0 },
__geoMeta: { yAxis: { x: 0, y: 1, z: 0 },
sourceRange: [108, 132], zAxis: { x: 0, y: 0, z: 1 },
id: expect.any(String), id: expect.any(String),
}, entityId: expect.any(String),
}, __meta: [{ sourceRange: [46, 71] }],
], })
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
id: expect.any(String),
entityId: expect.any(String),
__meta: [{ sourceRange: [46, 71] }],
},
])
}) })
test('extrude artifacts', async () => { test('extrude artifacts', async () => {
// Enable rotations #152 // Enable rotations #152
@ -73,30 +67,25 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %) |> extrude(2, %)`
show(mySketch001)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const shown = programMemory?.return?.map( const sketch001 = programMemory?.root?.mySketch001
// @ts-ignore expect(sketch001).toEqual({
(a) => programMemory?.root?.[a.name] type: 'ExtrudeGroup',
) id: expect.any(String),
expect(shown).toEqual([ value: [],
{ height: 2,
type: 'ExtrudeGroup', position: [0, 0, 0],
id: expect.any(String), rotation: [0, 0, 0, 1],
value: [], endCapId: null,
height: 2, startCapId: null,
position: [0, 0, 0], sketchGroupValues: expect.any(Array),
rotation: [0, 0, 0, 1], xAxis: { x: 1, y: 0, z: 0 },
endCapId: null, yAxis: { x: 0, y: 1, z: 0 },
startCapId: null, zAxis: { x: 0, y: 0, z: 1 },
xAxis: { x: 1, y: 0, z: 0 }, __meta: [{ sourceRange: [46, 71] }],
yAxis: { x: 0, y: 1, z: 0 }, })
zAxis: { x: 0, y: 0, z: 1 },
__meta: [{ sourceRange: [46, 71] }],
},
])
}) })
test('sketch extrude and sketch on one of the faces', async () => { test('sketch extrude and sketch on one of the faces', async () => {
// Enable rotations #152 // Enable rotations #152
@ -120,14 +109,10 @@ const sk2 = startSketchOn('XY')
// |> transform(theTransf, %) // |> transform(theTransf, %)
|> extrude(2, %) |> extrude(2, %)
`
show(theExtrude, sk2)`
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const geos = programMemory?.return?.map( const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
// @ts-ignore
({ name }) => programMemory?.root?.[name]
)
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'ExtrudeGroup', type: 'ExtrudeGroup',
@ -138,6 +123,7 @@ show(theExtrude, sk2)`
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null, endCapId: null,
startCapId: null, startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },
@ -153,6 +139,7 @@ show(theExtrude, sk2)`
endCapId: null, endCapId: null,
startCapId: null, startCapId: null,
sketchGroupValues: expect.any(Array),
xAxis: { x: 1, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 }, yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }, zAxis: { x: 0, y: 0, z: 1 },

View File

@ -47,9 +47,8 @@ const newVar = myVar + 1`
|> lineTo([2,3], %) |> lineTo([2,3], %)
|> lineTo({ to: [5,-1], tag: "rightPath" }, %) |> lineTo({ to: [5,-1], tag: "rightPath" }, %)
// |> close(%) // |> close(%)
show(mySketch)
` `
const { root, return: _return } = await exe(code) const { root } = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests // geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = root.mySketch.value const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
@ -84,15 +83,6 @@ show(mySketch)
name: 'rightPath', name: 'rightPath',
}, },
]) ])
// expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
expect(_return).toEqual([
{
type: 'Identifier',
start: 203,
end: 211,
name: 'mySketch',
},
])
}) })
it('pipe binary expression into call expression', async () => { it('pipe binary expression into call expression', async () => {
@ -357,7 +347,6 @@ describe('testing math operators', () => {
` -legLen(segLen('seg01', %), myVar)`, ` -legLen(segLen('seg01', %), myVar)`,
`], %)`, `], %)`,
``, ``,
`show(part001)`,
].join('\n') ].join('\n')
const { root } = await exe(code) const { root } = await exe(code)
const sketch = root.part001 const sketch = root.part001
@ -392,8 +381,7 @@ const theExtrude = startSketchOn('XY')
|> line([-0.76], myVarZ, %) |> line([-0.76], myVarZ, %)
|> line([5,5], %) |> line([5,5], %)
|> close(%) |> close(%)
|> extrude(4, %) |> extrude(4, %)`
show(theExtrude)`
await expect(exe(code)).rejects.toEqual( await expect(exe(code)).rejects.toEqual(
new KCLError( new KCLError(
'undefined_value', 'undefined_value',

View File

@ -122,7 +122,6 @@ describe('Testing addSketchTo', () => {
expect(str).toBe(`const part001 = startSketchOn('YZ') expect(str).toBe(`const part001 = startSketchOn('YZ')
|> startProfileAt('default', %) |> startProfileAt('default', %)
|> line('default', %) |> line('default', %)
show(part001)
`) `)
}) })
}) })
@ -147,8 +146,7 @@ describe('Testing giveSketchFnCallTag', () => {
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([-2.57, -0.13], %) |> line([-2.57, -0.13], %)
|> line([0, 0.83], %) |> line([0, 0.83], %)
|> line([0.82, 0.34], %) |> line([0.82, 0.34], %)`
show(part001)`
it('Should add tag to a sketch function call', () => { it('Should add tag to a sketch function call', () => {
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper( const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
code, code,
@ -204,8 +202,7 @@ const part001 = startSketchOn('XY')
|> angledLine([def(yo), 3.09], %) |> angledLine([def(yo), 3.09], %)
|> angledLine([ghi(%), 3.09], %) |> angledLine([ghi(%), 3.09], %)
|> angledLine([jkl(yo) + 2, 3.09], %) |> angledLine([jkl(yo) + 2, 3.09], %)
const yo2 = hmm([identifierGuy + 5]) const yo2 = hmm([identifierGuy + 5])`
show(part001)`
it('should move a binary expression into a new variable', async () => { it('should move a binary expression into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)

View File

@ -6,7 +6,6 @@ import {
PipeExpression, PipeExpression,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
ExpressionStatement,
Value, Value,
Literal, Literal,
PipeSubstitution, PipeSubstitution,
@ -128,16 +127,8 @@ export function addSketchTo(
createPipeExpression(pipeBody) createPipeExpression(pipeBody)
) )
const showCallIndex = getShowIndex(_node) _node.body = [...node.body, variableDeclaration]
let sketchIndex = showCallIndex let sketchIndex = _node.body.length - 1
if (showCallIndex === -1) {
_node.body = [...node.body, variableDeclaration]
sketchIndex = _node.body.length - 1
} else {
const newBody = [...node.body]
newBody.splice(showCallIndex, 0, variableDeclaration)
_node.body = newBody
}
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[sketchIndex, 'index'], [sketchIndex, 'index'],
@ -150,7 +141,7 @@ export function addSketchTo(
} }
return { return {
modifiedAst: addToShow(_node, _name), modifiedAst: _node,
id: _name, id: _name,
pathToNode, pathToNode,
} }
@ -191,44 +182,6 @@ export function findUniqueName(
return findUniqueName(searchStr, name, pad, index + 1) return findUniqueName(searchStr, name, pad, index + 1)
} }
function addToShow(node: Program, name: string): Program {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const showCallIndex = getShowIndex(_node)
if (showCallIndex === -1) {
const showCall = createCallExpressionStdLib('show', [
createIdentifier(name),
])
const showExpressionStatement: ExpressionStatement = {
type: 'ExpressionStatement',
...dumbyStartend,
expression: showCall,
}
_node.body = [..._node.body, showExpressionStatement]
return _node
}
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
const showCallArgs = (showCall.expression as CallExpression).arguments
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
_node.body[showCallIndex] = {
...showCall,
expression: newShowExpression,
}
return _node
}
function getShowIndex(node: Program): number {
return node.body.findIndex(
(statement) =>
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'CallExpression' &&
statement.expression.callee.type === 'Identifier' &&
statement.expression.callee.name === 'show'
)
}
export function mutateArrExp( export function mutateArrExp(
node: Value, node: Value,
updateWith: ArrayExpression updateWith: ArrayExpression
@ -348,15 +301,10 @@ export function extrudeSketch(
} }
const name = findUniqueName(node, 'part') const name = findUniqueName(node, 'part')
const VariableDeclaration = createVariableDeclaration(name, extrudeCall) const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
let showCallIndex = getShowIndex(_node) _node.body.splice(_node.body.length, 0, VariableDeclaration)
if (showCallIndex === -1) {
// We didn't find a show, so let's just append everything
showCallIndex = _node.body.length
}
_node.body.splice(showCallIndex, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
[showCallIndex, 'index'], [_node.body.length, 'index'],
['declarations', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
[0, 'index'], [0, 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
@ -365,7 +313,7 @@ export function extrudeSketch(
] ]
return { return {
modifiedAst: node, modifiedAst: node,
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']], pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
pathToExtrudeArg, pathToExtrudeArg,
} }
} }
@ -425,7 +373,7 @@ export function sketchOnExtrudedFace(
_node.body.splice(expressionIndex + 1, 0, newSketch) _node.body.splice(expressionIndex + 1, 0, newSketch)
return { return {
modifiedAst: addToShow(_node, newSketchName), modifiedAst: _node,
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']], pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
} }
} }

View File

@ -34,8 +34,7 @@ const part001 = startSketchOn('XY')
|> xLine(3.84, %) // selection-range-7ish-before-this |> xLine(3.84, %) // selection-range-7ish-before-this
const variableBelowShouldNotBeIncluded = 3 const variableBelowShouldNotBeIncluded = 3
`
show(part001)`
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7 const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
@ -69,8 +68,7 @@ describe('testing argIsNotIdentifier', () => {
|> angledLine([ghi(%), 3.09], %) |> angledLine([ghi(%), 3.09], %)
|> angledLine([jkl('yo') + 2, 3.09], %) |> angledLine([jkl('yo') + 2, 3.09], %)
const yo = 5 + 6 const yo = 5 + 6
const yo2 = hmm([identifierGuy + 5]) const yo2 = hmm([identifierGuy + 5])`
show(part001)`
it('find a safe binaryExpression', () => { it('find a safe binaryExpression', () => {
const ast = parse(code) const ast = parse(code)
const rangeStart = code.indexOf('100 + 100') + 2 const rangeStart = code.indexOf('100 + 100') + 2
@ -201,8 +199,7 @@ describe('testing getNodePathFromSourceRange', () => {
const code = `const part001 = startSketchOn('XY') const code = `const part001 = startSketchOn('XY')
|> startProfileAt([0.39, -0.05], %) |> startProfileAt([0.39, -0.05], %)
|> line([0.94, 2.61], %) |> line([0.94, 2.61], %)
|> line([-0.21, -1.4], %) |> line([-0.21, -1.4], %)`
show(part001)`
it('finds the second line when cursor is put at the end', () => { it('finds the second line when cursor is put at the end', () => {
const searchLn = `line([0.94, 2.61], %)` const searchLn = `line([0.94, 2.61], %)`
const sourceIndex = code.indexOf(searchLn) + searchLn.length const sourceIndex = code.indexOf(searchLn) + searchLn.length

View File

@ -68,8 +68,6 @@ log(5, myVar)
|> lineTo([1, 1], %) |> lineTo([1, 1], %)
|> lineTo({ to: [1, 0], tag: "rightPath" }, %) |> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|> close(%) |> close(%)
show(mySketch)
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)
@ -331,7 +329,6 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
intersectTag: 'seg01' intersectTag: 'seg01'
}, %) }, %)
|> line([-0.42, -1.72], %) |> line([-0.42, -1.72], %)
show(part001)
` `
const { ast } = code2ast(code) const { ast } = code2ast(code)
const recasted = recast(ast) const recasted = recast(ast)

View File

@ -3,7 +3,6 @@ import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
@ -290,12 +289,6 @@ class EngineConnection {
} }
} }
// shouldTrace will return true when Sentry should be used to instrument
// the Engine.
shouldTrace() {
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
}
// connect will attempt to connect to the Engine over a WebSocket, and // connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections. // establish the WebRTC connections.
// //
@ -308,41 +301,6 @@ class EngineConnection {
// Information on the connect transaction // Information on the connect transaction
class SpanPromise {
span: Sentry.Span
promise: Promise<void>
resolve?: (v: void) => void
constructor(span: Sentry.Span) {
this.span = span
this.promise = new Promise((resolve) => {
this.resolve = (v: void) => {
// here we're going to invoke finish before resolving the
// promise so that a `.then()` will order strictly after
// all spans have -- for sure -- been resolved, rather than
// doing a `then` on this promise.
this.span.finish()
resolve(v)
}
})
}
}
let webrtcMediaTransaction: Sentry.Transaction
let websocketSpan: SpanPromise
let mediaTrackSpan: SpanPromise
let dataChannelSpan: SpanPromise
let handshakeSpan: SpanPromise
let iceSpan: SpanPromise
const spanStart = (op: string) =>
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
if (this.shouldTrace()) {
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
websocketSpan = spanStart('websocket')
}
const createPeerConnection = () => { const createPeerConnection = () => {
this.pc = new RTCPeerConnection() this.pc = new RTCPeerConnection()
@ -393,10 +351,6 @@ class EngineConnection {
// From what I understand, only after have we done the ICE song and // From what I understand, only after have we done the ICE song and
// dance is it safest to connect the video tracks / stream // dance is it safest to connect the video tracks / stream
case 'connected': case 'connected':
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
// Let the browser attach to the video stream now // Let the browser attach to the video stream now
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! }) this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
break break
@ -429,17 +383,6 @@ class EngineConnection {
}, },
} }
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
this.webrtcStatsCollector = (): Promise<ClientMetrics> => { this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) { if (mediaStream.getVideoTracks().length !== 1) {
@ -522,10 +465,6 @@ class EngineConnection {
}, },
} }
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = { type: EngineConnectionStateType.ConnectionEstablished }
@ -577,27 +516,6 @@ class EngineConnection {
if (this.token) { if (this.token) {
this.send({ headers: { Authorization: `Bearer ${this.token}` } }) this.send({ headers: { Authorization: `Bearer ${this.token}` } })
} }
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = spanStart('handshake')
iceSpan = spanStart('ice')
dataChannelSpan = spanStart('data-channel')
mediaTrackSpan = spanStart('media-track')
}
if (this.shouldTrace()) {
void Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
}) })
this.websocket.addEventListener('close', (event) => { this.websocket.addEventListener('close', (event) => {
@ -786,13 +704,6 @@ failed cmd type was ${artifactThatFailed?.commandType}`
type: ConnectingType.WebRTCConnecting, type: ConnectingType.WebRTCConnecting,
}, },
} }
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
break break
case 'trickle_ice': case 'trickle_ice':
@ -885,7 +796,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
callback: (data: Extract<UnreliableResponses, { type: T }>) => void callback: (data: Extract<UnreliableResponses, { type: T }>) => void
} }
interface Subscription<T extends ModelTypes> { export interface Subscription<T extends ModelTypes> {
event: T event: T
callback: ( callback: (
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }> data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
@ -1015,6 +926,15 @@ export class EngineCommandManager {
}, },
}) })
sceneInfra.camControls.onCameraChange() sceneInfra.camControls.onCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
this.initPlanes().then(() => { this.initPlanes().then(() => {
this.resolveReady() this.resolveReady()

View File

@ -101,7 +101,6 @@ describe('testing changeSketchArguments', () => {
|> ${line} |> ${line}
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
show(mySketch001)
` `
const code = genCode(lineToChange) const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange) const expectedCode = genCode(lineAfterChange)
@ -128,8 +127,7 @@ const mySketch001 = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
// |> rx(45, %) // |> rx(45, %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)`
show(mySketch001)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
@ -155,7 +153,6 @@ show(mySketch001)`
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %) |> lineTo([2, 3], %)
show(mySketch001)
` `
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
@ -177,7 +174,6 @@ show(mySketch001)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> close(%) |> close(%)
show(mySketch001)
` `
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
}) })
@ -192,7 +188,6 @@ describe('testing addTagForSketchOnFace', () => {
// |> rx(45, %) // |> rx(45, %)
|> ${line} |> ${line}
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
show(mySketch001)
` `
const code = genCode(originalLine) const code = genCode(originalLine)
const ast = parse(code) const ast = parse(code)

View File

@ -91,12 +91,6 @@ export function createFirstArg(
throw new Error('all sketch line types should have been covered') throw new Error('all sketch line types should have been covered')
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type LineData = {
from: [number, number, number]
to: [number, number, number]
}
export const lineTo: SketchLineHelper = { export const lineTo: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -966,6 +960,30 @@ export const angledLineThatIntersects: SketchLineHelper = {
addTag: addTagWithTo('angleTo'), // TODO might be wrong addTag: addTagWithTo('angleTo'), // TODO might be wrong
} }
export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
node,
pathToNode,
to,
}) => {
const _node = { ...node }
const { node: callExpression } = getNodeFromPath<CallExpression>(
_node,
pathToNode
)
const toArrExp = createArrayExpression([
createLiteral(roundOff(to[0])),
createLiteral(roundOff(to[1])),
])
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
return {
modifiedAst: _node,
pathToNode,
}
}
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
line, line,
lineTo, lineTo,

View File

@ -88,7 +88,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo', () => {
` |> yLine(-1.07, %)`, ` |> yLine(-1.07, %)`,
` |> xLineTo(3.27, %)`, ` |> xLineTo(3.27, %)`,
` |> yLineTo(2.14, %)`, ` |> yLineTo(2.14, %)`,
`show(part001)`,
] ]
const bigExample = bigExampleArr.join('\n') const bigExample = bigExampleArr.join('\n')
it('line with tag converts to xLine', async () => { it('line with tag converts to xLine', async () => {
@ -290,7 +289,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo while keeping var
` |> angledLineToX([330, angledLineToXx], %)`, ` |> angledLineToX([330, angledLineToXx], %)`,
` |> angledLineToY([217, angledLineToYy], %)`, ` |> angledLineToY([217, angledLineToYy], %)`,
` |> line([0.89, -0.1], %)`, ` |> line([0.89, -0.1], %)`,
`show(part001)`,
] ]
const varExample = variablesExampleArr.join('\n') const varExample = variablesExampleArr.join('\n')
it('line keeps variable when converted to xLine', async () => { it('line keeps variable when converted to xLine', async () => {
@ -378,8 +376,7 @@ const part001 = startSketchOn('XY')
|> line([0, 0.4], %) |> line([0, 0.4], %)
|> xLine(3.48, %) |> xLine(3.48, %)
|> line([2.14, 1.35], %) // normal-segment |> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %) |> xLine(3.54, %)`
show(part001)`
it('normal case works', async () => { it('normal case works', async () => {
const programMemory = await enginelessExecutor(parse(code)) const programMemory = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7

View File

@ -123,7 +123,6 @@ const part001 = startSketchOn('XY')
|> yLine(1.04, %) // ln-yLine-free should sub in segLen |> yLine(1.04, %) // ln-yLine-free should sub in segLen
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine |> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine |> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
show(part001)
` `
const expectModifiedScript = `const myVar = 3 const expectModifiedScript = `const myVar = 3
const myVar2 = 5 const myVar2 = 5
@ -196,7 +195,6 @@ const part001 = startSketchOn('XY')
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen |> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine |> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine |> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
show(part001)
` `
it('should transform the ast', async () => { it('should transform the ast', async () => {
const ast = parse(inputScript) const ast = parse(inputScript)
@ -257,7 +255,6 @@ const part001 = startSketchOn('XY')
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9 |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10 |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10 |> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)
` `
it('should transform horizontal lines the ast', async () => { it('should transform horizontal lines the ast', async () => {
const expectModifiedScript = `const myVar = 2 const expectModifiedScript = `const myVar = 2
@ -286,7 +283,6 @@ const part001 = startSketchOn('XY')
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9 |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> xLineTo(myVar3, %) // select for horizontal constraint 10 |> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10 |> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)
` `
const ast = parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -345,7 +341,6 @@ const part001 = startSketchOn('XY')
|> yLineTo(7.68, %) // select for vertical constraint 9 |> yLineTo(7.68, %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10 |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10 |> yLineTo(myVar, %) // select for vertical constraint 10
show(part001)
` `
const ast = parse(inputScript) const ast = parse(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -389,7 +384,6 @@ const part001 = startSketchOn('XY')
|> line([0.45, 1.46], %) // free |> line([0.45, 1.46], %) // free
|> line([myVar, 0.01], %) // xRelative |> line([myVar, 0.01], %) // xRelative
|> line([0.7, myVar], %) // yRelative |> line([0.7, myVar], %) // yRelative
show(part001)
` `
it('testing for free to horizontal and vertical distance', async () => { it('testing for free to horizontal and vertical distance', async () => {
const expectedHorizontalCode = await helperThing( const expectedHorizontalCode = await helperThing(
@ -501,8 +495,7 @@ const part001 = startSketchOn('XY')
|> xLine(3.36, %) // partial |> xLine(3.36, %) // partial
|> line([-1.49, 1.06], %) // free |> line([-1.49, 1.06], %) // free
|> xLine(-3.43 + 0, %) // full |> xLine(-3.43 + 0, %) // full
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full |> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full`
show(part001)`
const ast = parse(code) const ast = parse(code)
const constraintLevels: ReturnType< const constraintLevels: ReturnType<
typeof getConstraintLevelFromSourceRange typeof getConstraintLevelFromSourceRange

View File

@ -15,8 +15,7 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
tag: "yo2" tag: "yo2"
}, %) }, %)
const intersect = segEndX('yo2', part001) const intersect = segEndX('yo2', part001)`
show(part001)`
const { root } = await enginelessExecutor(parse(code('-1'))) const { root } = await enginelessExecutor(parse(code('-1')))
expect(root.intersect.value).toBe(1 + Math.sqrt(2)) expect(root.intersect.value).toBe(1 + Math.sqrt(2))
const { root: noOffset } = await enginelessExecutor(parse(code('0'))) const { root: noOffset } = await enginelessExecutor(parse(code('0')))

View File

@ -40,9 +40,9 @@ export interface MouseGuard {
} }
const butName = (e: React.MouseEvent) => ({ const butName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4), middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2), right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1), left: !!(e.buttons & 1) || e.button === 0,
}) })
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = { export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {

View File

@ -28,7 +28,8 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: (context) => options: [],
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -43,7 +44,7 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValue: (context) => context.defaultProjectName, defaultValueFromContext: (context) => context.defaultProjectName,
}, },
}, },
}, },
@ -55,7 +56,8 @@ export const homeCommandBarConfig: CommandSetConfig<
name: { name: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: (context) => options: [],
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -71,7 +73,8 @@ export const homeCommandBarConfig: CommandSetConfig<
oldName: { oldName: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: (context) => options: [],
optionsFromContext: (context) =>
context.projects.map((p) => ({ context.projects.map((p) => ({
name: p.name!, name: p.name!,
value: p.name!, value: p.name!,
@ -80,7 +83,7 @@ export const homeCommandBarConfig: CommandSetConfig<
newName: { newName: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValue: (context) => context.defaultProjectName, defaultValueFromContext: (context) => context.defaultProjectName,
}, },
}, },
}, },

View File

@ -1,7 +1,13 @@
import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>
export const EXTRUSION_RESULTS = [ export const EXTRUSION_RESULTS = [
'new', 'new',
'add', 'add',
@ -11,6 +17,10 @@ export const EXTRUSION_RESULTS = [
export type ModelingCommandSchema = { export type ModelingCommandSchema = {
'Enter sketch': {} 'Enter sketch': {}
Export: {
type: OutputTypeKey
storage?: StorageUnion
}
Extrude: { Extrude: {
selection: Selections // & { type: 'face' } would be cool to lock that down selection: Selections // & { type: 'face' } would be cool to lock that down
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
@ -26,6 +36,80 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.', description: 'Enter sketch mode.',
icon: 'sketch', icon: 'sketch',
}, },
Export: {
description: 'Export the current model.',
icon: 'exportFile',
needsReview: true,
args: {
type: {
inputType: 'options',
defaultValue: 'gltf',
required: true,
options: [
{ name: 'gLTF', isCurrent: true, value: 'gltf' },
{ name: 'OBJ', isCurrent: false, value: 'obj' },
{ name: 'STL', isCurrent: false, value: 'stl' },
{ name: 'STEP', isCurrent: false, value: 'step' },
{ name: 'PLY', isCurrent: false, value: 'ply' },
],
},
storage: {
inputType: 'options',
defaultValue: (c) => {
switch (c.argumentsToSubmit.type) {
case 'gltf':
return 'embedded'
case 'stl':
return 'ascii'
case 'ply':
return 'ascii'
default:
return undefined
}
},
skip: true,
required: (commandContext) =>
['gltf', 'stl', 'ply'].includes(
commandContext.argumentsToSubmit.type as string
),
options: (commandContext) => {
const type = commandContext.argumentsToSubmit.type as
| OutputTypeKey
| undefined
switch (type) {
case 'gltf':
return [
{ name: 'embedded', isCurrent: true, value: 'embedded' },
{ name: 'binary', isCurrent: false, value: 'binary' },
{ name: 'standard', isCurrent: false, value: 'standard' },
]
case 'stl':
return [
{ name: 'binary', isCurrent: false, value: 'binary' },
{ name: 'ascii', isCurrent: true, value: 'ascii' },
]
case 'ply':
return [
{ name: 'ascii', isCurrent: true, value: 'ascii' },
{
name: 'binary_big_endian',
isCurrent: false,
value: 'binary_big_endian',
},
{
name: 'binary_little_endian',
isCurrent: false,
value: 'binary_little_endian',
},
]
default:
return []
}
},
},
},
},
Extrude: { Extrude: {
description: 'Pull a sketch into 3D along its normal or perpendicular.', description: 'Pull a sketch into 3D along its normal or perpendicular.',
icon: 'extrude', icon: 'extrude',

View File

@ -41,8 +41,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
baseUnit: { baseUnit: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: (context) => context.baseUnit, defaultValueFromContext: (context) => context.baseUnit,
options: (context) => options: [],
optionsFromContext: (context) =>
Object.values(baseUnitsUnion).map((v) => ({ Object.values(baseUnitsUnion).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -57,8 +58,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
cameraControls: { cameraControls: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: (context) => context.cameraControls, defaultValueFromContext: (context) => context.cameraControls,
options: (context) => options: [],
optionsFromContext: (context) =>
Object.values(cameraSystems).map((v) => ({ Object.values(cameraSystems).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -74,7 +76,7 @@ export const settingsCommandBarConfig: CommandSetConfig<
defaultProjectName: { defaultProjectName: {
inputType: 'string', inputType: 'string',
required: true, required: true,
defaultValue: (context) => context.defaultProjectName, defaultValueFromContext: (context) => context.defaultProjectName,
}, },
}, },
}, },
@ -84,8 +86,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
textWrapping: { textWrapping: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: (context) => context.textWrapping, defaultValueFromContext: (context) => context.textWrapping,
options: (context) => [ options: [],
optionsFromContext: (context) => [
{ {
name: 'On', name: 'On',
value: 'On' as Toggle, value: 'On' as Toggle,
@ -106,8 +109,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
theme: { theme: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: (context) => context.theme, defaultValueFromContext: (context) => context.theme,
options: (context) => options: [],
optionsFromContext: (context) =>
Object.values(Themes).map((v) => ({ Object.values(Themes).map((v) => ({
name: v, name: v,
value: v, value: v,
@ -122,8 +126,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
unitSystem: { unitSystem: {
inputType: 'options', inputType: 'options',
required: true, required: true,
defaultValue: (context) => context.unitSystem, defaultValueFromContext: (context) => context.unitSystem,
options: (context) => [ options: [],
optionsFromContext: (context) => [
{ {
name: 'Imperial', name: 'Imperial',
value: 'imperial' as UnitSystem, value: 'imperial' as UnitSystem,

View File

@ -8,6 +8,7 @@ import {
} from 'xstate' } from 'xstate'
import { Selection } from './selections' import { Selection } from './selections'
import { Identifier, Value, VariableDeclaration } from 'lang/wasm' import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
@ -93,15 +94,31 @@ export type CommandArgumentConfig<
> = > =
| { | {
description?: string description?: string
required: boolean required:
skip?: true | boolean
| ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
) => boolean)
skip?: boolean
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>
options: options:
| CommandArgumentOption<OutputType>[] | CommandArgumentOption<OutputType>[]
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[]) | ((
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType) commandBarContext: {
argumentsToSubmit: Record<string, unknown>
} // Should be the commandbarMachine's context, but it creates a circular dependency
) => CommandArgumentOption<OutputType>[])
optionsFromContext?: (
context: ContextFrom<T>
) => CommandArgumentOption<OutputType>[]
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
} }
| { | {
inputType: Extract<CommandInputType, 'selection'> inputType: Extract<CommandInputType, 'selection'>
@ -111,7 +128,12 @@ export type CommandArgumentConfig<
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values | { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
| { | {
inputType: Extract<CommandInputType, 'string'> inputType: Extract<CommandInputType, 'string'>
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType) defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
} }
) )
@ -121,24 +143,42 @@ export type CommandArgument<
> = > =
| { | {
description?: string description?: string
required: boolean required:
skip?: true | boolean
| ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
) => boolean)
skip?: boolean
machineActor: InterpreterFrom<T>
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>
options: CommandArgumentOption<OutputType>[] options:
defaultValue?: OutputType | CommandArgumentOption<OutputType>[]
| ((
commandBarContext: {
argumentsToSubmit: Record<string, unknown>
} // Should be the commandbarMachine's context, but it creates a circular dependency
) => CommandArgumentOption<OutputType>[])
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
} }
| { | {
inputType: Extract<CommandInputType, 'selection'> inputType: Extract<CommandInputType, 'selection'>
selectionTypes: Selection['type'][] selectionTypes: Selection['type'][]
actor: InterpreterFrom<T>
multiple: boolean multiple: boolean
} }
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values | { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
| { | {
inputType: Extract<CommandInputType, 'string'> inputType: Extract<CommandInputType, 'string'>
defaultValue?: OutputType defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>
) => OutputType)
} }
) )

View File

@ -17,7 +17,7 @@ interface CreateMachineCommandProps<
ownerMachine: T['id'] ownerMachine: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor?: InterpreterFrom<T> actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S> commandBarConfig?: CommandSetConfig<T, S>
onCancel?: () => void onCancel?: () => void
} }
@ -91,13 +91,13 @@ function buildCommandArguments<
>( >(
state: StateFrom<T>, state: StateFrom<T>,
args: CommandConfig<T, CommandName, S>['args'], args: CommandConfig<T, CommandName, S>['args'],
actor?: InterpreterFrom<T> machineActor: InterpreterFrom<T>
): NonNullable<Command<T, CommandName, S>['args']> { ): NonNullable<Command<T, CommandName, S>['args']> {
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']> const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
for (const arg in args) { for (const arg in args) {
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T> const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
const newArg = buildCommandArgument(argConfig, arg, state, actor) const newArg = buildCommandArgument(argConfig, arg, state, machineActor)
newArgs[arg] = newArg newArgs[arg] = newArg
} }
@ -111,44 +111,36 @@ function buildCommandArgument<
arg: CommandArgumentConfig<O, T>, arg: CommandArgumentConfig<O, T>,
argName: string, argName: string,
state: StateFrom<T>, state: StateFrom<T>,
actor?: InterpreterFrom<T> machineActor: InterpreterFrom<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
skip: arg.skip, skip: arg.skip,
machineActor,
} satisfies Omit<CommandArgument<O, T>, 'inputType'> } satisfies Omit<CommandArgument<O, T>, 'inputType'>
if (arg.inputType === 'options') { if (arg.inputType === 'options') {
const options = arg.options if (!arg.options) {
? arg.options instanceof Function
? arg.options(state.context)
: arg.options
: undefined
if (!options) {
throw new Error('Options must be provided for options input type') throw new Error('Options must be provided for options input type')
} }
return { return {
inputType: arg.inputType, inputType: arg.inputType,
...baseCommandArgument, ...baseCommandArgument,
defaultValue: defaultValue: arg.defaultValueFromContext
arg.defaultValue instanceof Function ? arg.defaultValueFromContext(state.context)
? arg.defaultValue(state.context) : arg.defaultValue,
: arg.defaultValue, options: arg.optionsFromContext
options, ? arg.optionsFromContext(state.context)
: arg.options,
} satisfies CommandArgument<O, T> & { inputType: 'options' } } satisfies CommandArgument<O, T> & { inputType: 'options' }
} else if (arg.inputType === 'selection') { } else if (arg.inputType === 'selection') {
if (!actor)
throw new Error('Actor must be provided for selection input type')
return { return {
inputType: arg.inputType, inputType: arg.inputType,
...baseCommandArgument, ...baseCommandArgument,
multiple: arg.multiple, multiple: arg.multiple,
selectionTypes: arg.selectionTypes, selectionTypes: arg.selectionTypes,
actor,
} satisfies CommandArgument<O, T> & { inputType: 'selection' } } satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else if (arg.inputType === 'kcl') { } else if (arg.inputType === 'kcl') {
return { return {
@ -159,10 +151,7 @@ function buildCommandArgument<
} else { } else {
return { return {
inputType: arg.inputType, inputType: arg.inputType,
defaultValue: defaultValue: arg.defaultValue,
arg.defaultValue instanceof Function
? arg.defaultValue(state.context)
: arg.defaultValue,
...baseCommandArgument, ...baseCommandArgument,
} }
} }

View File

@ -0,0 +1,27 @@
import { engineCommandManager } from 'lang/std/engineConnection'
import { type Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid'
// Isolating a function to call the engine to export the current scene.
// Because it has given us trouble in automated testing environments.
export function exportFromEngine({
source_unit,
format,
}: {
source_unit: Models['UnitLength_type']
format: Models['OutputFormat_type']
}) {
return engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
// By default let's leave this blank to export the whole scene.
// In the future we might want to let the user choose which entities
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format,
source_unit,
},
cmd_id: uuidv4(),
})
}

View File

@ -20,6 +20,7 @@ import {
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
sceneEntitiesManager, sceneEntitiesManager,
getParentGroup, getParentGroup,
PROFILE_START,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { Mesh } from 'three' import { Mesh } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra' import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
@ -188,7 +189,11 @@ export async function getEventForSelectWithPoint(
export function getEventForSegmentSelection( export function getEventForSegmentSelection(
obj: any obj: any
): ModelingMachineEvent | null { ): ModelingMachineEvent | null {
const group = getParentGroup(obj) const group = getParentGroup(obj, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const axisGroup = getParentGroup(obj, [AXIS_GROUP]) const axisGroup = getParentGroup(obj, [AXIS_GROUP])
if (!group && !axisGroup) return null if (!group && !axisGroup) return null
if (axisGroup?.userData.type === AXIS_GROUP) { if (axisGroup?.userData.type === AXIS_GROUP) {
@ -407,8 +412,8 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
} }
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if ( if (
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes( ![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, PROFILE_START].includes(
segmentGroup?.userData?.type segmentGroup?.name
) )
) )
return return
@ -420,7 +425,9 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
const groupHasCursor = codeBasedSelections.some((selection) => { const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection.range, [node.start, node.end]) return isOverlap(selection.range, [node.start, node.end])
}) })
const color = groupHasCursor ? 0x0000ff : 0xffffff const color = groupHasCursor
? 0x0000ff
: segmentGroup?.userData?.baseColor || 0xffffff
segmentGroup.traverse( segmentGroup.traverse(
(child) => child instanceof Mesh && child.material.color.set(color) (child) => child instanceof Mesh && child.material.color.set(color)
) )

View File

@ -15,7 +15,16 @@ export const FILE_EXT = '.kcl'
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
export const MAX_PADDING = 7 export const MAX_PADDING = 7
const RELEVANT_FILE_TYPES = ['kcl'] const RELEVANT_FILE_TYPES = [
'kcl',
'fbx',
'gltf',
'glb',
'obj',
'ply',
'step',
'stl',
]
// Initializes the project directory and returns the path // Initializes the project directory and returns the path
export async function initializeProjectDirectory(directory: string) { export async function initializeProjectDirectory(directory: string) {

View File

@ -8,21 +8,29 @@ import {
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils' import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
export type CommandBarContext = {
commands: Command[]
selectedCommand?: Command
currentArgument?: CommandArgument<unknown> & { name: string }
selectionRanges: Selections
argumentsToSubmit: { [x: string]: unknown }
}
export const commandBarMachine = createMachine( export const commandBarMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
predictableActionArguments: true,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: { context: {
commands: [] as Command[], commands: [],
selectedCommand: undefined as Command | undefined, selectedCommand: undefined,
currentArgument: undefined as currentArgument: undefined,
| (CommandArgument<unknown> & { name: string })
| undefined,
selectionRanges: { selectionRanges: {
otherSelections: [], otherSelections: [],
codeBasedSelections: [], codeBasedSelections: [],
} as Selections, },
argumentsToSubmit: {} as { [x: string]: unknown }, argumentsToSubmit: {},
}, } as CommandBarContext,
id: 'Command Bar', id: 'Command Bar',
initial: 'Closed', initial: 'Closed',
states: { states: {
@ -267,7 +275,6 @@ export const commandBarMachine = createMachine(
data: { [x: string]: CommandArgumentWithName<unknown> } data: { [x: string]: CommandArgumentWithName<unknown> }
}, },
}, },
predictableActionArguments: true,
preserveActionOrder: true, preserveActionOrder: true,
}, },
{ {
@ -279,28 +286,45 @@ export const commandBarMachine = createMachine(
(selectedCommand?.args && event.type === 'Submit command') || (selectedCommand?.args && event.type === 'Submit command') ||
event.type === 'done.invoke.validateArguments' event.type === 'done.invoke.validateArguments'
) { ) {
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data)) const resolvedArgs = {} as { [x: string]: unknown }
for (const [argName, argValue] of Object.entries(
getCommandArgumentKclValuesOnly(event.data)
)) {
resolvedArgs[argName] =
typeof argValue === 'function' ? argValue(context) : argValue
}
selectedCommand?.onSubmit(resolvedArgs)
} else { } else {
selectedCommand?.onSubmit() selectedCommand?.onSubmit()
} }
}, },
'Set current argument to first non-skippable': assign({ 'Set current argument to first non-skippable': assign({
currentArgument: (context) => { currentArgument: (context, event) => {
const { selectedCommand } = context const { selectedCommand } = context
if (!(selectedCommand && selectedCommand.args)) return undefined if (!(selectedCommand && selectedCommand.args)) return undefined
const rejectedArg = 'data' in event && event.data.arg
// Find the first argument that is not to be skipped: // Find the first argument that is not to be skipped:
// that is, the first argument that is not already in the argumentsToSubmit // that is, the first argument that is not already in the argumentsToSubmit
// or that is not undefined, or that is not marked as "skippable". // or that is not undefined, or that is not marked as "skippable".
// TODO validate the type of the existing arguments // TODO validate the type of the existing arguments
let argIndex = 0 let argIndex = 0
while (argIndex < Object.keys(selectedCommand.args).length) { while (argIndex < Object.keys(selectedCommand.args).length) {
const argName = Object.keys(selectedCommand.args)[argIndex] const [argName, argConfig] = Object.entries(selectedCommand.args)[
argIndex
]
const argIsRequired =
typeof argConfig.required === 'function'
? argConfig.required(context)
: argConfig.required
const mustNotSkipArg = const mustNotSkipArg =
!context.argumentsToSubmit.hasOwnProperty(argName) || argIsRequired &&
context.argumentsToSubmit[argName] === undefined || (!context.argumentsToSubmit.hasOwnProperty(argName) ||
!selectedCommand.args[argName].skip context.argumentsToSubmit[argName] === undefined ||
if (mustNotSkipArg) { (rejectedArg && rejectedArg.name === argName))
if (mustNotSkipArg === true) {
return { return {
...selectedCommand.args[argName], ...selectedCommand.args[argName],
name: argName, name: argName,
@ -308,14 +332,10 @@ export const commandBarMachine = createMachine(
} }
argIndex++ argIndex++
} }
// Just show the last argument if all are skippable
// TODO: use an XState service to continue onto review step // TODO: use an XState service to continue onto review step
// if all arguments are skippable and contain values. // if all arguments are skippable and contain values.
const argName = Object.keys(selectedCommand.args)[argIndex - 1] return undefined
return {
...selectedCommand.args[argName],
name: argName,
}
}, },
}), }),
'Clear current argument': assign({ 'Clear current argument': assign({
@ -333,8 +353,6 @@ export const commandBarMachine = createMachine(
'Set current argument': assign({ 'Set current argument': assign({
currentArgument: (context, event) => { currentArgument: (context, event) => {
switch (event.type) { switch (event.type) {
case 'error.platform.validateArguments':
return event.data.arg
case 'Edit argument': case 'Edit argument':
return event.data.arg return event.data.arg
default: default:
@ -343,27 +361,22 @@ export const commandBarMachine = createMachine(
}, },
}), }),
'Remove current argument and set a new one': assign({ 'Remove current argument and set a new one': assign({
currentArgument: (context, event) => {
if (event.type !== 'Change current argument')
return context.currentArgument
return Object.values(event.data)[0]
},
argumentsToSubmit: (context, event) => { argumentsToSubmit: (context, event) => {
if ( if (
event.type !== 'Change current argument' || event.type !== 'Change current argument' ||
!context.currentArgument !context.currentArgument
) )
return context.argumentsToSubmit return context.argumentsToSubmit
const { name, required } = context.currentArgument const { name } = context.currentArgument
if (required)
return {
[name]: undefined,
...context.argumentsToSubmit,
}
const { [name]: _, ...rest } = context.argumentsToSubmit const { [name]: _, ...rest } = context.argumentsToSubmit
return rest return rest
}, },
currentArgument: (context, event) => {
if (event.type !== 'Change current argument')
return context.currentArgument
return Object.values(event.data)[0]
},
}), }),
'Clear argument data': assign({ 'Clear argument data': assign({
selectedCommand: undefined, selectedCommand: undefined,
@ -388,11 +401,6 @@ export const commandBarMachine = createMachine(
}), }),
'Initialize arguments to submit': assign({ 'Initialize arguments to submit': assign({
argumentsToSubmit: (c, e) => { argumentsToSubmit: (c, e) => {
if (
e.type !== 'Select command' &&
e.type !== 'Find and select command'
)
return c.argumentsToSubmit
const command = const command =
'command' in e.data ? e.data.command : c.selectedCommand! 'command' in e.data ? e.data.command : c.selectedCommand!
if (!command.args) return {} if (!command.args) return {}
@ -421,38 +429,67 @@ export const commandBarMachine = createMachine(
}, },
'Validate all arguments': (context, _) => { 'Validate all arguments': (context, _) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
for (const [argName, arg] of Object.entries( for (const [argName, argConfig] of Object.entries(
context.argumentsToSubmit context.selectedCommand!.args!
)) { )) {
let argConfig = context.selectedCommand!.args![argName] let arg = context.argumentsToSubmit[argName]
let argValue = typeof arg === 'function' ? arg(context) : arg
if ( try {
('defaultValue' in argConfig && const isRequired =
argConfig.defaultValue && typeof argConfig.required === 'function'
typeof arg !== typeof argConfig.defaultValue && ? argConfig.required(context)
argConfig.inputType !== 'kcl') || : argConfig.required
(argConfig.inputType === 'kcl' &&
!(arg as Partial<KclCommandValue>).valueAst) ||
('options' in argConfig &&
typeof arg !== typeof argConfig.options[0].value)
) {
return reject({
message: 'Argument payload is of the wrong type',
arg: {
...argConfig,
name: argName,
},
})
}
if (!arg && argConfig.required) { const resolvedDefaultValue =
return reject({ 'defaultValue' in argConfig
message: 'Argument payload is falsy but is required', ? typeof argConfig.defaultValue === 'function'
arg: { ? argConfig.defaultValue(context)
...argConfig, : argConfig.defaultValue
name: argName, : undefined
},
}) const hasMismatchedDefaultValueType =
isRequired &&
typeof argValue !== typeof resolvedDefaultValue &&
!(argConfig.inputType === 'kcl' || argConfig.skip)
const hasInvalidKclValue =
argConfig.inputType === 'kcl' &&
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst
const hasInvalidOptionsValue =
isRequired &&
'options' in argConfig &&
!(
typeof argConfig.options === 'function'
? argConfig.options(context)
: argConfig.options
).some((o) => o.value === argValue)
if (
hasMismatchedDefaultValueType ||
hasInvalidKclValue ||
hasInvalidOptionsValue
) {
return reject({
message: 'Argument payload is of the wrong type',
arg: {
...argConfig,
name: argName,
},
})
}
if (!argValue && isRequired) {
return reject({
message: 'Argument payload is falsy but is required',
arg: {
...argConfig,
name: argName,
},
})
}
} catch (e) {
console.error('Error validating argument', context, e)
throw e
} }
} }

View File

@ -7,7 +7,7 @@ export const DEFAULT_FILE_NAME = 'Untitled'
export const fileMachine = createMachine( export const fileMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */ /** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiAKwBGcQDoALACYAHAE4AzGoUA2JXK1yANCACeiabOmSlGpkqsqAvg6NpMuQiXIyAEtSykuLAAzDDgKAGEAJzA8XmwQzGY2JBBuPgEhFLEESTVxWS1xBWVVcTU5PXEjUwRNFRkFWwB2FQVxJia5JnE5JxdQ92IyMB8-BLCAJTBSPBx40KThNNR+QWFslSVZJS1u3TUVSR1xJurEXSYZJslipismbTklPpBXbHwhr19YYNDYCOisXmiVYSx4Kwy6zMeQKRRKKjKFUKZwQPXqkjkmjUTCYWi0TQOCheb0GnhG31+mH+ABEwJg4pSwIsUstVplQNlcvkZIVikpSuVKijdFoZOItJJpEomtcYUTnK8Bh8yaMfuN-gB5DjTRnMzjgtlQnIwnlw-kIwXIkyIdSSGRKHF5XFqSwdYlKjzDVWM-4AZTAvCwsDpYAIcQgWAgqGiYa4kWMetSBshWTMNyaMkOuV0ChUBJUEpRnUuux6WmxGkkTF6CpJyq9URi-FIUEZFAgghGZAAblwANYjAiAuIAWnGidZKY50O5vPhiKF1oQcnF9q0myYCN2FSa4ndbnrXkbsTIrfGFDAkUicZkHHQsSCcZwMiHTbAY4WoJZybWqeNq82ddygUaQHiqJc7BkTRcwUOQjmKHR133d5PS8KYZhwU82w7Lwe37EZogw99xy-fV0l-adalzeQVForY1Ada4ni0FE5CaJRM0UAlmm6LRkNJL10NmLDz0va9Ilve9eEfSJn0I2ZiM-ZIyIhCjREQOoaLospGIxHYUQY0U8VaVoJUdB5+MPEZaXpETQnbTsZDwgcZAgENRxI5Sk3I9l1P-UVci6cUbg0JRlBRcRNkzHRdwUGVaPEOxLNQ6z3LszALyvG87wfJ9XPcxSQS8yc1M5PIMz0aU7gYuQCyOIsegaaUCTCwpnT42sPU+EYpjwKMWx9BzcNIXsXMBCAPypCcf187I7DUGQqweWDGgJNR8SLJ55AY9ibgLPJy2S7qZF6-qzz+IauxG-CZHGya4AYSRipmo15sWnFikUDoNA2pcXVkBQbFo7FuMkJojpVU70rCMTsqkmS5JiCb1WmnzXtyd7lq+tbfpqYoFEzSL9DqNofo6-oDxSigpiCaJYCIVHVNmxAtEaBpyxuZQ8iOaQUTBjNpWJxo2l3TpnheAI3PgFI6xSsE0b-EcWKXJWZBxHF2hAip0ySzrKeOikAh9eWmaNSVriappqy2CpWkkAzqLUJp8Q5h4DgRGsKZQg2xj+E3DT-c2OIlGVdwqFc4rUYUuntB0wesaQmhAvc9e9lVj2bc7MH9qc-OkNjMwFfkygLWqIpxe0cTsWCOiOE4IcE6ZhIG8Yc9KxBFFYlp5EkWirFB6Q1AbrwbIDaG2+ZnIegzTYLWLg59BUYUmAWwHKo3B51HlL2BLQpHoellSA8oooOOsSLGiRdowdY2r7RWu5OhdQLycVfWVS1aZx+-BXKPzmei70VLkvJcKhbBijKHicq3QQLiycEAA */
id: 'File machine', id: 'File machine',
initial: 'Reading files', initial: 'Reading files',
@ -24,6 +24,8 @@ export const fileMachine = createMachine(
})), })),
target: '.Reading files', target: '.Reading files',
}, },
Refresh: '.Reading files',
}, },
states: { states: {
'Has no files': { 'Has no files': {
@ -158,7 +160,8 @@ export const fileMachine = createMachine(
type: 'done.invoke.read-files' type: 'done.invoke.read-files'
data: ProjectWithEntryPointMetadata data: ProjectWithEntryPointMetadata
} }
| { type: 'assign'; data: { [key: string]: any } }, | { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },
}, },
predictableActionArguments: true, predictableActionArguments: true,

View File

@ -104,6 +104,7 @@ export type ModelingMachineEvent =
| { type: 'Constrain parallel' } | { type: 'Constrain parallel' }
| { type: 'Constrain remove constraints' } | { type: 'Constrain remove constraints' }
| { type: 'Re-execute' } | { type: 'Re-execute' }
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Equip Line tool' } | { type: 'Equip Line tool' }
| { type: 'Equip tangential arc to' } | { type: 'Equip tangential arc to' }
@ -119,7 +120,7 @@ export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine( export const modelingMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEohWO3Uh2UOSsmmhhn2jPyNGUolMhjWwvZoo8rUk3mJH2IZEomEdb0+9BGVN+qoQhoUPOMwgRi1BplkBqOZUDCkN4ksClEClt5zaHpJ7wdTveAEkrj0AkEugNwt7vr6VaBEoZQ2UKsIOcmrPMDRCLIKaOa6oKU6I0+LMx8c56C7jkCRXn0oABbMB3fwAN0enHIJEwiuiVZp-qsFskRxkmibmlSBX0Rg0hikifBcgk+SUGkH9uH2ff4+6k+nQTnC4Cd5UAebAAC9uHYDctx+at+CMeFJFUINu0NfJkRkA1xENcoNHkSwVmMCoZFfC531HLMv2IbhYBlEg8H8ICQPAu4N38CBsBo10wGgnd4hreDRA0Ts0hoKp0gtdRoTSdkLAqURwVwi0bxIjNc3Ij5KKIajaPolcHjXVj2M4ihuIrJVqT4uCA2WBRJGFY9UR1YxNAwy8EBkVlymsAVYSBM0X1cU4xTfNTP0LLTcBoh46NwfwAEEACFvH8AANHjlV3fjrOTWZxDEQwbCwmRbHEKThSE4VkUWVJzVqBpAsxELPXU-Nwu06L6MS5KAE10os2k9W2aw7GUOR9jNUboUTdRJCBOTLTUUMAqaO1SNC3NNPamL-DIKAuj6v0spvHJpCUapk0UOtTCk+S4X7Xz1WEUxTGEFSWpazbIp02KunwdgvQpbcMss2sShmFR1VEm80n2KTDlsptTGvJ6wxUV6GuCtbmrC3EIqi7amEeQncHY8hZUwEgniMyCTIO2Daw82ybEjKwMiOVRlCk+MhP5KoXqBBENDEN6yJx7o8e+hjgLAiCN0wPQdpwKAhjMoH+r3ZYZCZFMtAkXCMhWA1HMkYqddhfJ1RKEX1rHNqvo62K9IMzB5cV7BlbpzKrKsJ7cuPJ7HLMVEDVG7YUxeqo1mKxx6pW9N3rFqj7e22BcBIJh-HYVBUs9kGjCwqRlhBNQXuqMwpOyLWPPVeZVBTIFiIx1bVOxja7fx+jU-TzPs961WYK90GbwsJssNhPLjA0KSCpmQTHDkC1Qx1WOgubhO29xrb6LAABHWVWN+qB-tzgbiqkVEhaerI8gkC8ijrRNcpMcFuwm0xrdb23N+T+imEpuXqD914gNWQMwlgER7MjKeblDCRjAaUQ4z1MhqAHE3eOosN7iy3rFB4YBZyoBXP4cg2D2CwBPhrLQiFWbqmPNeFMpUYFKDKPhWw2QwRBkbnHIcNsKKFgAEpgEEGAPgIRZT3HIUdOoUgbywMTGkIWFRDDQnUFrdIqRkS1yUIcD+WYPqFmuHvbAGcAAyeAwA91QJuIBwMBoKBkhIFQzM9aSTckoMOmRhpyDmKiQwOiRyJwMbKIxmddoAWwKxSm5Ae4SKsiNWyIJLaWnkG4hh99jBlFPHXRQ6Q5jLVXugtScUADudFALS2YpBTAbEOI00oP4PAAAzVABAIDcDAO0XAS5UCvEkDAdgghGIyxYpgQQjTUAxMSCJISCIdSiVqM9ceUlrzmGTMeGGyhbCPT8dmYppSpZMVllU6mXF6m4CaQQR4DxgKSCYBTdgTSHizl6X4AZ5TDmjLOeM6x6ssoiW2IaBECZcLHhelJEEWtsimnBHUNCvi0HcOarsjgy5VzYHXEcmpJyxktLaR0rpPS+mCCdmijcHymkTMQDDLWN5LomGRLA-IUljxSEcDfOwpQgTbMkEigIxL0XVOMnU7Flzrm3JIPc4CTzCV8tJWMil7kipMm7GIReOsmWCkPBNVIIJzpKC5Ty+KSVUqnPOa03A7S8D4vaYSkgAAjWAgg+Bkq+YDAeecFWWlmqNe8cg6jrDcsVUah4MjXmXprWo+qSnIq6sa4VDwrkPBuXch5UqXl2odU6uV3zDqxLHrlHVdRIRzzKp6v1ZosLZFDO-eFTVdEGpjd1E1zSzUWs6d061ab7WCD0M6+V6zpCNgqLYOBuEpqmhmDYOx8l6HdjyY1LGdao0BAbU2i58bRXJslc8-p6bu29uzfTSlebzwrD8isUNU1LRCVNOyUaY08qcPyQixdezdpdFXS2vF7bt2CDfYIrNrrgH+hhuDSwN4b7PWDgGioPJIzdjAxUIMsJI2vvwO+uNCak3ipTT+v9+7AM2OA1hBG2Q9RWBLnWG6dZDzgayHIbsyx0ZcNrSOA1h9-oftxZa79hL2PvHwz6QjvysLbByNJKBMhzxwxyqaDlKg6xWBQ8ivjq6RWJrFRKx5P6+MCcrEJ3NnrjAmBTJaE8rkijLA1fMawdiFEonRDWhdrGl3+EJg8YmpNyaUwFbU8x2LP3cYJS8tzHm0VeYeIIY5JldPmRzZMzWdl6HWDvVHUQXM5DSH5HMM0kHfVKYCCFhcnmKZU0xSZVT671Obq04SwrJMwslci2VygMW1ZxcpQl4UShkseVS1JXymWCrzHNPZCQXLTHmosZgIsfRSxxD7ZJ8w15JNHEyEcPIqTKUvUPF10SNmkKPvnS3XRE3zFZ0sXiQxGcaYwDuOEqpkTokHsHpS2EQl9hmHBOoPmnM3L83zYoWEokHPMac9mU7U3JB5lwBwAgC2VBMgSatpGC8uTxhNiCSMkI8pPTSONsxkPoew7JARn5sT8ha3kseK95GNnQlhEtlYEhNkbPBKg0Hx2RwQ-O5gSQuBJUbhmyWEI83nvuusLUaQx54wcIyDXLkQtZrXgUdza8WF8eTZ55IAActnAACqgPApCCBxQgBAQIkF9KucN3cPt8wKqv3NIKRYOouQbNyhHLIigzoa7O6gSxOv9c2+N6QEyVjSftYDJocwJg0jVFErrFxRQshlBsIg0ox4VhcqJ+wOHYuBr4VmBdOlDGE8GmBLMDRywoWWbhRz96Oe4fkkE2T2sKEmSQkhHMWQ4ICrl4PIoPWhxLORhB0+lj2YAAqoS7sRIeFErOQv+ii4j4e9yo1zAbKUMKRMthirl+vEyK9knLC4RTFy6f+Awlz4X80wJwTuf+-Dy3yPlnpmJkH8KU05nEBIx5ObNIPCU0OYJjcfMHSQWUEmbOTifSd8AAeVwBxXNS-R6Tim8En0EEgNaUEBgPYHgJVlXxewDFkCkDmBMFwlEkyEWDvkQHNFshbEQxpScW2X8H538AaRIEoB6DmzYjAA4PJgKwpnNXlUEAKnMASWM0NFDAkFgS5AyyRnZCG0sAfFAKO0kDIGwFnHFVaB7lcyEO6ACzbR6Q0K0PuEECzkEA4MoHlUUUPGoMWAyCWChDclGniSsDBBBVDHUDehMO0PwF0LFXNSXzm0GD7SBBozkAXiQlDGhDnkPFWDsHBBWBqB8Jh1MJ0Kzn8GERqR0KJE9CQNbStXULSPFUEQsLyKzBsLNCoVZGeg8jSFhGhDoQHWPGALsDmFoRcECn5wwHgCiCOxfzX0EEWDKFyGySQk0B0DckEEEiVSgX2DyFGktDehxDAEGKIPpEoUehehBFwhPGUXRwkDUA+3cMsGrXr3fHWPdXZCZmLxUFLyRimi1g2UqEDmcnZzAM5w-EwSuJAWehNh1HjDymqF2GjGSBvDylSGqAtBhPy32SGUqR8yxU+V+L3CDCZgyFLn5BqFSDBWSABDvHmFsH5DhJlQxUFT8xRL01byMBUU1V7ykQ3yUQDXowsG8m5mG1RDhJjRSibVRKOjsSkDUCKlH0SNEjKiwh2CqlUAWhTEO0xi+O5RcxXTGX5O9jsUp1oWI0rjHWqOrkHSqGhLhL-T5OpMjyOGFEQjvByAcDyEjBujUCZDjxg0yFGjqDhJU1VLNLXwtMZAyFWBM1HzbADW5FmhhMcGRCRktjhLq2K28yiyFSpNix9OTH-3ZHkjPDTxKH63R15mTAtEbBtEc0VMf0sTVMmXsBHhKGTEnXRNiNUBNmVxWBl1wmRF90Jxh3YHLMpX2GYTqhrMNLWC5HBPmByRWC7w2XbK1350eQ3G7PchsAnXHmKjUFjx-wQBT1mEUMjCKiDFkCnKf0D38ANyNz6OTKIOsAkBNiYS0HsDSENjcibCrnjATCOCbFvOz07PnMIk331msBMG3z7xgRjBo3UATCTHP2LPekv1u04Bvx7nnLkE0Fmj8hUAciemuhgQYwBKDA5GvisA+LULIiwOgMtzwNzAQO-KtHKF7FTL9VkEwqKEEh5DmAZTPyqnVBYLYKsLWO9I2L7BouWVUDqCekmhcIy2vHsGnS0FkFTCgt8M4H8MyMCN4vPPdUtCkBelZxsHkFpzkJ5AAojLnmmlUIVOKM0L8KgF0OyJolyNzG-McBNlhGQjynZHZSmibF5HsGoKFmKgKi6KcCAA */ /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEFThkNEymFGlECkM0MM+0ZMgUa1MwuypgRhlFHlaEpufBYD3YiuiVN+qrpdg0klMwkFaU0ermwuhwlUkiBhxUyKUazt5za3mJH2IZEomFTb0+9BGnpVoESRrNkmMga0ahMplkhqOZVL+sM4ksCj1SfFOZJ70k3Y+AEkrj0AkEugNwvnvoWad7DIGyuqOe2rPNDRCLIKaKJa6JBXrRJ2Hf3eyeh7jkCRXn0oABbMB3fwAN0enHIJEw7p+Rf4RhkpkbapNEjTRUgKfQjA0FsUnBOQJHyJQNCPC4Tz7NN3nPbpL2vII7wfAJ3lQB5sAAL24dgPy-Gd4mLP9A0kVQzW3I18mRGRDXEI1yl1LJDBWYwKhkZCU3QtDc0w4huFgGUSDwfxCOIsi7g-fwIGwaTMzAKjlVnWiECsPdNzSGgqnSAD1GhNJ2QsCpRHBXUAJbYSxJ7FzB2HIgpJkuSX1dbB30wVT1IoigtKnJVqRo399OWBR-TyFdlCgtRBUs1lymsAVYRjPdnNQs8PK8h5ZNwfwAEEACFvH8AANbTItpKx21mcQxEMGxOP-NJLJ1eL41gndagaVxTjFY9RIK3FPNwaTirkyrqoATXqr09Ka7ZrDsZQ5H2ZQtXYiDijUOKgVsvi1ErPKJvQiTptmkr-DIKAuhWn8S3SKQQRBU15H1YRTEsuy4QPbKtX+gMrtzNyMMKmbvNKrp8HYPMKQ9HSove1rpD21QANkTjREsw4Tu1KD-oRBNhEh1zJu6O74f8JhHiZ3A1PIWVMBIJ41I00LXt06KrFNWYLU6jIjlUZRLP1P1+SqAMgQRDQxGpj5oduoqHoU0jyI-TA9EenAoCGcK0YaudlhkJlQ3bAEMhWQ1UR5f9Q1hfItRKVXTxu2H7p819-L1g2P2wY3+YxujzByU1-qdsxUUNbbtj1AMqjWf9HGGpp7RQ67xN9hnYFwEgmH8dhUFq8PGs4qRlhBNQA2qMxLOyK2ZDkeZ5m3Rx2699WC7m0qi5LsuK+W03vwF97oMyfjYVa4wNEs9qZj3Hud3nOYcj72nJLhwf-DAABHWUVMRqBkari3a3KFf-qyPIJHAop51bFqTHBbccdMHefamzW5JMC5nragE9qLV3yBYdu-EdzpE3oaWsMwTCcUOBaTIahDwjUxONKGu96YHweGAW8qAXz+HIAAu4sAr5rWsHFQ41h9qaH2DoQ6RxOIWD4rYbIYIzRCSwWNXOuC-7dAAEpgEEGAPgIRZT3GoYLOoUgWyGFrKkdIYgE6HXUFbdIqRkSdwTLafhOcRJCPzpKE+2BS4ABk8BgFHqgT8YD0aNQUNZCQKgbDCnSBZTRdReSK3bnuPaRpf5mJuBY0uIUYB3GwCpLm5BR5yMSFtOKIIPZ8V+go+slhoxrFUJkhESU+5lQAO6yQIkRHWylAo8xCpQfweAABmqACAQG4GAdouAnyoFeJIGA7BBDayUhRTAggmmoCSYgYyfpCkZADGYBW4hLJQXMO2U0aROEdypkY5M0NJClPKfJSpwyVK1M0g03AzSCCPAeERSQTBObsGaQ8W8fS-CDOObrUZ4zJkIGMtsI0NoWy6lNAGSy31Nw0FUCYPIkZDHZ12ahA5HBnwBwCkFXm9TxmtPaZ07pvT+mCF8m+D8YzLkTKcebPSGyrYtkUPYZEyj8hpSUEyMQcg7ClCBMUspKLiWBxqcFc52Kbl3IeSQJ5RFXmEv5QFMlzTfk0qZNuMQAExCaDSoKcsONVFaOZDyw5C1aoXKuW03AHS8D4o6YSkgAAjWAgg+DyopajSeEc-koNyTIWCcg6jrEOv+ba5YMhQSSrIbatQDUoqNTVE1LTRUPHuY8550r3l2odU6n5lLVrRQ2TMHIIIlA7m3FoHqfFpB6j2pxbIgYf47K7KJZFAQjWLTjTi81eKenWrTfawQehnWKs9XUeQFRbCIN1NCVsyIRauLsnqVsuV604Nck28qVV-CtpFQ8W5ibxWSpeW8gZ6a+0DuzW9KZnEkGsVassFYobJ18T9FC9k20do3qjQEJ6XQ21motV0rth7BBfvEVm114DvR5pSEaOocgLQaKKHBHkKi7JGgqGaWEH7Hr4G-VundSaJUpsA8B09YHnEQc4idbITUrAN3nIDec5YWzyFSHO5Y2yEUNqhqu8+yMf24stQBwlPH3gkYLGR6lBMWpWTJluZlAaOQWBWJelQ84rCYeE22hN+H92poGcJ0T05xO5s9cYGF+o1jKwOghpKVt5i0OtGIFE6Il2CJXbygITMHgszZhzLmGK6l2Oxb+ztBL3mee8-5XzDxBBnNCgZiKObkmW3ikoawr706EwDdUbYC85h7Tg76zD4WHw+c5tzIVoVNPbrFcmqVgHius0i2VmLFXKDxbNolqZyXhSpbmNA7LllsrSH5HlncwpCsuZMa5Gx5r7GYBHH0cccRFXeqjjRo4mQjh5CWQGgM5YesmVoYxPhHHl1qxm3Y8uDi8QRLLk9fCsTArxMSWeqeUzYR+n2GYcE6h5ZS0OgrFqrVHBZHZH3C7c3JADlwBwAgK2VBMjSRt60sGuT6kkMGWskJWr-TSOD2xkPoew7JKRqlub8hWzsqaR9NHtQRn+rMFYEhbBakhNufHs2ruYEkLgKVH4FtjhCMt177rrC1GkKafUvCMhan+0UJKMwFzKz1O1KCnEOeXdQA4yQAA5CuAAFVAeB2CwAIGVCAEBAgUVdIzI3dxFXzD9JqfLgpFhJS5NqFqqcsiKCUAoDXkO9f+EN8b03pBQqONJ51-SmhzAmGDKtrQep-VFF4iLVBpRTQrD7kT9gcOReNUsDyJPWQVDbkT4aYEjPIzLChboqCOeYd55J2JsnJZmJMkhJCOYshwTtUrwBaMts5irg6s507rm1YABV7sxLiQ8BJ5cBf9GF1H89fztrmG1EoLxh3-yV6gkyR93rLC6g7JNvZM-8APfn4vlp1xbsQ65w7hE79FA7jsttaE1oeRuzSMxqFOYdjUaYxPZWUVmCuDSV0E8AAeVwHbT-StX2W8Cn0EHALaUECgPYFgJNjXze30nxkZxMF1BMkyEWGfkQB3DihXDQzpU8UwQnymw+H8F538EaRIEoB6CW1UjAHYI5g805nNV+UEHanMDSRhSNEDAkGUS5DkH9BsEODmEsDgmAOwQuDIGwFvAlVaFHkZkEO6GCwE16Q0K0PuEEHLkEHYMoF+QqEAjIOyB9ShEOm2lSSsDBFBUDHUGchMO0PwF0PFXNWXyW0GEVSBEYw5R3EYkDGhDXnLFWDsHBBWBqG8Jh1MJ0PLkPj4GCh0KJFzAQJCw6R8LMIsNyJ7BsL2gYisFqAtHbjSFhGhCghywSkAN9BcKQhOF5wwHgCiDUKgFb2j0EEWDKFyEUDL3UC0B2yKEGJNC1EsFqCFGMDNGchxDAH6PX3pC0HKGRHmRDGAmhHUEXFbH1GkOMmUS9jWPwPZDihLxMHyQr0OiQQWGtEGlsHmAwwv3ymEQuPdRbAtAxySn1GB19UyxfiZTiNalSGqAAmhMwyGS+X82FXJW+JcTWFmDmUUH5BqFSHBWSABFbChW1HyHhRAMRUbXc1RT8nRViyxSRMMzbyMC0W1T7wUU3wNADTkEZFSCghlnmFsMwxjTjWRLnFcSkDUE6lrG2lMwBl23YV1HjFUHOj1BOxJM4zc0NXXU3VpIS3XyOGSAShyA21SkOinQ2jghHSqChMw2A0FLpOj11O2BWHshKCqDyFrEBjUCZGDAqFUBrzqHUwfAvneBtO1PwN1MZAyFWBVwlLXHkwR3+mUUcG2LlKzhVLO17FXQa1Kz82pMCy1I6x1PbF-3ZDslAgUJKEG3RzlnbAAnVDBw+NEify10wCFOpXsAsCGnbBsHUDWBiKjG5MEmQVtnH1TMn17EbO11zxbNzX2DKEyBKE7ItJ7OcOSD4jvR72721ADy5x5z52bNtPXyFHzXnn-GrFkCs0QDTwUOUUzzNFkC3KbN1wNztxNynOSV1FriUHan1HjxUEmIvKDWp2gyOEjC0GJN6L2UnP3NDO3C311EFAjJ3371YQbEYwOJbDbHP0YMv1n04Fv1HlfKmVDGjBjBUB+nBnXDyH+LNA5AfgMj7nQMgOt2wPQjgIIv0nNHKDqCTyZ1S2lKKCCUZyZTP3jC1C9hYIrisNWKgvdSmDkIkBWVUDqH+i-2cLkKgnsDnQmNNBSM0N8KgH8P0LYr4ikADG1CtHkFp1kJ5BMHUEjDXiONUIETaCKPSIrkkWyL8NKI+CMscAx1hCYlanZE5UnUjF5HsHIMswAnaKwpcr8IyICLsQAApyEmA9AABKNikQCQCXTJRiTQFhIoHhRTF9QJOlZU8C2K-S+K-Q-wZK1AVKtK2qlK9KzKsQT6OQUYvKiYydBESnHcawA0muYUFwFwIAA */
id: 'Modeling', id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0, tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
@ -170,6 +171,13 @@ export const modelingMachine = createMachine(
actions: ['AST extrude'], actions: ['AST extrude'],
internal: true, internal: true,
}, },
Export: {
target: 'idle',
internal: true,
cond: 'Has exportable geometry',
actions: 'Engine export',
},
}, },
entry: 'reset client scene mouse handlers', entry: 'reset client scene mouse handlers',
@ -481,6 +489,7 @@ export const modelingMachine = createMachine(
'animate after sketch', 'animate after sketch',
'tear down client sketch', 'tear down client sketch',
'remove sketch grid', 'remove sketch grid',
'engineToClient cam sync direction',
], ],
entry: ['add axis n grid', 'conditionally equip line tool'], entry: ['add axis n grid', 'conditionally equip line tool'],
@ -514,6 +523,8 @@ export const modelingMachine = createMachine(
internal: true, internal: true,
}, },
}, },
entry: 'clientToEngine cam sync direction',
}, },
'animating to existing sketch': { 'animating to existing sketch': {
@ -524,7 +535,12 @@ export const modelingMachine = createMachine(
onDone: 'Sketch', onDone: 'Sketch',
}, },
], ],
entry: 'clientToEngine cam sync direction',
}, },
'animating to plane (copy)': {},
'animating to plane (copy) (copy)': {},
}, },
initial: 'idle', initial: 'idle',
@ -824,13 +840,13 @@ export const modelingMachine = createMachine(
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.event.which !== 1) return if (args.mouseEvent.which !== 1) return
const { intersection2d } = args const { intersectionPoint } = args
if (!intersection2d || !sketchPathToNode) return if (!intersectionPoint?.twoD || !sketchPathToNode) return
const { modifiedAst } = addStartProfileAt( const { modifiedAst } = addStartProfileAt(
kclManager.ast, kclManager.ast,
sketchPathToNode, sketchPathToNode,
[intersection2d.x, intersection2d.y] [intersectionPoint.twoD.x, intersectionPoint.twoD.y]
) )
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
sceneEntitiesManager.removeIntersectionPlane() sceneEntitiesManager.removeIntersectionPlane()
@ -845,6 +861,12 @@ export const modelingMachine = createMachine(
// (note the orbit controls are always active though) // (note the orbit controls are always active though)
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
}, },
'clientToEngine cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'clientToEngine'
},
'engineToClient cam sync direction': () => {
sceneInfra.camControls.syncDirection = 'engineToClient'
},
}, },
// end actions // end actions
} }

View File

@ -72,7 +72,7 @@ const Home = () => {
} }
) )
const [state, send] = useMachine(homeMachine, { const [state, send, actor] = useMachine(homeMachine, {
context: { context: {
projects: loadedProjects, projects: loadedProjects,
defaultProjectName, defaultProjectName,
@ -176,6 +176,7 @@ const Home = () => {
send, send,
state, state,
commandBarConfig: homeCommandBarConfig, commandBarConfig: homeCommandBarConfig,
actor,
}) })
useEffect(() => { useEffect(() => {

View File

@ -21,7 +21,7 @@ export default function Export() {
<section className="flex-1"> <section className="flex-1">
<h2 className="text-2xl font-bold">Export</h2> <h2 className="text-2xl font-bold">Export</h2>
<p className="my-4"> <p className="my-4">
Try opening the project menu and clicking "Export Model". Try opening the project menu and clicking "Export Part".
</p> </p>
<p className="my-4"> <p className="my-4">
{APP_NAME} uses{' '} {APP_NAME} uses{' '}

157
src/wasm-lib/Cargo.lock generated
View File

@ -246,7 +246,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -257,7 +257,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -574,9 +574,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.0" version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -584,9 +584,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.0" version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -606,7 +606,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -856,7 +856,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -897,7 +897,7 @@ checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
"synstructure 0.13.0", "synstructure 0.13.0",
] ]
@ -949,7 +949,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -965,7 +965,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -977,7 +977,7 @@ dependencies = [
"diesel_table_macro_syntax", "diesel_table_macro_syntax",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -986,7 +986,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
dependencies = [ dependencies = [
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -1036,7 +1036,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -1118,7 +1118,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -1335,7 +1335,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -1404,9 +1404,9 @@ dependencies = [
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.12.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [ dependencies = [
"color_quant", "color_quant",
"weezl", "weezl",
@ -1440,7 +1440,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -1715,9 +1715,9 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.24.8" version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder", "byteorder",
@ -1877,9 +1877,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.68" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1947,14 +1947,14 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.54" version = "0.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13958174d876353f429ea8230dc92fe86f164819cea2e51bbf22e01a4c2a496e" checksum = "4080db4364c103601db486e4a8aa889ea56c011991e4c454373d8050a165d3da"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1990,11 +1990,12 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan" name = "kittycad-execution-plan"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"bytes", "bytes",
"insta", "insta",
"kittycad", "kittycad",
"kittycad-execution-plan-macros",
"kittycad-execution-plan-traits", "kittycad-execution-plan-traits",
"kittycad-modeling-cmds", "kittycad-modeling-cmds",
"kittycad-modeling-session", "kittycad-modeling-session",
@ -2008,18 +2009,18 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-macros" name = "kittycad-execution-plan-macros"
version = "0.1.6" version = "0.1.8"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
name = "kittycad-execution-plan-traits" name = "kittycad-execution-plan-traits"
version = "0.1.11" version = "0.1.12"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@ -2028,8 +2029,8 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.1.25" version = "0.1.28"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2056,18 +2057,18 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds-macros" name = "kittycad-modeling-cmds-macros"
version = "0.1.1" version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
name = "kittycad-modeling-session" name = "kittycad-modeling-session"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#9cb86ba54e4a60aa775fa2fd8af6f0ac9d05ebeb" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
dependencies = [ dependencies = [
"futures", "futures",
"kittycad", "kittycad",
@ -2258,9 +2259,9 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.9" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
@ -2426,7 +2427,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openapitor" name = "openapitor"
version = "0.0.9" version = "0.0.9"
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#8db292eaa7be0292512a2cdbef09f2d37af7c79c" source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#6f38abe149c74aa9675e9f0d370aa2f78980dc2d"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -2499,7 +2500,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -2656,7 +2657,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.7.5", "regex-syntax 0.7.5",
"structmeta 0.2.0", "structmeta 0.2.0",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -2670,7 +2671,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.2", "regex-syntax 0.8.2",
"structmeta 0.3.0", "structmeta 0.3.0",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -2736,7 +2737,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3604,7 +3605,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3620,9 +3621,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.113" version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [ dependencies = [
"indexmap 2.2.2", "indexmap 2.2.2",
"itoa", "itoa",
@ -3638,7 +3639,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3659,7 +3660,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3916,7 +3917,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive 0.2.0", "structmeta-derive 0.2.0",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3928,7 +3929,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive 0.3.0", "structmeta-derive 0.3.0",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3939,7 +3940,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -3950,7 +3951,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -4022,9 +4023,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.49" version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4057,7 +4058,7 @@ checksum = "285ba80e733fac80aa4270fbcdf83772a79b80aa35c97075320abfee4a915b06"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
"unicode-xid", "unicode-xid",
] ]
@ -4187,7 +4188,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -4294,7 +4295,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -4457,7 +4458,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -4485,7 +4486,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -4569,7 +4570,7 @@ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
"termcolor", "termcolor",
] ]
@ -4797,9 +4798,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -4807,16 +4808,16 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4835,9 +4836,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -4845,22 +4846,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]] [[package]]
name = "wasm-lib" name = "wasm-lib"
@ -5137,9 +5138,9 @@ dependencies = [
[[package]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]] [[package]]
name = "winapi" name = "winapi"
@ -5469,7 +5470,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]
@ -5489,7 +5490,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.49", "syn 2.0.52",
] ]
[[package]] [[package]]

View File

@ -14,14 +14,14 @@ bson = { version = "2.9.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad = { workspace = true } kittycad = { workspace = true }
serde_json = "1.0.108" serde_json = "1.0.114"
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] } uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.41" wasm-bindgen-futures = "0.4.41"
[dev-dependencies] [dev-dependencies]
anyhow = "1" anyhow = "1"
image = "0.24.8" image = "0.24.9"
kittycad = { workspace = true, default-features = true } kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
reqwest = { version = "0.11.24", default-features = false } reqwest = { version = "0.11.24", default-features = false }
@ -31,7 +31,7 @@ uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
futures = "0.3.30" futures = "0.3.30"
js-sys = "0.3.68" js-sys = "0.3.69"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen-futures = { version = "0.4.41", features = ["futures-core-03-stream"] } wasm-bindgen-futures = { version = "0.4.41", features = ["futures-core-03-stream"] }
wasm-streams = "0.4.0" wasm-streams = "0.4.0"
@ -58,7 +58,7 @@ members = [
] ]
[workspace.dependencies] [workspace.dependencies]
kittycad = { version = "0.2.54", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.2.59", default-features = false, features = ["js", "requests"] }
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -19,7 +19,7 @@ quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.49", features = ["full"] } syn = { version = "2.0.52", features = ["full"] }
[dev-dependencies] [dev-dependencies]
expectorate = "1.1.0" expectorate = "1.1.0"

View File

@ -19,4 +19,4 @@ uuid = "1.7"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "1"
serde_json = "1.0.113" serde_json = "1.0.114"

View File

@ -105,6 +105,10 @@ impl BindingScope {
"startSketchAt".into(), "startSketchAt".into(),
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)), EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
), ),
(
"lineTo".into(),
EpBinding::from(KclFunction::LineTo(native_functions::sketch::LineTo)),
),
]), ]),
parent: None, parent: None,
} }

View File

@ -45,11 +45,12 @@ pub enum CompileError {
NoReturnStmt, NoReturnStmt,
#[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")] #[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")]
NotInPipeline, NotInPipeline,
#[error("The function '{fn_name}' expects a parameter of type {expected} but you supplied {actual}")] #[error("The function '{fn_name}' expects a parameter of type {expected} as argument number {arg_number} but you supplied {actual}")]
ArgWrongType { ArgWrongType {
fn_name: &'static str, fn_name: &'static str,
expected: &'static str, expected: &'static str,
actual: String, actual: String,
arg_number: usize,
}, },
} }

View File

@ -252,6 +252,7 @@ impl Planner {
} = match callee { } = match callee {
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?, KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?, KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
KclFunction::LineTo(f) => f.call(&mut self.next_addr, args)?,
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?, KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
KclFunction::UserDefined(f) => { KclFunction::UserDefined(f) => {
let UserDefinedFunction { let UserDefinedFunction {
@ -619,6 +620,7 @@ impl Eq for UserDefinedFunction {}
enum KclFunction { enum KclFunction {
Id(native_functions::Id), Id(native_functions::Id),
StartSketchAt(native_functions::sketch::StartSketchAt), StartSketchAt(native_functions::sketch::StartSketchAt),
LineTo(native_functions::sketch::LineTo),
Add(native_functions::Add), Add(native_functions::Add),
UserDefined(UserDefinedFunction), UserDefined(UserDefinedFunction),
} }

View File

@ -2,6 +2,5 @@
pub mod helpers; pub mod helpers;
pub mod stdlib_functions; pub mod stdlib_functions;
pub mod types;
pub use stdlib_functions::StartSketchAt; pub use stdlib_functions::{LineTo, StartSketchAt};

View File

@ -1,4 +1,4 @@
use kittycad_execution_plan::{api_request::ApiRequest, Instruction}; use kittycad_execution_plan::{api_request::ApiRequest, Destination, Instruction};
use kittycad_execution_plan_traits::{Address, InMemory}; use kittycad_execution_plan_traits::{Address, InMemory};
use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint}; use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint};
@ -35,23 +35,31 @@ pub fn stack_api_call<const N: usize>(
})) }))
} }
pub fn single_binding(b: EpBinding, fn_name: &'static str, expected: &'static str) -> Result<Address, CompileError> { pub fn single_binding(
b: EpBinding,
fn_name: &'static str,
expected: &'static str,
arg_number: usize,
) -> Result<Address, CompileError> {
match b { match b {
EpBinding::Single(a) => Ok(a), EpBinding::Single(a) => Ok(a),
EpBinding::Sequence { .. } => Err(CompileError::ArgWrongType { EpBinding::Sequence { .. } => Err(CompileError::ArgWrongType {
fn_name, fn_name,
expected, expected,
actual: "array".to_owned(), actual: "array".to_owned(),
arg_number,
}), }),
EpBinding::Map { .. } => Err(CompileError::ArgWrongType { EpBinding::Map { .. } => Err(CompileError::ArgWrongType {
fn_name, fn_name,
expected, expected,
actual: "object".to_owned(), actual: "object".to_owned(),
arg_number,
}), }),
EpBinding::Function(_) => Err(CompileError::ArgWrongType { EpBinding::Function(_) => Err(CompileError::ArgWrongType {
fn_name, fn_name,
expected, expected,
actual: "function".to_owned(), actual: "function".to_owned(),
arg_number,
}), }),
} }
} }
@ -60,6 +68,7 @@ pub fn sequence_binding(
b: EpBinding, b: EpBinding,
fn_name: &'static str, fn_name: &'static str,
expected: &'static str, expected: &'static str,
arg_number: usize,
) -> Result<Vec<EpBinding>, CompileError> { ) -> Result<Vec<EpBinding>, CompileError> {
match b { match b {
EpBinding::Sequence { elements, .. } => Ok(elements), EpBinding::Sequence { elements, .. } => Ok(elements),
@ -67,16 +76,62 @@ pub fn sequence_binding(
fn_name, fn_name,
expected, expected,
actual: "single".to_owned(), actual: "single".to_owned(),
arg_number,
}), }),
EpBinding::Map { .. } => Err(CompileError::ArgWrongType { EpBinding::Map { .. } => Err(CompileError::ArgWrongType {
fn_name, fn_name,
expected, expected,
actual: "object".to_owned(), actual: "object".to_owned(),
arg_number,
}), }),
EpBinding::Function(_) => Err(CompileError::ArgWrongType { EpBinding::Function(_) => Err(CompileError::ArgWrongType {
fn_name, fn_name,
expected, expected,
actual: "function".to_owned(), actual: "function".to_owned(),
arg_number,
}), }),
} }
} }
/// Extract a 2D point from an argument to a Cabble.
pub fn arg_point2d(
arg: EpBinding,
fn_name: &'static str,
instructions: &mut Vec<Instruction>,
next_addr: &mut Address,
arg_number: usize,
) -> Result<Address, CompileError> {
let expected = "2D point (array with length 2)";
let elements = sequence_binding(arg, "startSketchAt", "an array of length 2", arg_number)?;
if elements.len() != 2 {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: format!("array of length {}", elements.len()),
arg_number: 0,
});
}
// KCL stores points as an array.
// KC API stores them as Rust objects laid flat out in memory.
let start = next_addr.offset_by(2);
let start_x = start;
let start_y = start + 1;
let start_z = start + 2;
instructions.extend([
Instruction::Copy {
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
destination: Destination::Address(start_x),
length: 1,
},
Instruction::Copy {
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
destination: Destination::Address(start_y),
length: 1,
},
Instruction::SetPrimitive {
address: start_z,
value: 0.0.into(),
},
]);
Ok(start)
}

View File

@ -1,4 +1,8 @@
use kittycad_execution_plan::{api_request::ApiRequest, Instruction}; use kittycad_execution_plan::{
api_request::ApiRequest,
sketch_types::{self, Axes, BasePath, Plane, SketchGroup},
Destination, Instruction,
};
use kittycad_execution_plan_traits::{Address, InMemory, Value}; use kittycad_execution_plan_traits::{Address, InMemory, Value};
use kittycad_modeling_cmds::{ use kittycad_modeling_cmds::{
shared::{Point3d, Point4d}, shared::{Point3d, Point4d},
@ -6,12 +10,84 @@ use kittycad_modeling_cmds::{
}; };
use uuid::Uuid; use uuid::Uuid;
use super::helpers::{arg_point2d, no_arg_api_call, single_binding, stack_api_call};
use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan}; use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan};
use super::{ #[derive(Debug, Clone)]
helpers::{no_arg_api_call, sequence_binding, single_binding, stack_api_call}, #[cfg_attr(test, derive(Eq, PartialEq))]
types::{Axes, BasePath, Plane, SketchGroup}, pub struct LineTo;
};
impl Callable for LineTo {
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let mut instructions = Vec::new();
let fn_name = "lineTo";
// Get both required params.
let mut args_iter = args.into_iter();
let Some(to) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 0,
});
};
let Some(sketch_group) = args_iter.next() else {
return Err(CompileError::NotEnoughArgs {
fn_name: fn_name.into(),
required: 2,
actual: 1,
});
};
// Check the type of both required params.
let to = arg_point2d(to, fn_name, &mut instructions, next_addr, 0)?;
let sg = single_binding(sketch_group, fn_name, "sketch group", 1)?;
let id = Uuid::new_v4();
let start_of_line = next_addr.offset(1);
let length_of_3d_point = Point3d::<f64>::default().into_parts().len();
instructions.extend([
// Push the `to` 2D point onto the stack.
Instruction::Copy {
source: to,
length: 2,
destination: Destination::StackPush,
},
// Make it a 3D point.
Instruction::StackExtend { data: vec![0.0.into()] },
// Append the new path segment to memory.
// First comes its tag.
Instruction::SetPrimitive {
address: start_of_line,
value: "Line".to_owned().into(),
},
// Then its end
Instruction::StackPop {
destination: Some(start_of_line + 1),
},
// Then its `relative` field.
Instruction::SetPrimitive {
address: start_of_line + 1 + length_of_3d_point,
value: false.into(),
},
// Send the ExtendPath request
Instruction::ApiRequest(ApiRequest {
endpoint: ModelingCmdEndpoint::ExtendPath,
store_response: None,
arguments: vec![
// Path ID
InMemory::Address(sg + SketchGroup::path_id_offset()),
// Segment
InMemory::Address(start_of_line),
],
cmd_id: id.into(),
}),
]);
// TODO: Create a new SketchGroup from the old one + add the new path, then store it.
Ok(EvalPlan {
instructions,
binding: EpBinding::Single(Address::ZERO + 9999),
})
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))] #[cfg_attr(test, derive(Eq, PartialEq))]
@ -29,42 +105,10 @@ impl Callable for StartSketchAt {
actual: 0, actual: 0,
}); });
}; };
let start_point = { let start_point = arg_point2d(start, "startSketchAt", &mut instructions, next_addr, 0)?;
let expected = "2D point (array with length 2)";
let fn_name = "startSketchAt";
let elements = sequence_binding(start, "startSketchAt", "an array of length 2")?;
if elements.len() != 2 {
return Err(CompileError::ArgWrongType {
fn_name,
expected,
actual: format!("array of length {}", elements.len()),
});
}
// KCL stores points as an array.
// KC API stores them as Rust objects laid flat out in memory.
let start = next_addr.offset_by(2);
let start_x = start;
let start_y = start + 1;
let start_z = start + 2;
instructions.extend([
Instruction::Copy {
source: single_binding(elements[0].clone(), "startSketchAt (first parameter, elem 0)", "number")?,
destination: start_x,
},
Instruction::Copy {
source: single_binding(elements[1].clone(), "startSketchAt (first parameter, elem 1)", "number")?,
destination: start_y,
},
Instruction::SetPrimitive {
address: start_z,
value: 0.0.into(),
},
]);
start
};
let tag = match args_iter.next() { let tag = match args_iter.next() {
None => None, None => None,
Some(b) => Some(single_binding(b, "startSketchAt", "a single string")?), Some(b) => Some(single_binding(b, "startSketchAt", "a single string", 1)?),
}; };
// Define some constants: // Define some constants:
@ -141,9 +185,9 @@ impl Callable for StartSketchAt {
name: Default::default(), name: Default::default(),
}, },
path_rest: Vec::new(), path_rest: Vec::new(),
on: super::types::SketchSurface::Plane(Plane { on: sketch_types::SketchSurface::Plane(Plane {
id: plane_id, id: plane_id,
value: super::types::PlaneType::XY, value: sketch_types::PlaneType::XY,
origin, origin,
axes, axes,
}), }),

View File

@ -1,133 +0,0 @@
use kittycad_execution_plan::Instruction;
use kittycad_execution_plan_macros::ExecutionPlanValue;
use kittycad_execution_plan_traits::{Address, Value};
use kittycad_modeling_cmds::shared::{Point2d, Point3d, Point4d};
use uuid::Uuid;
/// A sketch group is a collection of paths.
#[derive(Clone, ExecutionPlanValue)]
pub struct SketchGroup {
/// The id of the sketch group.
pub id: Uuid,
/// What the sketch is on (can be a plane or a face).
pub on: SketchSurface,
/// The position of the sketch group.
pub position: Point3d,
/// The rotation of the sketch group base plane.
pub rotation: Point4d,
/// The X, Y and Z axes of this sketch's base plane, in 3D space.
pub axes: Axes,
/// The plane id or face id of the sketch group.
pub entity_id: Option<Uuid>,
/// The base path.
pub path_first: BasePath,
/// Paths after the first path, if any.
pub path_rest: Vec<Path>,
}
impl SketchGroup {
pub fn set_base_path(&self, sketch_group: Address, start_point: Address, tag: Option<Address>) -> Vec<Instruction> {
let base_path_addr = sketch_group
+ self.id.into_parts().len()
+ self.on.into_parts().len()
+ self.position.into_parts().len()
+ self.rotation.into_parts().len()
+ self.axes.into_parts().len()
+ self.entity_id.into_parts().len()
+ self.entity_id.into_parts().len();
let mut out = vec![
// Copy over the `from` field.
Instruction::Copy {
source: start_point,
destination: base_path_addr,
},
// Copy over the `to` field.
Instruction::Copy {
source: start_point,
destination: base_path_addr + self.path_first.from.into_parts().len(),
},
];
if let Some(tag) = tag {
// Copy over the `name` field.
out.push(Instruction::Copy {
source: tag,
destination: base_path_addr
+ self.path_first.from.into_parts().len()
+ self.path_first.to.into_parts().len(),
});
}
out
}
}
/// The X, Y and Z axes.
#[derive(Clone, Copy, ExecutionPlanValue)]
pub struct Axes {
pub x: Point3d,
pub y: Point3d,
pub z: Point3d,
}
#[derive(Clone, ExecutionPlanValue)]
pub struct BasePath {
pub from: Point2d<f64>,
pub to: Point2d<f64>,
pub name: String,
}
/// A path.
#[derive(Clone, ExecutionPlanValue)]
pub enum Path {
/// A path that goes to a point.
ToPoint { base: BasePath },
/// A arc that is tangential to the last path segment that goes to a point
TangentialArcTo {
base: BasePath,
/// the arc's center
center: Point2d,
/// arc's direction
ccw: bool,
},
/// A path that is horizontal.
Horizontal {
base: BasePath,
/// The x coordinate.
x: f64,
},
/// An angled line to.
AngledLineTo {
base: BasePath,
/// The x coordinate.
x: Option<f64>,
/// The y coordinate.
y: Option<f64>,
},
/// A base path.
Base { base: BasePath },
}
#[derive(Clone, Copy, ExecutionPlanValue)]
pub enum SketchSurface {
Plane(Plane),
}
/// A plane.
#[derive(Clone, Copy, ExecutionPlanValue)]
pub struct Plane {
/// The id of the plane.
pub id: Uuid,
// The code for the plane either a string or custom.
pub value: PlaneType,
/// Origin of the plane.
pub origin: Point3d,
pub axes: Axes,
}
/// Type for a plane.
#[derive(Clone, Copy, ExecutionPlanValue)]
pub enum PlaneType {
XY,
XZ,
YZ,
Custom,
}

View File

@ -1,5 +1,4 @@
use std::collections::HashMap; use std::{collections::HashMap, env};
use std::env;
use ep::{Destination, UnaryArithmetic}; use ep::{Destination, UnaryArithmetic};
use ept::{ListHeader, ObjectHeader}; use ept::{ListHeader, ObjectHeader};
@ -1049,15 +1048,30 @@ fn store_object_with_array_property() {
#[tokio::test] #[tokio::test]
async fn stdlib_cube_partial() { async fn stdlib_cube_partial() {
let program = r#" let program = r#"
let cube = startSketchAt([22.0, 33.0]) let cube = startSketchAt([0.0, 0.0])
|> lineTo([4.0, 0.0], %)
"#; "#;
let (plan, _scope) = must_plan(program); let (_plan, _scope) = must_plan(program);
std::fs::write("stdlib_cube_partial.json", serde_json::to_string_pretty(&plan).unwrap()).unwrap();
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program)) let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast() .ast()
.unwrap(); .unwrap();
let mem = crate::execute(ast, Some(test_client().await)).await.unwrap(); let client = test_client().await;
dbg!(mem); let _mem = crate::execute(ast, Some(client)).await.unwrap();
// use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat, ModelingCmd};
// let out = client
// .run_command(
// uuid::Uuid::new_v4().into(),
// each_cmd::TakeSnapshot {
// format: ImageFormat::Png,
// },
// )
// .await
// .unwrap();
// let out = match out {
// OkModelingCmdResponse::TakeSnapshot(b) => b,
// other => panic!("wrong output: {other:?}"),
// };
// let out: Vec<u8> = out.contents.into();
} }
async fn test_client() -> Session { async fn test_client() -> Session {

View File

@ -15,7 +15,7 @@ databake = "0.1.7"
kcl-lib = { path = "../kcl" } kcl-lib = { path = "../kcl" }
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2.0.49", features = ["full"] } syn = { version = "2.0.52", features = ["full"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -14,7 +14,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
anyhow = { version = "1.0.79", features = ["backtrace"] } anyhow = { version = "1.0.79", features = ["backtrace"] }
async-recursion = "1.0.5" async-recursion = "1.0.5"
async-trait = "0.1.77" async-trait = "0.1.77"
clap = { version = "4.5.0", features = ["cargo", "derive", "env", "unicode"], optional = true } clap = { version = "4.5.1", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3" dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] } databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.8" } derive-docs = { version = "0.1.8" }
@ -30,14 +30,14 @@ reqwest = { version = "0.11.24", default-features = false, features = ["stream",
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.114"
thiserror = "1.0.57" thiserror = "1.0.57"
ts-rs = { version = "7.1.1", features = ["uuid-impl"] } ts-rs = { version = "7.1.1", features = ["uuid-impl"] }
uuid = { version = "1.7.0", features = ["v4", "js", "serde"] } uuid = { version = "1.7.0", features = ["v4", "js", "serde"] }
winnow = "0.5.40" winnow = "0.5.40"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.68" } js-sys = { version = "0.3.69" }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.41" wasm-bindgen-futures = "0.4.41"

View File

@ -96,7 +96,19 @@ impl Program {
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) { let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
Some(noncodes) => noncodes Some(noncodes) => noncodes
.iter() .iter()
.map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation)) .enumerate()
.map(|(i, custom_white_space_or_comment)| {
let formatted = custom_white_space_or_comment.format(&indentation);
if i == 0 && !formatted.trim().is_empty() {
if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
format!("\n{}", formatted)
} else {
formatted
}
} else {
formatted
}
})
.collect::<String>(), .collect::<String>(),
None => String::new(), None => String::new(),
}; };
@ -159,6 +171,35 @@ impl Program {
} }
} }
/// Returns a non code meta that includes the given character position.
pub fn get_non_code_meta_for_position(&self, pos: usize) -> Option<&NonCodeMeta> {
// Check if its in the body.
if self.non_code_meta.contains(pos) {
return Some(&self.non_code_meta);
}
let Some(item) = self.get_body_item_for_position(pos) else {
return None;
};
// Recurse over the item.
let value = match item {
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_value_for_position(pos),
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument),
};
// Check if the value's non code meta contains the position.
if let Some(value) = value {
if let Some(non_code_meta) = value.get_non_code_meta() {
if non_code_meta.contains(pos) {
return Some(non_code_meta);
}
}
}
None
}
/// Returns all the lsp symbols in the program. /// Returns all the lsp symbols in the program.
pub fn get_lsp_symbols(&self, code: &str) -> Vec<DocumentSymbol> { pub fn get_lsp_symbols(&self, code: &str) -> Vec<DocumentSymbol> {
let mut symbols = vec![]; let mut symbols = vec![];
@ -431,6 +472,24 @@ impl Value {
} }
} }
// Get the non code meta for the value.
pub fn get_non_code_meta(&self) -> Option<&NonCodeMeta> {
match self {
Value::BinaryExpression(_bin_exp) => None,
Value::ArrayExpression(_array_exp) => None,
Value::ObjectExpression(_obj_exp) => None,
Value::MemberExpression(_mem_exp) => None,
Value::Literal(_literal) => None,
Value::FunctionExpression(_func_exp) => None,
Value::CallExpression(_call_exp) => None,
Value::Identifier(_ident) => None,
Value::PipeExpression(pipe_exp) => Some(&pipe_exp.non_code_meta),
Value::UnaryExpression(_unary_exp) => None,
Value::PipeSubstitution(_pipe_substitution) => None,
Value::None(_none) => None,
}
}
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Value) { pub fn replace_value(&mut self, source_range: SourceRange, new_value: Value) {
if source_range == self.clone().into() { if source_range == self.clone().into() {
*self = new_value; *self = new_value;
@ -736,6 +795,10 @@ pub struct NonCodeNode {
} }
impl NonCodeNode { impl NonCodeNode {
pub fn contains(&self, pos: usize) -> bool {
self.start <= pos && pos <= self.end
}
pub fn value(&self) -> String { pub fn value(&self) -> String {
match &self.value { match &self.value {
NonCodeValue::InlineComment { value, style: _ } => value.clone(), NonCodeValue::InlineComment { value, style: _ } => value.clone(),
@ -755,18 +818,27 @@ impl NonCodeNode {
value, value,
style: CommentStyle::Block, style: CommentStyle::Block,
} => format!(" /* {} */", value), } => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => { NonCodeValue::BlockComment { value, style } => match style {
let add_start_new_line = if self.start == 0 { "" } else { "\n" }; CommentStyle::Block => format!("{}/* {} */", indentation, value),
match style { CommentStyle::Line => {
CommentStyle::Block => format!("{}{}/* {} */", add_start_new_line, indentation, value), if value.trim().is_empty() {
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value), format!("{}//\n", indentation)
} else {
format!("{}// {}\n", indentation, value.trim())
}
} }
} },
NonCodeValue::NewLineBlockComment { value, style } => { NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" }; let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
match style { match style {
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value), CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value), CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}{}//\n", add_start_new_line, indentation)
} else {
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
}
}
} }
} }
NonCodeValue::NewLine => "\n\n".to_string(), NonCodeValue::NewLine => "\n\n".to_string(),
@ -863,6 +935,16 @@ impl NonCodeMeta {
pub fn insert(&mut self, i: usize, new: NonCodeNode) { pub fn insert(&mut self, i: usize, new: NonCodeNode) {
self.non_code_nodes.entry(i).or_default().push(new); self.non_code_nodes.entry(i).or_default().push(new);
} }
pub fn contains(&self, pos: usize) -> bool {
if self.start.iter().any(|node| node.contains(pos)) {
return true;
}
self.non_code_nodes
.iter()
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
}
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
@ -1009,7 +1091,7 @@ impl CallExpression {
function_expression.params.len(), function_expression.params.len(),
fn_args.len(), fn_args.len(),
), ),
source_ranges: vec![(function_expression).into()], source_ranges: vec![self.into()],
})); }));
} }
@ -2533,7 +2615,13 @@ impl PipeExpression {
let non_code_meta = self.non_code_meta.clone(); let non_code_meta = self.non_code_meta.clone();
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) { if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
for val in non_code_meta_value { for val in non_code_meta_value {
s += val.format(&indentation).trim_end_matches('\n') let formatted = val.format(&indentation).trim_end_matches('\n').to_string();
if let NonCodeValue::BlockComment { .. } = val.value {
s += "\n";
s += &formatted;
} else {
s += &formatted;
}
} }
} }
@ -3003,8 +3091,7 @@ let baz = {a: 1, b: "thing"}
fn ghi = (x) => { fn ghi = (x) => {
return x return x
} }
"#;
show(part001)"#;
let tokens = crate::token::lexer(code); let tokens = crate::token::lexer(code);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
@ -3106,6 +3193,109 @@ show(part001)"#;
"# "#
); );
} }
#[test]
fn test_recast_comment_under_variable() {
let some_program_string = r#"const key = 'c'
// this is also a comment
const thing = 'foo'
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"const key = 'c'
// this is also a comment
const thing = 'foo'
"#
);
}
#[test]
fn test_recast_multiline_comment_start_file() {
let some_program_string = r#"// hello world
// I am a comment
const key = 'c'
// this is also a comment
// hello
const thing = 'foo'
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// hello world
// I am a comment
const key = 'c'
// this is also a comment
// hello
const thing = 'foo'
"#
);
}
#[test]
fn test_recast_empty_comment() {
let some_program_string = r#"// hello world
//
// I am a comment
const key = 'c'
//
// I am a comment
const thing = 'c'
const foo = 'bar' //
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// hello world
//
// I am a comment
const key = 'c'
//
// I am a comment
const thing = 'c'
const foo = 'bar' //
"#
);
}
#[test]
fn test_recast_multiline_comment_under_variable() {
let some_program_string = r#"const key = 'c'
// this is also a comment
// hello
const thing = 'foo'
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"const key = 'c'
// this is also a comment
// hello
const thing = 'foo'
"#
);
}
#[test] #[test]
fn test_recast_comment_at_start() { fn test_recast_comment_at_start() {
let test_program = r#" let test_program = r#"
@ -3181,9 +3371,7 @@ const mySk1 = startSketchOn('XY')
offset: -1.35, offset: -1.35,
intersectTag: 'seg01' intersectTag: 'seg01'
}, %) }, %)
|> line([-0.42, -1.72], %) |> line([-0.42, -1.72], %)"#;
show(part001)"#;
let tokens = crate::token::lexer(some_program_string); let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
@ -3332,8 +3520,7 @@ let baz = {a: 1, part001: "thing"}
fn ghi = (part001) => { fn ghi = (part001) => {
return part001 return part001
} }
"#;
show(part001)"#;
let tokens = crate::token::lexer(some_program_string); let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let mut program = parser.ast().unwrap(); let mut program = parser.ast().unwrap();
@ -3355,8 +3542,6 @@ let baz = { a: 1, part001: "thing" }
fn ghi = (part001) => { fn ghi = (part001) => {
return part001 return part001
} }
show(mySuperCoolPart)
"# "#
); );
} }
@ -3381,6 +3566,97 @@ show(mySuperCoolPart)
); );
} }
#[test]
fn test_recast_trailing_comma() {
let some_program_string = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> arc({
radius: 1,
angle_start: 0,
angle_end: 180,
}, %)"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> arc({
radius: 1,
angle_start: 0,
angle_end: 180
}, %)
"#
);
}
#[test]
fn test_ast_get_non_code_node() {
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
const h = 30
// st
const cylinder = startSketchOn('-XZ')
|> startProfileAt([50, 0], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: r
}, %)
|> extrude(h, %)
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let value = program.get_non_code_meta_for_position(50);
assert!(value.is_some());
}
#[test]
fn test_ast_get_non_code_node_pipe() {
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
const h = 30
// st
const cylinder = startSketchOn('-XZ')
|> startProfileAt([50, 0], %)
// comment
|> arc({
angle_end: 360,
angle_start: 0,
radius: r
}, %)
|> extrude(h, %)
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let value = program.get_non_code_meta_for_position(124);
assert!(value.is_some());
}
#[test]
fn test_ast_get_non_code_node_inline_comment() {
let some_program_string = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> xLine(5, %) // lin
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let value = program.get_non_code_meta_for_position(86);
assert!(value.is_some());
}
#[test] #[test]
fn test_recast_negative_var() { fn test_recast_negative_var() {
let some_program_string = r#"const w = 20 let some_program_string = r#"const w = 20
@ -3394,8 +3670,7 @@ const firstExtrude = startSketchOn('XY')
|> line([0, -l], %) |> line([0, -l], %)
|> close(%) |> close(%)
|> extrude(h, %) |> extrude(h, %)
"#;
show(firstExtrude)"#;
let tokens = crate::token::lexer(some_program_string); let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
@ -3414,8 +3689,48 @@ const firstExtrude = startSketchOn('XY')
|> line([0, -l], %) |> line([0, -l], %)
|> close(%) |> close(%)
|> extrude(h, %) |> extrude(h, %)
"#
);
}
show(firstExtrude) #[test]
fn test_recast_multiline_comment() {
let some_program_string = r#"const w = 20
const l = 8
const h = 10
// This is my comment
// It has multiple lines
// And it's really long
const firstExtrude = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
"#;
let tokens = crate::token::lexer(some_program_string);
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"const w = 20
const l = 8
const h = 10
// This is my comment
// It has multiple lines
// And it's really long
const firstExtrude = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
"# "#
); );
} }

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