Compare commits

..

93 Commits

Author SHA1 Message Date
68957ad7d8 try and reduce loading timeout noise 2024-03-05 16:13:26 +11: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
47d40eb801 Update test artifacts for patterns with holes (#1566)
* update test artifacts

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

* update known issues

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

* screenshots

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-28 19:18:23 -08:00
adc4b6148d Cut release v0.15.3 (#1546) 2024-02-29 12:39:20 +11:00
27d0d4a28b bump kittcad/lib version (#1565) 2024-02-29 11:57:47 +11:00
fb609c19ef Grackle: implement StartSketchAt stdlib function (#1535)
* Grackle: implement StartSketchAt stdlib function

* Move startsketchAt into a new 'sketch' module

* Further divide module

* Write SketchGroup to EP memory
2024-02-28 16:24:03 -06:00
8666989c85 add issue template (#1547)
* add issue template

* change report a bug link

* add issue form and delete issue template, update link
2024-02-27 14:10:50 -08:00
bdf49c2084 short term cam fix (#1543)
* short term cam fix

* fix

* 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-02-28 06:26:53 +11:00
a06b9d560a improve export test logs (#1536) 2024-02-27 15:19:14 +11:00
b81ff66f2b Sketch on face of face (#1524)
* add test

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

* add negative extrude

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

* fix sketch on face of face

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

* generate stdlib

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-26 14:54:42 -08:00
c0e6947170 update cli to reestablish export test (#1523)
* update cli to reestablish export test

* update cli v

* tweak
2024-02-27 09:41:09 +11:00
65ebde0b34 Disable actions when stream disconnected (#1483)
* pull out network indicator logic

* rename callbacks

* re-execute on reconnection

* make sure tool bar is disabled on start up

* clean up

* node safety

* disable toolbar buttons properly

* grey scale action icon icons dodgy

* test tweaks

* Revert "grey scale action icon icons dodgy"

This reverts commit c3d12a0f05.

* Disable modeling commands when network is bad (#1486)

* Disable modeling commands when network is bad

* disabel on execute too

* fmt

---------

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

* disable playwright snapshots temporarily

* disable export test instead

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

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-26 21:02:33 +11:00
0d6618b60a solve a couple of scene scale bugs (#1496)
* solve a couple of scene scale bugs

* Some cam fixes (#1520)

* rotate and zoom basics working

* intergrate mouse guards, and add pan

* implement orthographic camera again

* implement switch to perspective camera again

* migrate dollyzoom

* make pan robust for differnt FOV and orthographic cam

* tween to quaternion and default plane selection working with quirks

* fix pan

It the up and right was derived from the camera's up, which is a static [0,0,1] not the camera's current cameras real up, which aligns itself as best to [0,0,1] but is not that especially when looking straight up or down, and the pan felt very awkward in these vertical look sintuations

* fix raycastRing to use new camera

* fix tween to quaternion for camera lock situations

And get all playwright tests passing

* fix up CamToggle, even thought this component is not setup properly to use react properties from our scene class

* add animation to cameras back in

* first big clean up of sceneInfra

* move more cam stuff out of sceneInfra

* clean up mouse guard logic

* clean up camera change callbacks

* fix some sitations where animation to xy doesn't work great

* needs to take the target into consideration

* last bits of clean up

* more clean up

* make vitest happ

* fix up remaining interaction guards

* make scrolling less sensative for trackpads

* remove debug cube

* fix snapshot tests
2024-02-26 19:53:44 +11:00
f0c44d11b3 Revert "tauri-driver latest"
This reverts commit 44e71cd4bc.
2024-02-25 20:35:11 -05:00
44e71cd4bc tauri-driver latest 2024-02-25 20:33:14 -05:00
a9f716dad8 fixing discord automation to ignore nightly runs (#1516)
adding release if statement
2024-02-24 01:44:25 -07:00
a2455832e7 Honor mod+z and mod+shift+z even with editor not in focus (#1513)
Resolves #1504 and was way easier than I thought when I tried it a while back!
2024-02-23 17:37:05 -05:00
8f5034f997 Remove Sentry from production (#1515) 2024-02-23 22:07:33 +00:00
af1c2c7ae1 Update KNOWN-ISSUES.md 2024-02-23 13:19:03 -08:00
ff38ae091e Replace number command bar arg input type with kcl expression input (#1474)
* Rename useCalc

* Move CommandBar so it has access to settings and kcl

* Create codemirror variable mention extension

* Make project path a dep of TextEditor useMemo

* Add incomplete KCL input for CommandBar
to replace current number arg type

* Add previous variables autocompletion to kcl input

* Fix missed typos from merge

* Working AST mods, not working variable additions

* Add ability to create a new variable

* Add icon and tooltip to command arg tag if a variable is added

* Polish variable naming logic, preserve when going back

* Allow stepping back from KCL input

* Don't prevent keydown of enter, it's used by autocomplete

* Round the variable value in cmd bar header

* Add Playwright test

* Formatting, polish TS types

* More type wrangling

* Needed to fmt after above type wrangling

* Update snapshot tests to account for new variable name autogeneration

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

* Merge branch 'main' into cmd-bar-make-variable

* Update all test instances of var name with number index after merge with main

* Partial revert of "Polish variable naming logic, preserve when going back"

This reverts commit dddcb13c36.

* Revert "Update all test instances of var name with number index after merge with main"

This reverts commit 8c4b63b523.

* Revert "Update snapshot tests to account for new variable name autogeneration"

This reverts commit 11bfce3832.

* Retry a refactoring of findUniqueName

* minor feedback from @jgomez720
- better highlighting of kcl input
- consistent hotkeys
- disallow invalid var names

* Polish stepping back state logic

* Fix tests now that keyboard shortcut changed

* Remove unused imports

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

* Fix tests

* Trigger CI

* Update src/components/ProjectSidebarMenu.test.tsx

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

* re-trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-02-23 11:24:22 -05:00
1dd7c95b8c Sketch on arc error (#1495)
* add error for sketch on arc

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

* fixes

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

* generate std lib

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-22 19:07:17 -08:00
20042ec87c Release derive-docs 0.1.7 (#1491)
Release derive-docs 0.1.7
2024-02-22 21:24:02 +00:00
fccf3508a7 Trim space off the return type before continuing (#1487)
* Trim space off the return type before continuing

My nightly compiler has a space at the end that the stable compiler
doesn't. This will trim space, if it exists, before removing the generic
bracket, which will work for both stable and current nightly, future
stable.

In the future this may be worth doing a trim on "> " but I don't reckon
today is that day.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2024-02-22 15:59:51 -05:00
8dab5527b8 bump ahash to fix the nightly builds (#1488)
Specifically, i'm hitting https://github.com/tkaitchuck/aHash/issues/200
ahash FTBFS because of https://github.com/rust-lang/rust/pull/117372
which is fixed in 0.8.7.

This rolls us forward which should fix future builds.
2024-02-22 15:30:03 -05:00
f72eb0e8a7 Cube example didn't actually work (#1478)
* Cube example didn't actually work

* Bump h2 in fuzz tests
2024-02-22 12:03:05 -06:00
40479d177f Bump ip from 1.1.8 to 1.1.9 (#1471)
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  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: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-02-22 04:59:03 -05:00
b88359dee2 Move discord automation into ci.yml (#1479)
* updating ci.yml with discord release automation

* removed line
2024-02-21 17:35:35 -07:00
f4c0347104 Cut release v0.15.2 (#1467)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-02-22 08:36:38 +11:00
ad36b5f5fa add make release bash script (#1475)
* add make release bash script

* read me details

* uncomment git uncommited changes

* typo

* tweaks

* use package.json as source of truth, not git tags
2024-02-22 07:01:45 +11:00
b798cf19d3 Bump kcl-lib (#1477)
* bump version

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

* bump version

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

* Revert "bump version"

This reverts commit 44e0b6ac6e.

* bump version

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-21 11:05:10 -08:00
7cfa897561 add modulo and power operators (#1341)
* add modulo and power operators

* format

* point to main instead of serena branch

* reset cargo lock

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

---------

Co-authored-by: gserena <serena@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-21 11:11:57 -05:00
0d8804005a improve vitest hang (#1470)
* improve vitest hang

* fmt

* make types happy

* fix types

* fix
2024-02-21 13:23:50 +11:00
cbd26d29fa updates for units (#1458)
* updates for units

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

* scene units

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

* start passing in units to tests

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

* units tests

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

* add more images

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

* hacky code for now

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

* get settings context outside of react

* fmt

* pull through settings

* fix

* fmt

* move camera with units (#1461)

* temp patch tsc

* update kittycad.rs

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

* trait

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

* fix compile

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

* update screenshots

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

* execute on settings change

* Update src/clientSideScene/sceneInfra.ts

* try zoom

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

* more shit image

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

* new screenshots

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

* udpates

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

* updates

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

* tests

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

* update cam

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

* new

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

* updates for units

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

* fixles

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

* ;scale

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

* fixes

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

* fmt

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

* tweak playwright draft segments test

* another test tweak

* last test tweak

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

* update default plane snapshot scale

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

* number tweaks for playwright flow checks

* up[date

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-20 17:55:06 -08:00
e501a542ac Add arc icon, replace settings icon (#1469)
* Add icons for arc and settings

* Update arc icon in toolbar

* Use settings icon instead of gear for settings

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-20 18:34:03 +00:00
7cb4f4d101 deselect line bug (#1457) 2024-02-20 11:04:42 +11:00
1162f5f4c4 Bump kcl-lib (#1455)
* update version

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

* update version

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

* fix

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-02-19 14:23:16 -08:00
3975e6d8f5 auto complete test more robust (#1456) 2024-02-20 09:22:33 +11:00
d68d7a7e00 Cut release v0.15.1 (#1452)
cut release v0.15.1
2024-02-20 08:10:26 +11:00
b135b97de6 Code mirror plugin lsp interface (#1444)
* better named dirs

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

* move some stuff around

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

* more logging

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

* updates

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

* less logging

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

* add fs in

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

* updates

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

* file reader

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

* workspace

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

* start of workspace folders

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

* start of workspace folders

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

* cleanup workspace folders

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

* cleanup logs

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-19 12:33:16 -08:00
de5885ce0b Enable/disable "start sketch", "edit sketch" and "extrude" appropriately (#1449)
* test that fails for when to enable extrude and sketch features

* add fix to make test pass

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-19 17:23:03 +11:00
ad7c544754 draft line snapshots (#1445)
* draft line snapshots

Make sure they don't get broken at some point, visual regression is only way to test these really

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-19 13:18:31 +11:00
4d77875bdc mouse listners should be reset outside of sketch (#1442)
* mouse listners should be reset outside of sketch (only orbit controls are needed) and also check mouse button

* tweak
2024-02-19 12:41:36 +11:00
3377923dcb fix flacky auto complete test (#1443) 2024-02-19 12:15:57 +11:00
c6005660c8 jsxify svgs (#1441) 2024-02-19 10:20:02 +11:00
66e62c6037 cancel execution on file change (#1440) 2024-02-19 09:23:18 +11:00
0a4a517bb4 try arm latest (#1439) 2024-02-17 22:12:39 -08:00
240 changed files with 15539 additions and 5876 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_SKIP_AUTH=false
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_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

85
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: Bug Report
description: File a bug report for the Zoo Modeling App
title: "[BUG]: "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us resolve it."
- type: textarea
id: describe-bug
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: "Explain the bug..."
validations:
required: true
- type: textarea
id: reproduce-bug
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Description of what you expected to happen.
placeholder: "I expected that..."
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots and Recordings
description: If applicable, add screenshots to help explain your problem. Maximum upload size is 10MB.
placeholder: "You can attach images or video recordings here."
validations:
required: false
- type: input
id: desktop-os
attributes:
label: Desktop OS
description: "Your operating system"
placeholder: "example: Windows 10, MacOS Big Sur"
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: "If you are using the web version, please specify the browser you are using."
placeholder: "example: Chrome, Safari"
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: "The version of the Zoo Modeling App you're using."
placeholder: "example: v0.15.0. You can find this in the settings."
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
placeholder: "Anything else you want to add..."
validations:
required: false

View File

@ -1,29 +0,0 @@
name: Announce Release
on:
release:
types: [published]
jobs:
announce_release:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

View File

@ -40,6 +40,20 @@ jobs:
run: |
sudo apt-get update
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@nextest
- name: Rust Cache
@ -48,7 +62,7 @@ jobs:
shell: bash
run: |-
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:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000

View File

@ -86,8 +86,6 @@ jobs:
- run: yarn test:nowatch
- run: yarn test:cov
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
@ -338,7 +336,7 @@ jobs:
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.1'
uses: 'google-github-actions/auth@v2.1.2'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
@ -372,3 +370,28 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

View File

@ -4,6 +4,11 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
playwright-ubuntu:
timeout-minutes: 60
@ -14,7 +19,7 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@v0.2.16
- uses: KittyCAD/action-install-cli@v0.2.21
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
@ -79,8 +84,7 @@ jobs:
playwright-macos:
timeout-minutes: 60
runs-on: macos-latest
needs: playwright-ubuntu
runs-on: macos-14
steps:
- uses: actions/checkout@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"

3
.gitignore vendored
View File

@ -33,6 +33,7 @@ src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info
src/wasm-lib/grackle/test_json_output
e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png
@ -54,3 +55,5 @@ e2e/playwright/export-snapshots/*embedded.gltf
## generated files
src/**/*.typegen.ts
src/wasm-lib/grackle/stdlib_cube_partial.json

View File

@ -136,6 +136,11 @@ Before you submit a contribution PR to this repo, please ensure that:
VERSION=x.y.z yarn run bump-jsons
```
Alternatively you can try the experimental `make-release.sh` bash script that will create the branch with the updated json files for you.
run `./make-release.sh` for a patch update
run `./make-release.sh "minor"` for minor
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
```typescript

View File

@ -6,11 +6,9 @@ once fixed in engine will just start working here with no language changes.
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
are sketching) you will get multiple models returned instead of one single
model for that sketch and its underlying 3D object.
If you see a red line around your model, it means this is happening.
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
item in the pattern is being subtracted. This is an engine bug that is being
worked on.
- **Import**: Right now you can import a file, even if that file has brep data
you cannot edit it. You also cannot move or transform the imported objects at
all. In the future, after v1, the engine will account for this.
you cannot edit it, after v1, the engine will account for this. You also cannot
currently move or transform the imported objects at all, once we have assemblies
this will work.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 193 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

@ -19,59 +19,59 @@ DATA;
);
#4 = CARTESIAN_POINT('NONE', (0, 0, -0));
#5 = VERTEX_POINT('NONE', #4);
#6 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#6 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
#7 = VERTEX_POINT('NONE', #6);
#8 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
#8 = CARTESIAN_POINT('NONE', (0, -0.64516, 2.58064));
#9 = VERTEX_POINT('NONE', #8);
#10 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#10 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
#11 = VERTEX_POINT('NONE', #10);
#12 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#12 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
#13 = VERTEX_POINT('NONE', #12);
#14 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
#14 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, 2.58064));
#15 = VERTEX_POINT('NONE', #14);
#16 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#16 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
#17 = VERTEX_POINT('NONE', #16);
#18 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
#18 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, 2.58064));
#19 = VERTEX_POINT('NONE', #18);
#20 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#20 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
#21 = VERTEX_POINT('NONE', #20);
#22 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
#22 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, 2.58064));
#23 = VERTEX_POINT('NONE', #22);
#24 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#24 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
#25 = VERTEX_POINT('NONE', #24);
#26 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
#26 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, 2.58064));
#27 = VERTEX_POINT('NONE', #26);
#28 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#28 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
#29 = VERTEX_POINT('NONE', #28);
#30 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
#30 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, 2.58064));
#31 = VERTEX_POINT('NONE', #30);
#32 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#32 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
#33 = VERTEX_POINT('NONE', #32);
#34 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
#34 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, 2.58064));
#35 = VERTEX_POINT('NONE', #34);
#36 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#36 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
#37 = VERTEX_POINT('NONE', #36);
#38 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
#38 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, 2.58064));
#39 = VERTEX_POINT('NONE', #38);
#40 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#40 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
#41 = VERTEX_POINT('NONE', #40);
#42 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
#42 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, 2.58064));
#43 = VERTEX_POINT('NONE', #42);
#44 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#44 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
#45 = VERTEX_POINT('NONE', #44);
#46 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
#46 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, 2.58064));
#47 = VERTEX_POINT('NONE', #46);
#48 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#48 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
#49 = VERTEX_POINT('NONE', #48);
#50 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
#50 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, 2.58064));
#51 = VERTEX_POINT('NONE', #50);
#52 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#52 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
#53 = VERTEX_POINT('NONE', #52);
#54 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
#54 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, 2.58064));
#55 = VERTEX_POINT('NONE', #54);
#56 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#56 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
#57 = VERTEX_POINT('NONE', #56);
#58 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#58 = CARTESIAN_POINT('NONE', (0, 0.64516, 2.58064));
#59 = VERTEX_POINT('NONE', #58);
#60 = DIRECTION('NONE', (0, -1, 0));
#61 = VECTOR('NONE', #60, 1);
@ -79,11 +79,11 @@ DATA;
#63 = LINE('NONE', #62, #61);
#64 = DIRECTION('NONE', (0, 0, 1));
#65 = VECTOR('NONE', #64, 1);
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#66 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
#67 = LINE('NONE', #66, #65);
#68 = DIRECTION('NONE', (0, -1, 0));
#69 = VECTOR('NONE', #68, 1);
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#70 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
#71 = LINE('NONE', #70, #69);
#72 = DIRECTION('NONE', (0, 0, 1));
#73 = VECTOR('NONE', #72, 1);
@ -91,155 +91,155 @@ DATA;
#75 = LINE('NONE', #74, #73);
#76 = DIRECTION('NONE', (1, 0, 0));
#77 = VECTOR('NONE', #76, 1);
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#78 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
#79 = LINE('NONE', #78, #77);
#80 = DIRECTION('NONE', (0, 0, 1));
#81 = VECTOR('NONE', #80, 1);
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#82 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
#83 = LINE('NONE', #82, #81);
#84 = DIRECTION('NONE', (1, 0, 0));
#85 = VECTOR('NONE', #84, 1);
#86 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
#86 = CARTESIAN_POINT('NONE', (0, -0.64516, 2.58064));
#87 = LINE('NONE', #86, #85);
#88 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#88 = DIRECTION('NONE', (0.819152044288992, -0.5735764363510459, 0));
#89 = VECTOR('NONE', #88, 1);
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#90 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
#91 = LINE('NONE', #90, #89);
#92 = DIRECTION('NONE', (0, 0, 1));
#93 = VECTOR('NONE', #92, 1);
#94 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#94 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
#95 = LINE('NONE', #94, #93);
#96 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#96 = DIRECTION('NONE', (0.819152044288992, -0.5735764363510459, 0));
#97 = VECTOR('NONE', #96, 1);
#98 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
#98 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, 2.58064));
#99 = LINE('NONE', #98, #97);
#100 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#100 = DIRECTION('NONE', (1, -0.00000000000000038794063361359933, 0));
#101 = VECTOR('NONE', #100, 1);
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#102 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
#103 = LINE('NONE', #102, #101);
#104 = DIRECTION('NONE', (0, 0, 1));
#105 = VECTOR('NONE', #104, 1);
#106 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#106 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
#107 = LINE('NONE', #106, #105);
#108 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#108 = DIRECTION('NONE', (1, -0.00000000000000038794063361359933, 0));
#109 = VECTOR('NONE', #108, 1);
#110 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
#110 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, 2.58064));
#111 = LINE('NONE', #110, #109);
#112 = DIRECTION('NONE', (0, 1, 0));
#113 = VECTOR('NONE', #112, 1);
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#114 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
#115 = LINE('NONE', #114, #113);
#116 = DIRECTION('NONE', (0, 0, 1));
#117 = VECTOR('NONE', #116, 1);
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#118 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
#119 = LINE('NONE', #118, #117);
#120 = DIRECTION('NONE', (0, 1, 0));
#121 = VECTOR('NONE', #120, 1);
#122 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
#122 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, 2.58064));
#123 = LINE('NONE', #122, #121);
#124 = DIRECTION('NONE', (-1, 0, 0));
#125 = VECTOR('NONE', #124, 1);
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#126 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
#127 = LINE('NONE', #126, #125);
#128 = DIRECTION('NONE', (0, 0, 1));
#129 = VECTOR('NONE', #128, 1);
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#130 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
#131 = LINE('NONE', #130, #129);
#132 = DIRECTION('NONE', (-1, 0, 0));
#133 = VECTOR('NONE', #132, 1);
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
#134 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, 2.58064));
#135 = LINE('NONE', #134, #133);
#136 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
#137 = VECTOR('NONE', #136, 1);
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#138 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
#139 = LINE('NONE', #138, #137);
#140 = DIRECTION('NONE', (0, 0, 1));
#141 = VECTOR('NONE', #140, 1);
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#142 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
#143 = LINE('NONE', #142, #141);
#144 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
#145 = VECTOR('NONE', #144, 1);
#146 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
#146 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, 2.58064));
#147 = LINE('NONE', #146, #145);
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406992, 0));
#149 = VECTOR('NONE', #148, 1);
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#150 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
#151 = LINE('NONE', #150, #149);
#152 = DIRECTION('NONE', (0, 0, 1));
#153 = VECTOR('NONE', #152, 1);
#154 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#154 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
#155 = LINE('NONE', #154, #153);
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406992, 0));
#157 = VECTOR('NONE', #156, 1);
#158 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
#158 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, 2.58064));
#159 = LINE('NONE', #158, #157);
#160 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#160 = DIRECTION('NONE', (1, -0.0000000000000001378647737807002, 0));
#161 = VECTOR('NONE', #160, 1);
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#162 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
#163 = LINE('NONE', #162, #161);
#164 = DIRECTION('NONE', (0, 0, 1));
#165 = VECTOR('NONE', #164, 1);
#166 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#166 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
#167 = LINE('NONE', #166, #165);
#168 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#168 = DIRECTION('NONE', (1, -0.0000000000000001378647737807002, 0));
#169 = VECTOR('NONE', #168, 1);
#170 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
#170 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, 2.58064));
#171 = LINE('NONE', #170, #169);
#172 = DIRECTION('NONE', (0, 1, 0));
#173 = VECTOR('NONE', #172, 1);
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#174 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
#175 = LINE('NONE', #174, #173);
#176 = DIRECTION('NONE', (0, 0, 1));
#177 = VECTOR('NONE', #176, 1);
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#178 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
#179 = LINE('NONE', #178, #177);
#180 = DIRECTION('NONE', (0, 1, 0));
#181 = VECTOR('NONE', #180, 1);
#182 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
#182 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, 2.58064));
#183 = LINE('NONE', #182, #181);
#184 = DIRECTION('NONE', (-1, 0, 0));
#185 = VECTOR('NONE', #184, 1);
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#186 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
#187 = LINE('NONE', #186, #185);
#188 = DIRECTION('NONE', (0, 0, 1));
#189 = VECTOR('NONE', #188, 1);
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#190 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
#191 = LINE('NONE', #190, #189);
#192 = DIRECTION('NONE', (-1, 0, 0));
#193 = VECTOR('NONE', #192, 1);
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
#194 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, 2.58064));
#195 = LINE('NONE', #194, #193);
#196 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
#196 = DIRECTION('NONE', (-0.90630778703665, -0.4226182617406995, 0));
#197 = VECTOR('NONE', #196, 1);
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#198 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
#199 = LINE('NONE', #198, #197);
#200 = DIRECTION('NONE', (0, 0, 1));
#201 = VECTOR('NONE', #200, 1);
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#202 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
#203 = LINE('NONE', #202, #201);
#204 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
#204 = DIRECTION('NONE', (-0.90630778703665, -0.4226182617406995, 0));
#205 = VECTOR('NONE', #204, 1);
#206 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
#206 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, 2.58064));
#207 = LINE('NONE', #206, #205);
#208 = DIRECTION('NONE', (-1, 0, 0));
#209 = VECTOR('NONE', #208, 1);
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#210 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
#211 = LINE('NONE', #210, #209);
#212 = DIRECTION('NONE', (0, 0, 1));
#213 = VECTOR('NONE', #212, 1);
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#214 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
#215 = LINE('NONE', #214, #213);
#216 = DIRECTION('NONE', (-1, 0, 0));
#217 = VECTOR('NONE', #216, 1);
#218 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
#218 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, 2.58064));
#219 = LINE('NONE', #218, #217);
#220 = DIRECTION('NONE', (0, -1, 0));
#221 = VECTOR('NONE', #220, 1);
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#222 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
#223 = LINE('NONE', #222, #221);
#224 = DIRECTION('NONE', (0, -1, 0));
#225 = VECTOR('NONE', #224, 1);
#226 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#226 = CARTESIAN_POINT('NONE', (0, 0.64516, 2.58064));
#227 = LINE('NONE', #226, #225);
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);
#229 = EDGE_CURVE('NONE', #7, #9, #67, .T.);
@ -383,59 +383,59 @@ DATA;
#367 = ORIENTED_EDGE('NONE', *, *, #267, .T.);
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
#369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368));
#370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508));
#370 = CARTESIAN_POINT('NONE', (0, -0.3225799999999985, 1.2903199999999995));
#371 = DIRECTION('NONE', (-1, -0, 0));
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
#373 = PLANE('NONE', #372);
#374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508));
#374 = CARTESIAN_POINT('NONE', (0.9983910612778368, -0.6451599999999998, 1.2903199999999997));
#375 = DIRECTION('NONE', (0, -1, 0));
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
#377 = PLANE('NONE', #376);
#378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999));
#378 = CARTESIAN_POINT('NONE', (2.918166090585415, -1.2903199999999988, 1.2903199999999997));
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.8191520442889919, 0));
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
#381 = PLANE('NONE', #380);
#382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508));
#382 = CARTESIAN_POINT('NONE', (4.984285029307579, -1.9354799999999992, 1.2903199999999997));
#383 = DIRECTION('NONE', (0, -1, 0));
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
#385 = PLANE('NONE', #384);
#386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508));
#386 = CARTESIAN_POINT('NONE', (6.129019999999999, -1.7741899999999997, 1.2903199999999997));
#387 = DIRECTION('NONE', (1, -0, 0));
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
#389 = PLANE('NONE', #388);
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
#390 = CARTESIAN_POINT('NONE', (5.035139570965871, -1.6128999999999998, 1.2903199999999997));
#391 = DIRECTION('NONE', (0, 1, -0));
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
#393 = PLANE('NONE', #392);
#394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508));
#395 = DIRECTION('NONE', (0.5735764363510459, 0.8191520442889917, -0));
#394 = CARTESIAN_POINT('NONE', (2.7895291818945633, -0.8064499999999998, 1.2903199999999995));
#395 = DIRECTION('NONE', (0.5735764363510459, 0.8191520442889918, -0));
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
#397 = PLANE('NONE', #396);
#398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508));
#399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, 0));
#398 = CARTESIAN_POINT('NONE', (2.6754617854843468, 0.4838700000000003, 1.2903199999999997));
#399 = DIRECTION('NONE', (0.4226182617406992, -0.90630778703665, 0));
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
#401 = PLANE('NONE', #400);
#402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508));
#402 = CARTESIAN_POINT('NONE', (4.921072174555653, 0.9677399999999998, 1.2903199999999995));
#403 = DIRECTION('NONE', (0, -1, 0));
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
#405 = PLANE('NONE', #404);
#406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508));
#406 = CARTESIAN_POINT('NONE', (6.129019999999998, 1.1290299999999989, 1.2903199999999995));
#407 = DIRECTION('NONE', (1, -0, 0));
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
#409 = PLANE('NONE', #408);
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
#410 = CARTESIAN_POINT('NONE', (4.8853150424179725, 1.2903199999999997, 1.2903199999999997));
#411 = DIRECTION('NONE', (0, 1, -0));
#412 = AXIS2_PLACEMENT_3D('NONE', #410, #411, $);
#413 = PLANE('NONE', #412);
#414 = CARTESIAN_POINT('NONE', (0.11613523789047137, 0.0381, 0.05079999999999999));
#415 = DIRECTION('NONE', (-0.42261826174069966, 0.90630778703665, -0));
#414 = CARTESIAN_POINT('NONE', (2.9498350424179733, 0.9677399999999998, 1.2903199999999997));
#415 = DIRECTION('NONE', (-0.42261826174069933, 0.9063077870366499, -0));
#416 = AXIS2_PLACEMENT_3D('NONE', #414, #415, $);
#417 = PLANE('NONE', #416);
#418 = CARTESIAN_POINT('NONE', (0.044449999999999996, 0.0254, 0.0508));
#418 = CARTESIAN_POINT('NONE', (1.1290299999999998, 0.6451599999999998, 1.29032));
#419 = DIRECTION('NONE', (0, 1, -0));
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
#421 = PLANE('NONE', #420);
#422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508));
#422 = CARTESIAN_POINT('NONE', (0, 0.32257999999999987, 1.2903199999999995));
#423 = DIRECTION('NONE', (-1, -0, 0));
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
#425 = PLANE('NONE', #424);
@ -443,7 +443,7 @@ DATA;
#427 = DIRECTION('NONE', (0, 0, 1));
#428 = AXIS2_PLACEMENT_3D('NONE', #426, #427, $);
#429 = PLANE('NONE', #428);
#430 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#430 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
#431 = DIRECTION('NONE', (0, 0, 1));
#432 = AXIS2_PLACEMENT_3D('NONE', #430, #431, $);
#433 = PLANE('NONE', #432);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -1,478 +1,478 @@
solid unnamed
facet normal -1 0 0
outer loop
vertex 0 -4 0
vertex 0 -101.600006 0
vertex 0 -0 0
vertex 0 -4 -1
vertex 0 -101.600006 -25.400002
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 -1
vertex 0 -101.600006 -25.400002
vertex 0 -0 0
vertex 0 -0 -1
vertex 0 -0 -25.400002
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 0 -4 -1
vertex 0 -0 -1
vertex 3.0950184 -4 -1
vertex 0 -101.600006 -25.400002
vertex 0 -0 -25.400002
vertex 78.613464 -101.600006 -25.400002
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 3.0950184 -4 -1
vertex 0 -0 -1
vertex 3.0950184 -0 -1
vertex 78.613464 -101.600006 -25.400002
vertex 0 -0 -25.400002
vertex 78.613464 -0 -25.400002
endloop
endfacet
facet normal -0.57357645 0 -0.81915206
facet normal -0.5735764 0 -0.8191522
outer loop
vertex 3.0950184 -4 -1
vertex 3.0950184 -0 -1
vertex 5.9513144 -4 -3
vertex 78.613464 -101.600006 -25.400002
vertex 78.613464 -0 -25.400002
vertex 151.16339 -101.600006 -76.2
endloop
endfacet
facet normal -0.57357645 0 -0.81915206
facet normal -0.5735764 0 -0.8191522
outer loop
vertex 5.9513144 -4 -3
vertex 3.0950184 -0 -1
vertex 5.9513144 -0 -3
vertex 151.16339 -101.600006 -76.2
vertex 78.613464 -0 -25.400002
vertex 151.16339 -0 -76.2
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 5.9513144 -4 -3
vertex 5.9513144 -0 -3
vertex 9.5 -4 -3
vertex 151.16339 -101.600006 -76.2
vertex 151.16339 -0 -76.2
vertex 241.3 -101.600006 -76.2
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 9.5 -4 -3
vertex 5.9513144 -0 -3
vertex 9.5 -0 -3
vertex 241.3 -101.600006 -76.2
vertex 151.16339 -0 -76.2
vertex 241.3 -0 -76.2
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 9.5 -4 -3
vertex 9.5 -0 -3
vertex 9.5 -4 -2.5
vertex 241.3 -101.600006 -76.2
vertex 241.3 -0 -76.2
vertex 241.3 -101.600006 -63.5
endloop
endfacet
facet normal 1 -0 0
outer loop
vertex 9.5 -4 -2.5
vertex 9.5 -0 -3
vertex 9.5 -0 -2.5
vertex 241.3 -101.600006 -63.5
vertex 241.3 -0 -76.2
vertex 241.3 -0 -63.5
endloop
endfacet
facet normal 0 -0 1
outer loop
vertex 241.3 -101.600006 -63.5
vertex 241.3 -0 -63.5
vertex 155.16768 -101.600006 -63.5
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 155.16768 -101.600006 -63.5
vertex 241.3 -0 -63.5
vertex 155.16768 -0 -63.5
endloop
endfacet
facet normal 0.5735765 0 0.81915194
outer loop
vertex 87.15214 -101.600006 -15.875
vertex 109.82398 -101.600006 -31.75
vertex 109.82398 -0 -31.75
endloop
endfacet
facet normal 0.57357645 0 0.819152
outer loop
vertex 109.82398 -101.600006 -31.75
vertex 155.16768 -101.600006 -63.5
vertex 155.16768 -0 -63.5
endloop
endfacet
facet normal 0.57357645 0 0.81915206
outer loop
vertex 87.15214 -0 -15.875
vertex 64.480286 -101.600006 0
vertex 87.15214 -101.600006 -15.875
endloop
endfacet
facet normal 0.5735765 0 0.81915194
outer loop
vertex 109.82398 -0 -31.75
vertex 87.15214 -0 -15.875
vertex 87.15214 -101.600006 -15.875
endloop
endfacet
facet normal 0.57357645 -0 0.819152
outer loop
vertex 109.82398 -101.600006 -31.75
vertex 155.16768 -0 -63.5
vertex 109.82398 -0 -31.75
endloop
endfacet
facet normal 0.57357645 -0 0.81915206
outer loop
vertex 64.480286 -101.600006 0
vertex 87.15214 -0 -15.875
vertex 64.480286 -0 0
endloop
endfacet
facet normal 0.4226182 0 -0.9063078
outer loop
vertex 84.906715 -101.600006 9.525
vertex 64.480286 -101.600006 0
vertex 64.480286 -0 0
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 105.33314 -101.600006 19.05
vertex 84.906715 -101.600006 9.525
vertex 84.906715 -0 9.525
endloop
endfacet
facet normal 0.4226182 0 -0.9063078
outer loop
vertex 84.906715 -0 9.525
vertex 84.906715 -101.600006 9.525
vertex 64.480286 -0 0
endloop
endfacet
facet normal 0.4226183 0 -0.9063078
outer loop
vertex 105.33314 -0 19.05
vertex 146.18599 -101.600006 38.1
vertex 105.33314 -101.600006 19.05
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 105.33314 -101.600006 19.05
vertex 84.906715 -0 9.525
vertex 105.33314 -0 19.05
endloop
endfacet
facet normal 0.4226183 0 -0.9063078
outer loop
vertex 146.18599 -101.600006 38.1
vertex 105.33314 -0 19.05
vertex 146.18599 -0 38.1
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 146.18599 -101.600006 38.1
vertex 146.18599 -0 38.1
vertex 241.3 -101.600006 38.1
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 241.3 -101.600006 38.1
vertex 146.18599 -0 38.1
vertex 241.3 -0 38.1
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 241.3 -101.600006 38.1
vertex 241.3 -0 38.1
vertex 241.3 -101.600006 50.800003
endloop
endfacet
facet normal 1 -0 0
outer loop
vertex 241.3 -101.600006 50.800003
vertex 241.3 -0 38.1
vertex 241.3 -0 50.800003
endloop
endfacet
facet normal 0 -0 0.99999994
outer loop
vertex 9.5 -4 -2.5
vertex 9.5 -0 -2.5
vertex 6.108964 -4 -2.5
vertex 241.3 -101.600006 50.800003
vertex 241.3 -0 50.800003
vertex 143.37048 -101.600006 50.800003
endloop
endfacet
facet normal 0 0 0.99999994
outer loop
vertex 6.108964 -4 -2.5
vertex 9.5 -0 -2.5
vertex 6.108964 -0 -2.5
vertex 143.37048 -101.600006 50.800003
vertex 241.3 -0 50.800003
vertex 143.37048 -0 50.800003
endloop
endfacet
facet normal 0.5735763 0 0.8191522
facet normal -0.42261827 0 0.9063078
outer loop
vertex 3.4311862 -4 -0.625
vertex 4.323779 -4 -1.25
vertex 4.323779 -0 -1.25
vertex 143.37048 -101.600006 50.800003
vertex 143.37048 -0 50.800003
vertex 88.9 -101.600006 25.400002
endloop
endfacet
facet normal 0.57357645 0 0.819152
facet normal -0.42261827 0 0.9063078
outer loop
vertex 4.323779 -4 -1.25
vertex 6.108964 -4 -2.5
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0.57357645 0 0.819152
outer loop
vertex 3.4311862 -0 -0.625
vertex 2.5385938 -4 0
vertex 3.4311862 -4 -0.625
endloop
endfacet
facet normal 0.5735763 0 0.8191522
outer loop
vertex 4.323779 -0 -1.25
vertex 3.4311862 -0 -0.625
vertex 3.4311862 -4 -0.625
endloop
endfacet
facet normal 0.57357645 -0 0.819152
outer loop
vertex 4.323779 -4 -1.25
vertex 6.108964 -0 -2.5
vertex 4.323779 -0 -1.25
endloop
endfacet
facet normal 0.57357645 -0 0.819152
outer loop
vertex 2.5385938 -4 0
vertex 3.4311862 -0 -0.625
vertex 2.5385938 -0 0
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 3.342784 -4 0.375
vertex 2.5385938 -4 0
vertex 2.5385938 -0 0
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 4.146974 -4 0.75
vertex 3.342784 -4 0.375
vertex 3.342784 -0 0.375
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 3.342784 -0 0.375
vertex 3.342784 -4 0.375
vertex 2.5385938 -0 0
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 4.146974 -0 0.75
vertex 5.755354 -4 1.5
vertex 4.146974 -4 0.75
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 4.146974 -4 0.75
vertex 3.342784 -0 0.375
vertex 4.146974 -0 0.75
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 5.755354 -4 1.5
vertex 4.146974 -0 0.75
vertex 5.755354 -0 1.5
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 5.755354 -4 1.5
vertex 5.755354 -0 1.5
vertex 9.5 -4 1.5
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 9.5 -4 1.5
vertex 5.755354 -0 1.5
vertex 9.5 -0 1.5
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 9.5 -4 1.5
vertex 9.5 -0 1.5
vertex 9.5 -4 2
endloop
endfacet
facet normal 1 -0 0
outer loop
vertex 9.5 -4 2
vertex 9.5 -0 1.5
vertex 9.5 -0 2
vertex 88.9 -101.600006 25.400002
vertex 143.37048 -0 50.800003
vertex 88.9 -0 25.400002
endloop
endfacet
facet normal 0 -0 1
outer loop
vertex 9.5 -4 2
vertex 9.5 -0 2
vertex 5.644507 -4 2
vertex 88.9 -101.600006 25.400002
vertex 88.9 -0 25.400002
vertex 0 -101.600006 25.400002
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 5.644507 -4 2
vertex 9.5 -0 2
vertex 5.644507 -0 2
endloop
endfacet
facet normal -0.42261824 0 0.90630776
outer loop
vertex 5.644507 -4 2
vertex 5.644507 -0 2
vertex 3.5 -4 1
endloop
endfacet
facet normal -0.42261824 0 0.90630776
outer loop
vertex 3.5 -4 1
vertex 5.644507 -0 2
vertex 3.5 -0 1
endloop
endfacet
facet normal 0 -0 1
outer loop
vertex 3.5 -4 1
vertex 3.5 -0 1
vertex 0 -4 1
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 0 -4 1
vertex 3.5 -0 1
vertex 0 -0 1
vertex 0 -101.600006 25.400002
vertex 88.9 -0 25.400002
vertex 0 -0 25.400002
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 1
vertex 0 -0 1
vertex 0 -4 0
vertex 0 -101.600006 25.400002
vertex 0 -0 25.400002
vertex 0 -101.600006 0
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 0
vertex 0 -0 1
vertex 0 -101.600006 0
vertex 0 -0 25.400002
vertex 0 -0 0
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 3.342784 -0 0.375
vertex 2.5385938 -0 0
vertex 3.5 -0 1
vertex 84.906715 -0 9.525
vertex 64.480286 -0 0
vertex 88.9 -0 25.400002
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 4.146974 -0 0.75
vertex 3.342784 -0 0.375
vertex 3.5 -0 1
vertex 105.33314 -0 19.05
vertex 84.906715 -0 9.525
vertex 88.9 -0 25.400002
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 3.4311862 -0 -0.625
vertex 4.323779 -0 -1.25
vertex 3.0950184 -0 -1
vertex 87.15214 -0 -15.875
vertex 109.82398 -0 -31.75
vertex 78.613464 -0 -25.400002
endloop
endfacet
facet normal 0 0.99999994 0
facet normal 0 1 0
outer loop
vertex 4.146974 -0 0.75
vertex 5.644507 -0 2
vertex 5.755354 -0 1.5
vertex 105.33314 -0 19.05
vertex 143.37048 -0 50.800003
vertex 146.18599 -0 38.1
endloop
endfacet
facet normal -0 1 0
outer loop
vertex 0 -0 1
vertex 3.5 -0 1
vertex 2.5385938 -0 0
vertex 0 -0 25.400002
vertex 88.9 -0 25.400002
vertex 64.480286 -0 0
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 1
vertex 2.5385938 -0 0
vertex 0 -0 25.400002
vertex 64.480286 -0 0
vertex 0 -0 0
endloop
endfacet
facet normal -0 1 0
outer loop
vertex 5.644507 -0 2
vertex 9.5 -0 2
vertex 5.755354 -0 1.5
vertex 143.37048 -0 50.800003
vertex 241.3 -0 50.800003
vertex 146.18599 -0 38.1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 9.5 -0 2
vertex 9.5 -0 1.5
vertex 5.755354 -0 1.5
vertex 241.3 -0 50.800003
vertex 241.3 -0 38.1
vertex 146.18599 -0 38.1
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 4.146974 -0 0.75
vertex 3.5 -0 1
vertex 5.644507 -0 2
vertex 105.33314 -0 19.05
vertex 88.9 -0 25.400002
vertex 143.37048 -0 50.800003
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 2.5385938 -0 0
vertex 3.4311862 -0 -0.625
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 4.323779 -0 -1.25
vertex 5.9513144 -0 -3
vertex 3.0950184 -0 -1
vertex 64.480286 -0 0
vertex 87.15214 -0 -15.875
vertex 78.613464 -0 -25.400002
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 6.108964 -0 -2.5
vertex 5.9513144 -0 -3
vertex 4.323779 -0 -1.25
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 9.5 -0 -2.5
vertex 9.5 -0 -3
vertex 6.108964 -0 -2.5
vertex 109.82398 -0 -31.75
vertex 151.16339 -0 -76.2
vertex 78.613464 -0 -25.400002
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 6.108964 -0 -2.5
vertex 9.5 -0 -3
vertex 5.9513144 -0 -3
vertex 155.16768 -0 -63.5
vertex 151.16339 -0 -76.2
vertex 109.82398 -0 -31.75
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 2.5385938 -0 0
vertex 3.0950184 -0 -1
vertex 0 -0 -1
vertex 241.3 -0 -63.5
vertex 241.3 -0 -76.2
vertex 155.16768 -0 -63.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -1
vertex 155.16768 -0 -63.5
vertex 241.3 -0 -76.2
vertex 151.16339 -0 -76.2
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 64.480286 -0 0
vertex 78.613464 -0 -25.400002
vertex 0 -0 -25.400002
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -25.400002
vertex 0 -0 0
vertex 2.5385938 -0 0
vertex 64.480286 -0 0
endloop
endfacet
facet normal -0 -1 0
outer loop
vertex 3.342784 -4 0.375
vertex 3.5 -4 1
vertex 2.5385938 -4 0
vertex 84.906715 -101.600006 9.525
vertex 88.9 -101.600006 25.400002
vertex 64.480286 -101.600006 0
endloop
endfacet
facet normal -0 -1 0
outer loop
vertex 4.146974 -4 0.75
vertex 3.5 -4 1
vertex 3.342784 -4 0.375
vertex 105.33314 -101.600006 19.05
vertex 88.9 -101.600006 25.400002
vertex 84.906715 -101.600006 9.525
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 3.4311862 -4 -0.625
vertex 3.0950184 -4 -1
vertex 4.323779 -4 -1.25
vertex 87.15214 -101.600006 -15.875
vertex 78.613464 -101.600006 -25.400002
vertex 109.82398 -101.600006 -31.75
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 105.33314 -101.600006 19.05
vertex 146.18599 -101.600006 38.1
vertex 143.37048 -101.600006 50.800003
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -101.600006 25.400002
vertex 64.480286 -101.600006 0
vertex 88.9 -101.600006 25.400002
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -101.600006 25.400002
vertex 0 -101.600006 0
vertex 64.480286 -101.600006 0
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 143.37048 -101.600006 50.800003
vertex 146.18599 -101.600006 38.1
vertex 241.3 -101.600006 50.800003
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 241.3 -101.600006 50.800003
vertex 146.18599 -101.600006 38.1
vertex 241.3 -101.600006 38.1
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 105.33314 -101.600006 19.05
vertex 143.37048 -101.600006 50.800003
vertex 88.9 -101.600006 25.400002
endloop
endfacet
facet normal 0 -0.99999994 0
outer loop
vertex 4.146974 -4 0.75
vertex 5.755354 -4 1.5
vertex 5.644507 -4 2
vertex 64.480286 -101.600006 0
vertex 78.613464 -101.600006 -25.400002
vertex 87.15214 -101.600006 -15.875
endloop
endfacet
facet normal 0 -1 0
facet normal -0 -1 -0
outer loop
vertex 0 -4 1
vertex 2.5385938 -4 0
vertex 3.5 -4 1
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -4 1
vertex 0 -4 0
vertex 2.5385938 -4 0
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 5.644507 -4 2
vertex 5.755354 -4 1.5
vertex 9.5 -4 2
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 9.5 -4 2
vertex 5.755354 -4 1.5
vertex 9.5 -4 1.5
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 4.146974 -4 0.75
vertex 5.644507 -4 2
vertex 3.5 -4 1
endloop
endfacet
facet normal 0 -0.99999994 0
outer loop
vertex 2.5385938 -4 0
vertex 3.0950184 -4 -1
vertex 3.4311862 -4 -0.625
endloop
endfacet
facet normal -0 -0.99999994 -0
outer loop
vertex 4.323779 -4 -1.25
vertex 3.0950184 -4 -1
vertex 5.9513144 -4 -3
vertex 109.82398 -101.600006 -31.75
vertex 78.613464 -101.600006 -25.400002
vertex 151.16339 -101.600006 -76.2
endloop
endfacet
facet normal -0 -1 0
outer loop
vertex 6.108964 -4 -2.5
vertex 4.323779 -4 -1.25
vertex 5.9513144 -4 -3
vertex 155.16768 -101.600006 -63.5
vertex 109.82398 -101.600006 -31.75
vertex 151.16339 -101.600006 -76.2
endloop
endfacet
facet normal -0 -0.99999994 -0
facet normal -0 -1 -0
outer loop
vertex 9.5 -4 -2.5
vertex 6.108964 -4 -2.5
vertex 9.5 -4 -3
vertex 241.3 -101.600006 -63.5
vertex 155.16768 -101.600006 -63.5
vertex 241.3 -101.600006 -76.2
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 6.108964 -4 -2.5
vertex 5.9513144 -4 -3
vertex 9.5 -4 -3
vertex 155.16768 -101.600006 -63.5
vertex 151.16339 -101.600006 -76.2
vertex 241.3 -101.600006 -76.2
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 2.5385938 -4 0
vertex 0 -4 -1
vertex 3.0950184 -4 -1
vertex 64.480286 -101.600006 0
vertex 0 -101.600006 -25.400002
vertex 78.613464 -101.600006 -25.400002
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -4 -1
vertex 2.5385938 -4 0
vertex 0 -4 0
vertex 0 -101.600006 -25.400002
vertex 64.480286 -101.600006 0
vertex 0 -101.600006 0
endloop
endfacet
endsolid unnamed

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -3,6 +3,8 @@ import { secrets } from './secrets'
import { getUtils } from './test-utils'
import waitOn from 'wait-on'
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
@ -14,6 +16,12 @@ document.addEventListener('mousemove', (e) =>
)
*/
const commonPoints = {
startAt: '[9.06, -12.22]',
num1: 9.14,
num2: 18.2,
}
test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up
await waitOn({
@ -52,14 +60,15 @@ test('Basic sketch', async ({ page }) => {
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
)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 200)
@ -68,39 +77,37 @@ test('Basic sketch', async ({ page }) => {
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)
|> line([-48, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool
await page.getByRole('button', { name: 'Line' }).click()
@ -122,12 +129,132 @@ test('Basic sketch', async ({ page }) => {
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${num + 0.01}], %)
|> startProfileAt(${commonPoints.startAt}, %)
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|> line([0, ${commonPoints.num1}], %)
|> 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 }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
@ -276,10 +403,9 @@ test('Can create sketches on all planes and their back sides', async ({
}) => {
await u.openDebugPanel()
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.updateCamPosition(viewCmd)
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
@ -305,11 +431,9 @@ test('Can create sketches on all planes and their back sides', async ({
}
const codeTemplate = (
plane = 'XY',
rounded = false,
otherThing = '1'
plane = 'XY'
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
|> startProfileAt([1.14, -1.54], %)`
await TestSinglePlane({
viewCmd: camPos,
expectedCode: codeTemplate('XY'),
@ -318,8 +442,8 @@ test('Can create sketches on all planes and their back sides', async ({
})
await TestSinglePlane({
viewCmd: camPos,
expectedCode: codeTemplate('YZ', true),
clickCoords: { x: 700, y: 300 }, // green plane
expectedCode: codeTemplate('YZ'),
clickCoords: { x: 700, y: 250 }, // green plane
})
await TestSinglePlane({
viewCmd: camPos,
@ -329,7 +453,7 @@ test('Can create sketches on all planes and their back sides', async ({
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', false, '3'),
expectedCode: codeTemplate('-XY'),
clickCoords: { x: 601, y: 118 }, // back of red plane
})
await TestSinglePlane({
@ -339,7 +463,7 @@ test('Can create sketches on all planes and their back sides', async ({
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ', true),
expectedCode: codeTemplate('-XZ'),
clickCoords: { x: 680, y: 427 }, // back of blue plane
})
})
@ -367,6 +491,7 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.type('([0,0], %)')
@ -374,16 +499,21 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.type(' |> lin')
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
await page.waitForTimeout(100)
// press arrow down twice then enter to accept xLine
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
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'))
.toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> xLine(5, %)`)
|> xLine(5, %) // lin`)
})
// Onboarding tests
@ -451,6 +581,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
await u.clearCommandLogs()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
// select a plane
@ -459,35 +592,32 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const num = 24.11
const num2 = '48'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)
|> line([-${num2}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool
await page.getByRole('button', { name: 'Line' }).click()
@ -537,7 +667,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled()
await xAxisClick()
@ -548,7 +678,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.waitForTimeout(300)
await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1)
@ -572,12 +702,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await u.closeDebugPanel()
// select a line
await topHorzSegmentClick()
await page.waitForTimeout(200)
// await topHorzSegmentClick()
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
// enter sketch again
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(700) // wait for animation
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(300) // wait for animation
// hover again and check it works
await selectionSequence()
@ -612,12 +743,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
const themeOption = page.getByRole('option', { name: 'Set Theme' })
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
const themeInput = page.getByPlaceholder('system')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
'data-headlessui-state',
'active'
@ -634,12 +765,15 @@ test('Can extrude from the command bar', async ({ page, context }) => {
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)`
`
const distance = sqrt(20)
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
)
})
@ -664,24 +798,44 @@ test('Can extrude from the command bar', async ({ page, context }) => {
// Click to select face and set distance
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await page.getByRole('button', { name: 'Continue' }).click()
// Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
await page.keyboard.press('Enter')
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
// Review step and argument hotkeys
await page.keyboard.press('2')
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
// Check that the code was updated
await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
|> extrude(5, %)`
`const distance = sqrt(20)
const distance001 = 5 + 7
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
)
})
@ -693,6 +847,9 @@ test('Can add multiple sketches', async ({ page }) => {
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
@ -713,34 +870,32 @@ test('Can add multiple sketches', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)
|> line([-48, 0], %)`
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch
@ -762,7 +917,7 @@ test('Can add multiple sketches', async ({ page }) => {
await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[23.61, -31.85]'
const startAt2 = '[0.93,-1.25]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
@ -776,7 +931,7 @@ const part002 = startSketchOn('XY')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = 23.83
const num2 = 0.94
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
@ -794,7 +949,7 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> 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 expect(
@ -804,14 +959,14 @@ const part002 = startSketchOn('XY')
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)
|> line([-47.44, 0], %)`.replace(/\s/g, '')
|> line([0, ${roundOff(num2 - 0.01)}], %)
|> line([-1.87, 0], %)`.replace(/\s/g, '')
)
})
test('ProgramMemory can be serialised', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async (token) => {
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part = startSketchOn('XY')
@ -847,3 +1002,331 @@ test('ProgramMemory can be serialised', async ({ page, context }) => {
})
})
})
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page,
context,
}) => {
const u = getUtils(page)
const selectionsSnippets = {
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
editOnly: '|> startProfileAt([15.79, -14.6], %)',
}
await context.addInitScript(
async ({
extrudeAndEditBlocked,
extrudeAndEditBlockedInFunction,
extrudeAndEditAllowed,
editOnly,
}: any) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
${extrudeAndEditBlocked}
|> line([25.96, 2.93], %)
|> line([5.25, -5.72], %)
|> line([-2.01, -10.35], %)
|> line([-27.65, -2.78], %)
|> close(%)
|> extrude(5, %)
const part002 = startSketchOn('-XZ')
${extrudeAndEditAllowed}
|> line([10.32, 6.47], %)
|> line([9.71, -6.16], %)
|> line([-3.08, -9.86], %)
|> line([-12.02, -1.54], %)
|> close(%)
const part003 = startSketchOn('-XZ')
${editOnly}
|> line([27.55, -1.65], %)
|> line([4.95, -8], %)
|> line([-20.38, -10.12], %)
|> line([-15.79, 17.08], %)
fn yohey = (pos) => {
const part004 = startSketchOn('-XZ')
${extrudeAndEditBlockedInFunction}
|> line([27.55, -1.65], %)
|> line([4.95, -10.53], %)
|> line([-20.38, -8], %)
|> line([-15.79, 17.08], %)
return ''
}
yohey([15.79, -34.6])
`
)
},
selectionsSnippets
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// wait for start sketch as a proxy for the stream being ready
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page.getByText(selectionsSnippets.editOnly).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page
.getByText(selectionsSnippets.extrudeAndEditBlockedInFunction)
.click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.click(700, 200)
// expect main content to contain `part005` i.e. started a new sketch
await expect(page.locator('.cm-content')).toHaveText(
/part005 = startSketchOn\('-XZ'\)/
)
})
test('Deselecting line tool should mean nothing happens on click', 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()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText()
// deselect the line tool by clicking it
await page.getByRole('button', { name: 'Line' }).click()
await page.mouse.click(700, 200)
await page.waitForTimeout(100)
await page.mouse.click(700, 250)
await page.waitForTimeout(100)
await page.mouse.click(750, 200)
await page.waitForTimeout(100)
// expect no change
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
// select line tool again
await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel()
// line tool should work as expected again
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(700, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.mouse.click(750, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
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

@ -29,92 +29,10 @@ test.beforeEach(async ({ context, page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' })
})
test.setTimeout(60000)
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.setTimeout(60_000)
test('exports of each format should work', async ({ page, context }) => {
test.setTimeout(120_000)
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = getUtils(page)
@ -160,7 +78,7 @@ const part001 = startSketchOn('-XZ')
}, %)
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|> xLineTo(ZERO, %)
|> close(%)
|> close(%)
|> extrude(4, %)`
)
})
@ -173,8 +91,6 @@ const part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await page.getByRole('button', { name: APP_NAME }).click()
interface Paths {
modelPath: string
imagePath: string
@ -183,19 +99,21 @@ const part001 = startSketchOn('-XZ')
const doExport = async (
output: Models['OutputFormat_type']
): Promise<Paths> => {
await page.getByRole('button', { name: 'Export Model' }).click()
const exportSelect = page.getByTestId('export-type')
await exportSelect.selectOption({ label: output.type })
await page.getByRole('button', { name: APP_NAME }).click()
await page.getByRole('button', { name: 'Export Part' }).click()
// Go through export via command bar
await page.getByRole('option', { name: output.type, exact: false }).click()
if ('storage' in output) {
const storageSelect = page.getByTestId('export-storage')
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.getByRole('button', { name: 'Submit command' }).click()
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Export', exact: true }).click()
const download = await downloadPromise
// Handle download
const download = await page.waitForEvent('download')
const downloadLocationer = (extra = '', isImage = false) =>
`./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : ''
@ -342,24 +260,33 @@ const part001 = startSketchOn('-XZ')
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
// context: https://github.com/KittyCAD/modeling-app/issues/1222
for (const { modelPath, imagePath, outputType } of exportLocations) {
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
console.log(
`taking snapshot of using: "zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}"`
)
const cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
const child = spawn(cliCommand, { shell: true })
await new Promise((resolve, reject) => {
const result = await new Promise<string>((resolve, reject) => {
child.on('error', (code: any, msg: any) => {
console.log('error', code, msg)
reject()
reject('error')
})
child.on('exit', (code, msg) => {
console.log('exit', code, msg)
if (code !== 0) {
reject(`exit code ${code} for model ${modelPath}`)
} else {
resolve(true)
resolve('success')
}
})
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
})
expect(result).toBe('success')
if (result === 'success') {
console.log(`snapshot taken for ${modelPath}`)
} else {
console.log(`snapshot failed for ${modelPath}`)
}
}
})
@ -369,13 +296,13 @@ test('extrude on each default plane should be stable', async ({
}) => {
const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([14.06, 8.88], %)
|> line([12.98, -0.15], %)
|> line([5.56, 9.89], %)
|> line([-11.28, 8.96], %)
|> line([-10.81, -7.57], %)
|> startProfileAt([7.00, 4.40], %)
|> line([6.60, -0.20], %)
|> line([2.80, 5.00], %)
|> line([-5.60, 4.40], %)
|> line([-5.40, -3.80], %)
|> close(%)
|> extrude(20, %)
|> extrude(10.00, %)
`
await context.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
@ -419,3 +346,277 @@ test('extrude on each default plane should be stable', async ({
await runSnapshotsForOtherPlanes('YZ')
await runSnapshotsForOtherPlanes('-YZ')
})
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)
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.move(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
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.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await expect(page).toHaveScreenshot({
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.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,16 +1,23 @@
import { expect, Page } from '@playwright/test'
import { expect, Page, errors } from '@playwright/test'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' })
await page.getByTestId('start-sketch').waitFor()
try {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' })
await page.getByTestId('start-sketch').waitFor()
} catch (e) {
if (e instanceof errors.TimeoutError) {
console.log('Timeout while waiting for page load.')
} else {
throw e // re-throw the error if it is not a TimeoutError
}
}
}
async function removeCurrentCode(page: Page) {

69
make-release.sh Executable file
View File

@ -0,0 +1,69 @@
#!/bin/bash
if ! git diff-index --quiet HEAD --; then
echo "Please stash uncommitted changes before running release script"
exit 1
fi
git checkout main
git pull
git fetch --all
# Get the latest semver tag from git
latest_tag=$(jq -r '.version' package.json)
latest_tag="v$latest_tag"
# Print the latest semver tag
echo "Latest semver tag: $latest_tag"
# Function to bump version numbers
bump_version() {
local version=$1
local bump_type=$2
local major=$(echo $version | cut -d '.' -f 1 | sed 's/v//')
local minor=$(echo $version | cut -d '.' -f 2)
local patch=$(echo $version | cut -d '.' -f 3)
case "$bump_type" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
*)
patch=$((patch + 1))
;;
esac
echo "v${major}.${minor}.${patch}"
}
# Determine the type of bump based on the argument
bump_type=${1:-patch}
# Bump the version
new_version=$(bump_version $latest_tag $bump_type)
# Print the new semver tag
echo "New semver tag: $new_version"
new_version_number=${new_version:1}
echo "New version number without 'v': $new_version_number"
git checkout -b "cut-release-$new_version"
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
git add package.json src-tauri/tauri.conf.json
git commit -m "Cut release $new_version"
echo ""
echo "Versions has been bumped in relevant json files, a branch has been created and committed to."
echo ""
echo "What's left for you to do is, push the branch and make the release PR."
echo ""

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.15.0",
"version": "0.15.4",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.10.2",
@ -10,12 +10,11 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.53",
"@kittycad/lib": "^0.0.54",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.77.0",
"@tauri-apps/api": "^1.5.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
@ -34,6 +33,7 @@
"fuse.js": "^7.0.0",
"http-server": "^14.1.1",
"json-rpc-2.0": "^1.6.0",
"node-fetch": "^3.3.2",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -51,7 +51,7 @@
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1",
"vitest": "^0.34.6",
"vitest": "^1.3.1",
"vscode-jsonrpc": "^8.1.0",
"vscode-languageserver-protocol": "^3.17.5",
"wasm-pack": "^0.12.1",
@ -72,7 +72,6 @@
"test": "vitest --mode development",
"test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
"test:cov": "vitest run --coverage --mode development",
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
@ -85,7 +84,7 @@
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "patch-package && yarn xstate:typegen",
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
},
"prettier": {
@ -113,7 +112,6 @@
"@tauri-apps/cli": "^1.5.6",
"@types/crypto-js": "^4.1.1",
"@types/debounce-promise": "^3.1.8",
"@types/isomorphic-fetch": "^0.0.36",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3",
@ -122,8 +120,7 @@
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2020.9.6",
"@types/ws": "^8.5.5",
"@vitejs/plugin-react": "^4.1.1",
"@vitest/coverage-istanbul": "^0.34.6",
"@vitejs/plugin-react": "^4.2.1",
"@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.24.3",
"@wdio/local-runner": "^8.24.3",
@ -136,7 +133,6 @@
"eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^10.8.0",
"husky": "^8.0.3",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",
@ -144,10 +140,10 @@
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6",
"vite": "^4.5.2",
"vite": "^5.1.3",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.2.1",
"vite-tsconfig-paths": "^4.3.1",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"yarn": "^1.22.19"

View File

@ -1,138 +0,0 @@
diff --git a/node_modules/three/examples/jsm/controls/OrbitControls.js b/node_modules/three/examples/jsm/controls/OrbitControls.js
index f29e7fe..0ef636b 100644
--- a/node_modules/three/examples/jsm/controls/OrbitControls.js
+++ b/node_modules/three/examples/jsm/controls/OrbitControls.js
@@ -113,6 +113,25 @@ class OrbitControls extends EventDispatcher {
// public methods
//
+ this.interactionGuards = {
+ pan: {
+ description: 'Right click + Shift + drag or middle click + drag',
+ callback: (e) => e.button === 2 && !e.ctrlKey,
+ },
+ zoom: {
+ description: 'Scroll wheel or Right click + Ctrl + drag',
+ dragCallback: (e) => e.button === 2 && e.ctrlKey,
+ scrollCallback: () => true,
+ },
+ rotate: {
+ description: 'Right click + drag',
+ callback: (e) => e.button === 0,
+ },
+ }
+ this.setMouseGuards = (interactionGuards) => {
+ this.interactionGuards = interactionGuards
+ }
+
this.getPolarAngle = function () {
return spherical.phi;
@@ -1057,92 +1076,21 @@ class OrbitControls extends EventDispatcher {
function onMouseDown( event ) {
- let mouseAction;
-
- switch ( event.button ) {
-
- case 0:
-
- mouseAction = scope.mouseButtons.LEFT;
- break;
-
- case 1:
-
- mouseAction = scope.mouseButtons.MIDDLE;
- break;
-
- case 2:
-
- mouseAction = scope.mouseButtons.RIGHT;
- break;
-
- default:
-
- mouseAction = - 1;
-
- }
-
- switch ( mouseAction ) {
-
- case MOUSE.DOLLY:
-
- if ( scope.enableZoom === false ) return;
-
- handleMouseDownDolly( event );
-
- state = STATE.DOLLY;
-
- break;
-
- case MOUSE.ROTATE:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- } else {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- }
-
- break;
-
- case MOUSE.PAN:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- } else {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- }
-
- break;
-
- default:
-
- state = STATE.NONE;
-
- }
+ if (scope.interactionGuards.pan.callback(event)) {
+ if (scope.enablePan === false) return
+ handleMouseDownPan(event)
+ state = STATE.PAN
+ } else if (scope.interactionGuards.rotate.callback(event)) {
+ if (scope.enableRotate === false) return
+ handleMouseDownRotate(event)
+ state = STATE.ROTATE
+ } else if (scope.interactionGuards.zoom.dragCallback(event)) {
+ if (scope.enableZoom === false) return
+ handleMouseDownDolly(event)
+ state = STATE.DOLLY
+ } else {
+ return
+ }
if ( state !== STATE.NONE ) {

View File

@ -1,16 +1,34 @@
import requests
import re
import os
import requests
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
release_version = os.getenv('RELEASE_VERSION')
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 = {
"content":
f'''
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
{release_body}
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
{modified_release_body}
''',
"username": "Modeling App Release Updates",
"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:
print("Successfully sent the message to Discord.")
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"])

22
src-tauri/Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "app"
@ -1664,9 +1664,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.2.53"
version = "0.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb"
dependencies = [
"anyhow",
"async-trait",
@ -3235,9 +3235,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
@ -3253,9 +3253,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
@ -3275,9 +3275,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.113"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa 1.0.6",
"ryu",
@ -3872,7 +3872,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs-extra"
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#ed682dd96eb765e7cd3cdbc3cc64f794a0d6f9df"
dependencies = [
"log",
"serde",

View File

@ -16,7 +16,7 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies]
anyhow = "1"
kittycad = "0.2.53"
kittycad = "0.2.58"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

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

View File

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

View File

@ -3,15 +3,8 @@ import {
createBrowserRouter,
Outlet,
redirect,
useLocation,
RouterProvider,
} from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
@ -35,9 +28,9 @@ import {
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import CommandBarProvider, {
CommandBar,
} from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
import FileMachineProvider from 'components/FileMachineProvider'
@ -46,38 +39,6 @@ import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
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'
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
@ -117,6 +78,7 @@ const router = createBrowserRouter(
<ModelingMachineProvider>
<Outlet />
<App />
<CommandBar />
</ModelingMachineProvider>
<WasmErrBanner />
</FileMachineProvider>
@ -216,6 +178,7 @@ const router = createBrowserRouter(
<Auth>
<Outlet />
<Home />
<CommandBar />
</Auth>
),
loader: async (): Promise<HomeLoaderData | Response> => {

View File

@ -5,6 +5,13 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { ActionButton } from 'components/ActionButton'
import usePlatform from 'hooks/usePlatform'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { kclManager, useKclContext } from 'lang/KclSingleton'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore'
export const Toolbar = () => {
const platform = usePlatform()
@ -13,14 +20,22 @@ export const Toolbar = () => {
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const bgClassName =
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
const pathId = useMemo(
() =>
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
context.selectionRanges
),
[engineCommandManager.artifactMap, context.selectionRanges]
)
const pathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
return false
}
return isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
context.selectionRanges
)
}, [engineCommandManager.artifactMap, context.selectionRanges])
const { overallState } = useNetworkStatus()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current
@ -50,11 +65,14 @@ export const Toolbar = () => {
<li className="contents">
<ActionButton
Element="button"
onClick={() => send({ type: 'Enter sketch' })}
onClick={() =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
}
icon={{
icon: 'sketch',
bgClassName,
}}
disabled={disableAllButtons}
>
<span data-testid="start-sketch">Start Sketch</span>
</ActionButton>
@ -69,6 +87,7 @@ export const Toolbar = () => {
icon: 'sketch',
bgClassName,
}}
disabled={disableAllButtons}
>
Edit Sketch
</ActionButton>
@ -83,6 +102,7 @@ export const Toolbar = () => {
icon: 'arrowLeft',
bgClassName,
}}
disabled={disableAllButtons}
>
Exit Sketch
</ActionButton>
@ -104,6 +124,7 @@ export const Toolbar = () => {
icon: 'line',
bgClassName,
}}
disabled={disableAllButtons}
>
Line
</ActionButton>
@ -119,12 +140,13 @@ export const Toolbar = () => {
aria-pressed={state.matches('Sketch.Tangential arc to')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'line',
icon: 'arc',
bgClassName,
}}
disabled={
!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')
(!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')) ||
disableAllButtons
}
>
Tangential Arc
@ -164,7 +186,7 @@ export const Toolbar = () => {
disabled={
!state.nextEvents
.filter((event) => state.can(event as any))
.includes(eventName)
.includes(eventName) || disableAllButtons
}
title={eventName}
icon={{
@ -189,7 +211,7 @@ export const Toolbar = () => {
data: { name: 'Extrude', ownerMachine: 'modeling' },
})
}
disabled={!state.can('Extrude')}
disabled={!state.can('Extrude') || disableAllButtons}
title={
state.can('Extrude')
? 'extrude'

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore'
import {
DEBUG_SHOW_BOTH_SCENES,
ReactCameraProperties,
sceneInfra,
} from './sceneInfra'
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
@ -18,7 +15,7 @@ function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const { state } = useModelingContext()
useEffect(() => {
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving)
setIsTween(isTween)
})
@ -52,7 +49,8 @@ export const ClientSideScene = ({
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
useEffect(() => {
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
sceneInfra.camControls.interactionGuards =
cameraMouseDragGuards[cameraControls]
}, [cameraControls])
useEffect(() => {
sceneInfra.updateOtherSelectionColors(
@ -93,7 +91,7 @@ export const ClientSideScene = ({
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
sceneInfra.dollyZoom(a.fov)
sceneInfra.camControls.dollyZoom(a.fov)
}
}, 1000 / 15)
@ -107,7 +105,7 @@ export const CamDebugSettings = () => {
const [fov, setFov] = useState(12)
useEffect(() => {
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
}, [sceneInfra])
useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) {
@ -124,9 +122,9 @@ export const CamDebugSettings = () => {
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
sceneInfra.useOrthographicCamera()
sceneInfra.camControls.useOrthographicCamera()
} else {
sceneInfra.usePerspectiveCamera()
sceneInfra.camControls.usePerspectiveCamera()
}
}}
/>
@ -156,7 +154,7 @@ export const CamDebugSettings = () => {
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
@ -173,7 +171,7 @@ export const CamDebugSettings = () => {
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
@ -194,7 +192,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
@ -214,7 +212,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],
@ -234,7 +232,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],

View File

@ -1,5 +1,5 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './sceneInfra'
import { isQuaternionVertical } from './helpers'
describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => {

View File

@ -1,3 +1,4 @@
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import {
GridHelper,
LineBasicMaterial,
@ -5,6 +6,8 @@ import {
PerspectiveCamera,
Group,
Mesh,
Quaternion,
Vector3,
} from 'three'
export function createGridHelper({
@ -31,3 +34,9 @@ export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)
// no x or y components means it's vertical
return compareVec2Epsilon2([v.x, v.y], [0, 0])
}

View File

@ -3,10 +3,13 @@ import {
DoubleSide,
ExtrudeGeometry,
Group,
Intersection,
LineCurve3,
Matrix4,
Mesh,
MeshBasicMaterial,
Object3D,
Object3DEventMap,
OrthographicCamera,
PerspectiveCamera,
PlaneGeometry,
@ -24,7 +27,7 @@ import {
defaultPlaneColor,
getSceneScale,
INTERSECTION_PLANE_LAYER,
isQuaternionVertical,
OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE,
sceneInfra,
SKETCH_GROUP_SEGMENTS,
@ -34,6 +37,7 @@ import {
Y_AXIS,
YZ_PLANE,
} from './sceneInfra'
import { isQuaternionVertical } from './helpers'
import {
CallExpression,
getTangentialArcToInfo,
@ -56,6 +60,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import {
createArcGeometry,
dashedStraight,
profileStart,
straightSegment,
tangentialArcToSegment,
} from './segments'
@ -63,7 +68,7 @@ import {
addCloseToPipe,
addNewSketchLn,
changeSketchArguments,
compareVec2Epsilon2,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isReducedMotion, throttle } from 'lib/utils'
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_DASH =
'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.
// That mostly mean sketch elements.
@ -98,17 +104,18 @@ class SceneEntities {
currentSketchQuaternion: Quaternion | null = null
constructor() {
this.scene = sceneInfra?.scene
sceneInfra?.setOnCamChange(this.onCamChange)
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
}
onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camera)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
Object.values(this.activeSegments).forEach((segment) => {
const factor =
sceneInfra.camera instanceof OrthographicCamera
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, segment)
: perspScale(sceneInfra.camControls.camera, segment)) /
sceneInfra._baseUnitMultiplier
if (
segment.userData.from &&
segment.userData.to &&
@ -136,21 +143,29 @@ class SceneEntities {
scale: factor,
})
}
if (segment.name === PROFILE_START) {
segment.scale.set(factor, factor, factor)
}
})
if (this.axisGroup) {
const factor =
sceneInfra.camera instanceof OrthographicCamera
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, this.axisGroup)
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
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)
y?.scale.set(factor, 1, 1)
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
}
}
createIntersectionPlane() {
const planeGeometry = new PlaneGeometry(100000, 100000)
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
console.warn('createIntersectionPlane called when it already exists')
return
}
const hundredM = 1000000
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({
color: 0xff0000,
side: DoubleSide,
@ -164,6 +179,7 @@ class SceneEntities {
this.scene.add(this.intersectionPlane)
}
createSketchAxis(sketchPathToNode: PathToNode) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055
const baseYColor = 0x550000
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
@ -195,13 +211,22 @@ class SceneEntities {
this.axisGroup = new Group()
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
gridHelper.position.z = -0.01
gridHelper.renderOrder = -3 // is this working?
gridHelper.name = 'gridHelper'
const sceneScale = getSceneScale(
sceneInfra.camera,
sceneInfra.controls.target
sceneInfra.camControls.camera,
sceneInfra.camControls.target
)
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.currentSketchQuaternion &&
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
@ -238,16 +263,8 @@ class SceneEntities {
ast?: Program
draftSegment?: DraftSegment
}) {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane()
const distance = sceneInfra.controls.target.distanceTo(
sceneInfra.camera.position
)
// TODO this should probably be distance to the sketch group, more important after sketch on face
// since sketches won't always so close to the origin
// is this the best place to adjust camera far?
if (sceneInfra.camera.far < distance * 1.5) {
sceneInfra.camera.far = distance * 2
}
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
this.prepareTruncatedMemoryAndAst(
@ -279,19 +296,57 @@ class SceneEntities {
sketchGroup.position[1],
sketchGroup.position[2]
)
const orthoFactor = orthoScale(sceneInfra.camera)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
sceneInfra.camera instanceof OrthographicCamera
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.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) => {
let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast,
kclManager.ast,
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 =
draftSegment && index === sketchGroup.value.length - 1
let seg
const callExpName = getNodeFromPath<CallExpression>(
kclManager.ast,
segPathToNode,
'CallExpression'
)?.node?.callee?.name
if (segment.type === 'TangentialArcTo') {
seg = tangentialArcToSegment({
prevSegment: sketchGroup.value[index - 1],
@ -310,6 +365,7 @@ class SceneEntities {
pathToNode: segPathToNode,
isDraftSegment,
scale: factor,
callExpName,
})
}
seg.layers.set(SKETCH_LAYER)
@ -331,15 +387,19 @@ class SceneEntities {
this.scene.add(group)
if (!draftSegment) {
sceneInfra.setCallbacks({
onDrag: (args) => {
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
if (mouseEvent.which !== 1) return
this.onDragSegment({
...args,
object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
sketchPathToNode,
})
},
onMove: () => {},
onClick: (args) => {
if (!args || !args.object) {
if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) {
sceneInfra.modelingSend({
type: 'Set selection',
data: {
@ -348,72 +408,32 @@ class SceneEntities {
})
return
}
const { object } = args
const event = getEventForSegmentSelection(object)
const { selected } = args
const event = getEventForSegmentSelection(selected)
if (!event) return
sceneInfra.modelingSend(event)
},
onMouseEnter: ({ object }) => {
// 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)
}
},
...mouseEnterLeaveCallbacks(),
})
} else {
sceneInfra.setCallbacks({
onDrag: () => {},
onClick: async (args) => {
if (!args) return
const { intersection2d } = args
if (!intersection2d) return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
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
if (isClosingSketch) {
// TODO close needs a better UX
if (profileStart) {
modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
} else {
} else if (intersection2d) {
const lastSegment = sketchGroup.value.slice(-1)[0]
modifiedAst = addNewSketchLn({
node: kclManager.ast,
@ -426,6 +446,9 @@ class SceneEntities {
: 'line',
pathToNode: sketchPathToNode,
}).modifiedAst
} else {
// return early as we didn't modify the ast
return
}
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
@ -434,8 +457,9 @@ class SceneEntities {
},
onMove: (args) => {
this.onDragSegment({
...args,
intersection2d: args.intersectionPoint.twoD,
object: Object.values(this.activeSegments).slice(-1)[0],
intersects: args.intersects,
sketchPathToNode,
draftInfo: {
draftSegment,
@ -445,9 +469,10 @@ class SceneEntities {
},
})
},
...mouseEnterLeaveCallbacks(),
})
}
sceneInfra.controls.enableRotate = false
sceneInfra.camControls.enableRotate = false
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
@ -481,17 +506,15 @@ class SceneEntities {
)
onDragSegment({
object,
event,
intersectPoint,
intersection2d,
intersection2d: _intersection2d,
sketchPathToNode,
draftInfo,
intersects,
}: {
object: any
event: any
intersectPoint: Vector3
intersection2d: Vector2
sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: {
draftSegment: DraftSegment
truncatedAst: Program
@ -499,7 +522,20 @@ class SceneEntities {
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
const pathToNode: PathToNode = JSON.parse(
JSON.stringify(group.userData.pathToNode)
@ -523,13 +559,28 @@ class SceneEntities {
).node
if (node.type !== 'CallExpression') return
const modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
[node.start, node.end],
to,
from
)
let modded: {
modifiedAst: Program
pathToNode: PathToNode
}
if (group.name === PROFILE_START) {
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
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
draftInfo
@ -548,10 +599,16 @@ class SceneEntities {
programMemoryOverride,
})
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[variableDeclarationName]
.value as Path[]
const orthoFactor = orthoScale(sceneInfra.camera)
sketchGroup.forEach((segment, index) => {
const sketchGroup = programMemory.root[
variableDeclarationName
] as SketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const updateSegment = (
segment: Path | SketchGroup['start'],
index: number
) => {
const segPathToNode = getNodePathFromSourceRange(
modifiedAst,
segment.__geoMeta.sourceRange
@ -566,12 +623,13 @@ class SceneEntities {
// const prevSegment = sketchGroup.slice(index - 1)[0]
const type = group?.userData?.type
const factor =
sceneInfra.camera instanceof OrthographicCamera
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, group)
: perspScale(sceneInfra.camControls.camera, group)) /
sceneInfra._baseUnitMultiplier
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: sketchGroup[index - 1],
prevSegment: sgPaths[index - 1],
from: segment.from,
to: segment.to,
group: group,
@ -584,8 +642,13 @@ class SceneEntities {
group: group,
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)
})()
}
@ -605,9 +668,7 @@ class SceneEntities {
group.userData.from = from
group.userData.to = to
group.userData.prevSegment = prevSegment
const arrowGroup = group.children.find(
(child) => child.userData.type === ARROWHEAD
) as Group
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
arrowGroup.position.set(to[0], to[1], 0)
@ -682,20 +743,20 @@ class SceneEntities {
const shape = new Shape()
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
const arrowGroup = group.children.find(
(child) => child.userData.type === ARROWHEAD
) as Group
const arrowGroup = group.getObjectByName(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()
.subVectors(
new Vector3(to[0], to[1], 0),
new Vector3(from[0], from[1], 0)
)
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale)
const dir = new Vector3()
.subVectors(
new Vector3(to[0], to[1], 0),
new Vector3(from[0], from[1], 0)
)
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale)
}
const straightSegmentBody = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
@ -725,10 +786,10 @@ class SceneEntities {
}
async animateAfterSketch() {
if (isReducedMotion()) {
sceneInfra.usePerspectiveCamera()
} else {
await sceneInfra.animateToPerspective()
sceneInfra.camControls.usePerspectiveCamera()
return
}
await sceneInfra.camControls.animateToPerspective()
}
removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup)
@ -760,7 +821,7 @@ class SceneEntities {
reject()
}
}
sceneInfra.controls.enableRotate = true
sceneInfra.camControls.enableRotate = true
this.activeSegments = {}
// maybe should reset onMove etc handlers
if (shouldResolve) resolve(true)
@ -780,21 +841,24 @@ class SceneEntities {
}
setupDefaultPlaneHover() {
sceneInfra.setCallbacks({
onMouseEnter: ({ object }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = object.userData.type
object.material.color = defaultPlaneColor(type, 0.5, 1)
onMouseEnter: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type, 0.5, 1)
},
onMouseLeave: ({ object }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = object.userData.type
object.material.color = defaultPlaneColor(type)
onMouseLeave: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type)
},
onClick: (args) => {
if (!args || !args.object) return
const { object, intersection } = args
const type = object?.userData?.type || ''
const posNorm = Number(intersection.normal?.z) > 0
if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return
const { intersects } = args
const type = intersects?.[0].object.name || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
if (type === YZ_PLANE) {
@ -966,9 +1030,9 @@ export function quaternionFromSketchGroup(
}
function colorSegment(object: any, color: number) {
const arrowHead = getParentGroup(object, [ARROWHEAD])
if (arrowHead) {
arrowHead.traverse((child) => {
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
if (segmentHead) {
segmentHead.traverse((child) => {
if (child instanceof Mesh) {
child.material.color.set(color)
}
@ -1024,3 +1088,53 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2])
: 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)
}
},
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -87,7 +87,7 @@ export function useCalc({
inputRef: React.RefObject<HTMLInputElement>
valueNode: Value | null
calcResult: string
prevVariables: PrevVariable<any>[]
prevVariables: PrevVariable<unknown>[]
newVariableName: string
isNewVariableNameUnique: boolean
newVariableInsertIndex: number

View File

@ -4,7 +4,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.dollyZoom(newFov),
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
1000 / 15
)
@ -15,19 +15,19 @@ export const CamToggle = () => {
useEffect(() => {
engineCommandManager.waitForReady.then(async () => {
sceneInfra.dollyZoom(fov)
sceneInfra.camControls.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? sceneInfra.useOrthographicCamera()
: sceneInfra.animateToOrthographic()
? sceneInfra.camControls.useOrthographicCamera()
: sceneInfra.camControls.animateToOrthographic()
} else {
isReducedMotion()
? sceneInfra.usePerspectiveCamera()
: sceneInfra.animateToPerspective()
? sceneInfra.camControls.usePerspectiveCamera()
: sceneInfra.camControls.animateToPerspective()
}
setIsPerspective(!isPerspective)
}
@ -60,9 +60,9 @@ export const CamToggle = () => {
<button
onClick={() => {
if (enableRotate) {
sceneInfra.controls.enableRotate = false
sceneInfra.camControls.enableRotate = false
} else {
sceneInfra.controls.enableRotate = true
sceneInfra.camControls.enableRotate = true
}
setEnableRotate(!enableRotate)
}}

View File

@ -1,8 +1,8 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useRef, useState } from 'react'
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useMemo, useRef, useState } from 'react'
function CommandArgOptionInput({
options,
@ -11,51 +11,89 @@ function CommandArgOptionInput({
onSubmit,
placeholder,
}: {
options: CommandArgumentOption<unknown>[]
options: (CommandArgument<unknown> & { inputType: 'options' })['options']
argName: string
stepBack: () => void
onSubmit: (data: unknown) => void
placeholder?: string
}) {
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 formRef = useRef<HTMLFormElement>(null)
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
commandBarState.context.argumentsToSubmit[argName] ||
options[0].value
const [selectedOption, setSelectedOption] = useState<
CommandArgumentOption<unknown>
>(currentOption || resolvedOptions[0])
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, {
keys: ['name', 'description'],
threshold: 0.3,
})
// Reset the query and selected option when the argName changes
useEffect(() => {
setQuery(initialQuery)
setSelectedOption(currentOption || resolvedOptions[0])
}, [argName])
// Auto focus and select the input when the component mounts
useEffect(() => {
inputRef.current?.focus()
inputRef.current?.select()
}, [inputRef])
// Filter the options based on the query,
// resetting the query when the options change
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : options)
}, [query])
setFilteredOptions(query.length > 0 ? results : resolvedOptions)
}, [query, resolvedOptions, fuse])
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)
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
onSubmit(argValue)
// We submit the value of the selected option, not the whole object
onSubmit(selectedOption.value)
}
return (
<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">
<label
htmlFor="option-input"
@ -75,10 +113,12 @@ function CommandArgOptionInput({
stepBack()
}
}}
value={query}
placeholder={
(argValue as CommandArgumentOption<unknown>)?.name ||
currentOption?.name ||
placeholder ||
'Select an option for ' + argName
argName ||
'Select an option'
}
autoCapitalize="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"
>
<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">
current
</small>

View File

@ -27,13 +27,8 @@ export const CommandBarProvider = ({
}) => {
const { pathname } = useLocation()
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
devTools: true,
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) => {
return (
!context.selectedCommand?.args ||
@ -56,12 +51,11 @@ export const CommandBarProvider = ({
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
export const CommandBar = () => {
const { commandBarState, commandBarSend } = useCommandsContext()
const {
context: { selectedCommand, currentArgument, commands },
@ -81,17 +75,23 @@ const CommandBar = () => {
function stepBack() {
if (!currentArgument) {
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 currentArg = {
name: currentArgName,
...entries[entries.length - 1][1],
}
commandBarSend({
type: commandBarState.matches('Review')
? 'Edit argument'
: 'Change current argument',
type: 'Edit argument',
data: {
arg: {
name: entries[entries.length - 1][0],
...entries[entries.length - 1][1],
},
arg: currentArg,
},
})
} else {

View File

@ -4,6 +4,7 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import CommandBarKclInput from './CommandBarKclInput'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
commandBarSend({
type: 'Submit argument',
data: {
[currentArgument.name]:
currentArgument.inputType === 'number'
? parseFloat((data as string) || '0')
: data,
[currentArgument.name]: data,
},
})
}
@ -68,6 +66,10 @@ function ArgumentInput({
onSubmit={onSubmit}
/>
)
case 'kcl':
return (
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
)
default:
return (
<CommandBarBasicInput

View File

@ -9,7 +9,7 @@ function CommandBarBasicInput({
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'number' | 'string'
inputType: 'string'
name: string
}
stepBack: () => void
@ -18,7 +18,6 @@ function CommandBarBasicInput({
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
const inputType = arg.inputType === 'number' ? 'number' : 'text'
useEffect(() => {
if (inputRef.current) {
@ -40,9 +39,9 @@ function CommandBarBasicInput({
</span>
<input
id="arg-form"
name={inputType}
name={arg.inputType}
ref={inputRef}
type={inputType}
type={arg.inputType}
required
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
placeholder="Enter a value"

View File

@ -4,6 +4,9 @@ import React, { ReactNode, useState } from 'react'
import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook'
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
import Tooltip from 'components/Tooltip'
import { roundOff } from 'lib/utils'
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const { commandBarState, commandBarSend } = useCommandsContext()
@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
parseInt(b.keys[0], 10) - 1
]
const arg = selectedCommand?.args[argName]
if (!argName || !arg) return
commandBarSend({
type: 'Change current argument',
data: { arg: { ...arg, name: argName } },
@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
selectedCommand &&
argumentsToSubmit && (
<>
<div className="px-4 text-sm flex gap-4 items-start">
<div className="group px-4 text-sm flex gap-4 items-start">
<div className="flex flex-1 flex-wrap gap-2">
<p
data-command-name={selectedCommand?.name}
@ -72,47 +76,82 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
)}
{selectedCommand?.name}
</p>
{Object.entries(selectedCommand?.args || {}).map(
([argName, arg], i) => (
<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'
}`}
>
{argumentsToSubmit[argName] ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(
argumentsToSubmit[argName] as Selections
)
) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argumentsToSubmit[argName])
) : (
<em>{argumentsToSubmit[argName] as ReactNode}</em>
)
) : (
<em>{argName}</em>
)}
{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>
)}
</button>
{Object.entries(selectedCommand?.args || {})
.filter(([_, argConfig]) =>
typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required
)
)}
.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 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>
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
</div>

View File

@ -0,0 +1,17 @@
.editor {
@apply text-base flex-1;
}
.editor :global(.cm-editor) {
@apply bg-transparent;
}
.editor :global(.cm-line)::selection {
@apply px-1;
@apply text-chalkboard-100;
@apply bg-energy-10/50;
}
:global(.dark) .editor :global(.cm-line)::selection {
@apply text-energy-10;
@apply bg-energy-10/20;
}

View File

@ -0,0 +1,221 @@
import { Completion } from '@codemirror/autocomplete'
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
import { CustomIcon } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
function CommandBarKclInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'kcl'
name: string
}
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
const previouslySetValue = commandBarState.context.argumentsToSubmit[
arg.name
] as KclCommandValue | undefined
const { settings } = useGlobalStateContext()
const defaultValue = (arg.defaultValue as string) || ''
const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || ''
)
const [createNewVariable, setCreateNewVariable] = useState(
previouslySetValue && 'variableName' in previouslySetValue
)
const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const editorRef = useRef<HTMLDivElement>(null)
const {
prevVariables,
calcResult,
newVariableInsertIndex,
valueNode,
newVariableName,
setNewVariableName,
isNewVariableNameUnique,
} = useCalculateKclExpression({
value,
initialVariableName:
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
})
const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key,
detail: String(roundOff(v.value as number)),
}))
const { setContainer } = useCodeMirror({
container: editorRef.current,
value,
indentWithTab: false,
basicSetup: false,
autoFocus: true,
selection: {
anchor: 0,
head:
previouslySetValue && 'valueText' in previouslySetValue
? previouslySetValue.valueText.length
: defaultValue.length,
},
accessKey: 'command-bar',
theme:
settings.context.theme === 'system'
? getSystemTheme()
: settings.context.theme,
extensions: [
EditorView.domEventHandlers({
keydown: (event) => {
if (event.key === 'Backspace' && value === '') {
event.preventDefault()
stepBack()
}
},
}),
varMentions(varMentionData),
EditorState.transactionFilter.of((tr) => {
if (tr.newDoc.lines > 1) {
handleSubmit()
return []
}
return tr
}),
],
onChange: (newValue) => setValue(newValue),
})
useEffect(() => {
if (editorRef.current) {
setContainer(editorRef.current)
}
}, [arg, editorRef])
useEffect(() => {
setCanSubmit(
calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique)
)
}, [calcResult, createNewVariable, isNewVariableNameUnique])
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
if (!canSubmit || valueNode === null) return
onSubmit(
createNewVariable
? ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
variableName: newVariableName,
insertIndex: newVariableInsertIndex,
variableIdentifierAst: createIdentifier(newVariableName),
variableDeclarationAst: createVariableDeclaration(
newVariableName,
valueNode
),
} satisfies KclCommandValue)
: ({
valueAst: valueNode,
valueText: value,
valueCalculated: calcResult,
} satisfies KclCommandValue)
)
}
return (
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
<span className="capitalize text-chalkboard-80 dark:text-chalkboard-20">
{arg.name}
</span>
<div ref={editorRef} className={styles.editor} />
<CustomIcon
name="equal"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
<span
className={
calcResult === 'NAN'
? 'text-destroy-80 dark:text-destroy-40'
: 'text-energy-60 dark:text-energy-20'
}
>
{calcResult === 'NAN'
? "Can't calculate"
: roundOff(Number(calcResult), 4)}
</span>
</label>
{createNewVariable ? (
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
<label
htmlFor="variable-name"
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
>
Variable name
</label>
<input
type="text"
id="variable-name"
name="variable-name"
className="flex-1 border-none bg-transparent"
placeholder="Variable name"
value={newVariableName}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
autoFocus
onChange={(e) => setNewVariableName(e.target.value)}
onKeyDown={(e) => {
if (e.currentTarget.value === '' && e.key === 'Backspace') {
setCreateNewVariable(false)
}
}}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSubmit()
}
}}
/>
<span
className={
isNewVariableNameUnique
? 'text-energy-60 dark:text-energy-20'
: 'text-destroy-60 dark:text-destroy-40'
}
>
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
</span>
</div>
) : (
<div className="flex justify-between gap-2 px-4">
<button
onClick={() => setCreateNewVariable(true)}
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
<CustomIcon name="plus" className="w-5 h-5" />
Create new variable
</button>
</div>
)}
</form>
)
}
export default CommandBarKclInput

View File

@ -14,7 +14,18 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
})
useHotkeys(
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
[
'alt+1',
'alt+2',
'alt+3',
'alt+4',
'alt+5',
'alt+6',
'alt+7',
'alt+8',
'alt+9',
'alt+0',
],
(_, b) => {
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
if (!selectedCommand?.args) return
@ -37,7 +48,8 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
if (!arg) return
})
function submitCommand() {
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
commandBarSend({
type: 'Submit command',
data: argumentsToSubmit,

View File

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

View File

@ -1,4 +1,5 @@
export type CustomIconName =
| 'arc'
| 'arrowDown'
| 'arrowLeft'
| 'arrowRight'
@ -8,6 +9,7 @@ export type CustomIconName =
| 'clipboardCheckmark'
| 'close'
| 'equal'
| 'exportFile'
| 'extrude'
| 'file'
| 'filePlus'
@ -17,11 +19,14 @@ export type CustomIconName =
| 'horizontal'
| 'horizontalDash'
| 'line'
| 'make-variable'
| 'move'
| 'network'
| 'networkCrossedOut'
| 'parallel'
| 'plus'
| 'search'
| 'settings'
| 'sketch'
| 'vertical'
@ -32,6 +37,22 @@ export const CustomIcon = ({
name: CustomIconName
} & React.SVGProps<SVGSVGElement>) => {
switch (name) {
case 'arc':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 1.5C8.60217 1.5 7.22591 1.84474 5.99313 2.50367C4.76035 3.1626 3.70911 4.1154 2.93251 5.27765C2.15592 6.43991 1.67794 7.77575 1.54093 9.16685C1.40392 10.558 1.6121 11.9614 2.14703 13.2528C2.68195 14.5442 3.52712 15.6838 4.60766 16.5706C5.6882 17.4574 6.97076 18.064 8.34173 18.3367C9.71271 18.6094 11.1298 18.5398 12.4674 18.134C13.8051 17.7282 15.022 16.9988 16.0104 16.0104C16.3068 15.714 16.5796 15.3974 16.8273 15.0634L16.0241 14.4677C15.8055 14.7624 15.5648 15.0418 15.3033 15.3033C14.4312 16.1754 13.3574 16.819 12.1771 17.1771C10.9969 17.5351 9.74651 17.5965 8.53682 17.3559C7.32714 17.1153 6.19547 16.58 5.24205 15.7976C4.28863 15.0151 3.5429 14.0096 3.07091 12.8701C2.59891 11.7306 2.41522 10.4923 2.53612 9.26487C2.65701 8.03743 3.07875 6.85874 3.76398 5.83322C4.44921 4.8077 5.37678 3.967 6.46453 3.38559C7.55227 2.80418 8.76662 2.5 10 2.5C10.3699 2.5 10.7376 2.52734 11.1005 2.58117L11.2472 1.59199C10.836 1.53099 10.4192 1.5 10 1.5ZM13.2067 3.22008C13.5383 3.37691 13.8593 3.5585 14.1668 3.76398C14.4743 3.96946 14.7649 4.19652 15.0367 4.44286L15.7083 3.70191C15.4002 3.42271 15.0709 3.16538 14.7223 2.93251C14.3738 2.69964 14.0101 2.49384 13.6342 2.31609L13.2067 3.22008ZM16.433 6.14423C16.6216 6.45886 16.7876 6.78818 16.9291 7.12987C17.0706 7.47157 17.1861 7.82181 17.2752 8.17765L18.2453 7.93467C18.1443 7.53138 18.0134 7.13444 17.853 6.74719C17.6926 6.35995 17.5044 5.98672 17.2907 5.63012L16.433 6.14423ZM17.491 10.368C17.473 10.7344 17.428 11.1004 17.3559 11.4632C17.2837 11.8259 17.1852 12.1813 17.0616 12.5267L18.0031 12.8636C18.1432 12.4721 18.2549 12.0694 18.3367 11.6583C18.4184 11.2472 18.4694 10.8323 18.4898 10.4171L17.491 10.368Z"
fill="currentColor"
/>
</svg>
)
case 'arrowDown':
return (
<svg
@ -121,8 +142,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM15.6855 11.5L13.2101 14.8005L12.2071 13.7975L11.5 14.5046L12.9107 15.9153L13.6642 15.8617L16.4855 12.1L15.6855 11.5Z"
fill="currentColor"
/>
@ -137,8 +158,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
fill="currentColor"
/>
@ -174,6 +195,22 @@ export const CustomIcon = ({
/>
</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':
return (
<svg
@ -295,8 +332,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M14 10.5H6V9.5H14V10.5Z"
fill="currentColor"
/>
@ -318,6 +355,22 @@ export const CustomIcon = ({
/>
</svg>
)
case 'make-variable':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.07178 6.57735L9.99998 3.1547L15.9282 6.57735V13.4227L9.99998 16.8453L4.07178 13.4227V6.57735ZM9.99998 2L16.9282 6V14L9.99998 18L3.07178 14V6L9.99998 2ZM9.45068 6.854C9.20802 6.798 8.97468 6.78867 8.75068 6.826C8.39602 6.90067 8.06468 7.04533 7.75668 7.26C7.73802 7.26933 7.72402 7.27867 7.71468 7.288C7.45335 7.484 7.24802 7.694 7.09868 7.918C6.96802 8.09533 6.86068 8.282 6.77668 8.478C6.69268 8.65533 6.63668 8.814 6.60868 8.954C6.60868 9.00067 6.62268 9.038 6.65068 9.066L6.69268 9.108H6.95868C7.13602 9.108 7.23402 9.09867 7.25268 9.08C7.28068 9.052 7.30868 8.982 7.33668 8.87C7.45802 8.52467 7.65402 8.212 7.92468 7.932C8.13002 7.72667 8.36802 7.58667 8.63868 7.512C8.83468 7.456 9.02602 7.456 9.21268 7.512C9.40868 7.57733 9.53002 7.68467 9.57668 7.834C9.62335 7.96467 9.61402 8.198 9.54868 8.534L8.77868 11.614C8.65735 11.9593 8.47535 12.216 8.23268 12.384C8.10202 12.4587 7.97602 12.4913 7.85468 12.482C7.68668 12.482 7.53735 12.4307 7.40668 12.328L7.36468 12.286L7.42068 12.272C7.50468 12.244 7.57002 12.216 7.61668 12.188C7.93402 12.02 8.10668 11.7493 8.13468 11.376C8.15335 11.1053 8.05535 10.9187 7.84068 10.816C7.60735 10.6853 7.34135 10.69 7.04268 10.83C6.73468 10.9793 6.54802 11.2547 6.48268 11.656C6.45468 11.8893 6.47335 12.1087 6.53868 12.314C6.56668 12.4073 6.60868 12.4913 6.66468 12.566C6.92602 12.986 7.32268 13.182 7.85468 13.154C8.31202 13.126 8.72268 12.8787 9.08668 12.412L9.12868 12.37L9.21268 12.496C9.44602 12.8133 9.80068 13.0233 10.2767 13.126C10.5474 13.1633 10.79 13.1633 11.0047 13.126C11.6954 12.9767 12.2507 12.58 12.6707 11.936C12.6894 11.9173 12.7034 11.894 12.7127 11.866C12.9553 11.474 13.0767 11.18 13.0767 10.984C13.0767 10.9373 13.0674 10.9047 13.0487 10.886C13.0207 10.8673 12.918 10.858 12.7407 10.858C12.61 10.858 12.526 10.8627 12.4887 10.872C12.442 10.8813 12.4047 10.9327 12.3767 11.026C12.2834 11.3807 12.092 11.7073 11.8027 12.006C11.56 12.23 11.3174 12.3793 11.0747 12.454C11.0094 12.4727 10.9067 12.482 10.7667 12.482C10.6174 12.482 10.5194 12.4727 10.4727 12.454C10.314 12.398 10.1974 12.3 10.1227 12.16C10.0667 12.0573 10.062 11.8613 10.1087 11.572C10.1087 11.5347 10.132 11.4367 10.1787 11.278C10.58 9.542 10.8274 8.55733 10.9207 8.324C11.0887 7.88533 11.3127 7.61467 11.5927 7.512C11.6114 7.50267 11.63 7.498 11.6487 7.498C11.8914 7.43267 12.0967 7.47467 12.2647 7.624L12.3207 7.68L12.2087 7.722C11.8354 7.85267 11.6207 8.128 11.5647 8.548C11.5367 8.76267 11.5927 8.94 11.7327 9.08C11.77 9.11733 11.8167 9.15 11.8727 9.178C12.1714 9.32733 12.4887 9.28067 12.8247 9.038C12.9367 8.954 13.03 8.83267 13.1047 8.674C13.282 8.26333 13.2774 7.87133 13.0907 7.498C12.9787 7.26467 12.7874 7.078 12.5167 6.938C12.162 6.77933 11.8074 6.76533 11.4527 6.896C11.1447 7.01733 10.8787 7.20867 10.6547 7.47L10.5707 7.582C10.552 7.582 10.524 7.554 10.4867 7.498C10.2627 7.17133 9.91735 6.95667 9.45068 6.854Z"
fill="currentColor"
/>
</svg>
)
case 'move':
return (
<svg
@ -343,8 +396,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M18 9.64741C17.1925 8.24871 16.0344 7.08457 14.6399 6.26971C13.2455 5.45486 11.6628 5.01742 10.0478 5.00051C8.4328 4.9836 6.84127 5.38779 5.43006 6.17326C4.01884 6.95873 2.83666 8.09837 2 9.47985L2.76881 9.94546C3.52456 8.69756 4.59243 7.66813 5.86718 6.95862C7.14193 6.2491 8.57955 5.88399 10.0384 5.89927C11.4972 5.91455 12.9269 6.30968 14.1865 7.04574C15.4461 7.7818 16.4922 8.83337 17.2216 10.0968L18 9.64741ZM15.2155 11.0953C14.6772 10.1628 13.9051 9.3867 12.9755 8.84347C12.0459 8.30023 10.9907 8.00861 9.91406 7.99733C8.8374 7.98606 7.77638 8.25552 6.83557 8.77917C5.89476 9.30281 5.10664 10.0626 4.54887 10.9836L5.34391 11.4651C5.81802 10.6822 6.48792 10.0364 7.28761 9.59132C8.0873 9.14622 8.98916 8.91718 9.90432 8.92676C10.8195 8.93635 11.7164 9.18423 12.5065 9.64598C13.2967 10.1077 13.953 10.7674 14.4106 11.56L15.2155 11.0953ZM10 14C10.8284 14 11.5 13.3284 11.5 12.5C11.5 11.6716 10.8284 11 10 11C9.17157 11 8.5 11.6716 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor"
/>
@ -359,8 +412,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM12.5065 9.64599C11.9609 9.32713 11.3643 9.11025 10.746 9.00341L9.74058 7.99796C9.79835 7.99694 9.85618 7.99674 9.91406 7.99735C10.9907 8.00862 12.0459 8.30025 12.9755 8.84348C13.9051 9.38672 14.6772 10.1628 15.2155 11.0953L14.4106 11.56C13.953 10.7674 13.2967 10.1077 12.5065 9.64599ZM6.48788 8.98789L7.16295 9.66297C6.41824 10.1045 5.79317 10.7233 5.34391 11.4651L4.54887 10.9836C5.03646 10.1785 5.70009 9.49656 6.48788 8.98789ZM10.0384 5.89928C9.3134 5.89169 8.59366 5.97804 7.89655 6.15392L7.16867 5.42605C8.09637 5.13507 9.06776 4.99026 10.0478 5.00052C11.6628 5.01744 13.2455 5.45488 14.6399 6.26973C16.0344 7.08458 17.1925 8.24872 18 9.64742L17.2216 10.0968C16.4922 8.83338 15.4461 7.78181 14.1865 7.04575C12.9269 6.3097 11.4972 5.91456 10.0384 5.89928ZM5.00782 7.50783L4.36522 6.86524C3.42033 7.57557 2.61639 8.46208 2 9.47986L2.76881 9.94547C3.34775 8.98952 4.10986 8.16177 5.00782 7.50783ZM10 14C10.4142 14 10.7892 13.8321 11.0607 13.5607L8.93934 11.4394C8.66789 11.7108 8.5 12.0858 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor"
/>
@ -382,6 +435,22 @@ export const CustomIcon = ({
/>
</svg>
)
case 'plus':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
fill="currentColor"
/>
</svg>
)
case 'search':
return (
<svg
@ -398,6 +467,22 @@ export const CustomIcon = ({
/>
</svg>
)
case 'settings':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5.5C8 5.77614 7.77614 6 7.5 6C7.22386 6 7 5.77614 7 5.5C7 5.22386 7.22386 5 7.5 5C7.77614 5 8 5.22386 8 5.5ZM6.08535 6C6.29127 6.5826 6.84689 7 7.5 7C8.32843 7 9 6.32843 9 5.5C9 4.67157 8.32843 4 7.5 4C6.84689 4 6.29127 4.4174 6.08535 5H5V6H6.08535ZM15 6H9.94999C9.98278 5.83844 10 5.67123 10 5.5C10 5.32877 9.98278 5.16155 9.94999 5H15V6ZM11 14.5C11 14.7761 10.7761 15 10.5 15C10.2239 15 10 14.7761 10 14.5C10 14.2239 10.2239 14 10.5 14C10.7761 14 11 14.2239 11 14.5ZM9.08535 15C9.29127 15.5826 9.84689 16 10.5 16C11.3284 16 12 15.3284 12 14.5C12 13.6716 11.3284 13 10.5 13C9.84689 13 9.29127 13.4174 9.08535 14H5V15H9.08535ZM15 15H12.95C12.9828 14.8384 13 14.6712 13 14.5C13 14.3288 12.9828 14.1616 12.95 14H15V15ZM11.5 10.5C11.7761 10.5 12 10.2761 12 10C12 9.72386 11.7761 9.5 11.5 9.5C11.2239 9.5 11 9.72386 11 10C11 10.2761 11.2239 10.5 11.5 10.5ZM11.5 8.5C12.1531 8.5 12.7087 8.9174 12.9146 9.5H15V10.5H12.9146C12.7087 11.0826 12.1531 11.5 11.5 11.5C10.6716 11.5 10 10.8284 10 10C10 9.17157 10.6716 8.5 11.5 8.5ZM9.05001 10.5C9.01722 10.3384 9 10.1712 9 10C9 9.82877 9.01722 9.66155 9.05001 9.5H5V10.5H9.05001Z"
fill="currentColor"
/>
</svg>
)
case 'sketch':
return (
<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>
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

@ -18,6 +18,7 @@ import {
import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { sceneInfra } from 'clientSideScene/sceneInfra'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -30,6 +31,12 @@ type GlobalContext = {
settings: MachineContext<typeof settingsMachine>
}
// a little hacky for sure, open to changing it
// this implies that we should only even have one instance of this provider mounted at any one time
// but I think that's a safe assumption
let settingsStateRef: (typeof settingsMachine)['context'] | undefined
export const getSettingsState = () => settingsStateRef
export const GlobalStateContext = createContext({} as GlobalContext)
export const GlobalStateProvider = ({
@ -50,33 +57,38 @@ export const GlobalStateProvider = ({
>
)
const [settingsState, settingsSend] = useMachine(settingsMachine, {
context: persistedSettings,
actions: {
toastSuccess: (context, event) => {
const truncatedNewValue =
'data' in event && event.data instanceof Object
? (context[Object.keys(event.data)[0] as keyof typeof context]
.toString()
.substring(0, 28) as any)
: undefined
toast.success(
event.type +
(truncatedNewValue
? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : ''
}"`
: '')
)
const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine,
{
context: persistedSettings,
actions: {
toastSuccess: (context, event) => {
const truncatedNewValue =
'data' in event && event.data instanceof Object
? (String(
context[Object.keys(event.data)[0] as keyof typeof context]
).substring(0, 28) as any)
: undefined
toast.success(
event.type +
(truncatedNewValue
? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : ''
}"`
: '')
)
},
},
},
})
}
)
settingsStateRef = settingsState.context
useStateMachineCommands({
machineId: 'settings',
state: settingsState,
send: settingsSend,
commandBarConfig: settingsCommandBarConfig,
actor: settingsActor,
})
// Listen for changes to the system theme and update the app theme accordingly
@ -90,13 +102,14 @@ export const GlobalStateProvider = ({
if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
}
sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm'
matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener)
}, [settingsState.context])
// Auth machine setup
const [authState, authSend] = useMachine(authMachine, {
const [authState, authSend, authActor] = useMachine(authMachine, {
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
@ -116,6 +129,7 @@ export const GlobalStateProvider = ({
state: authState,
send: authSend,
commandBarConfig: authCommandBarConfig,
actor: authActor,
})
return (

View File

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

View File

@ -37,6 +37,11 @@ import { sceneInfra } from 'clientSideScene/sceneInfra'
import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm'
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> = {
state: StateFrom<T>
@ -53,7 +58,12 @@ export const ModelingMachineProvider = ({
}: {
children: React.ReactNode
}) => {
const { auth } = useGlobalStateContext()
const {
auth,
settings: {
context: { baseUnit },
},
} = useGlobalStateContext()
const { code } = useKclContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
@ -169,6 +179,56 @@ export const ModelingMachineProvider = ({
}
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: {
'has valid extrude selection': ({ selectionRanges }) => {
@ -182,12 +242,17 @@ export const ModelingMachineProvider = ({
return canExtrudeSelection(selectionRanges)
},
'Selection is one face': ({ selectionRanges }) => {
'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
return false
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
},
'Has exportable geometry': () =>
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
},
services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
@ -199,6 +264,7 @@ export const ModelingMachineProvider = ({
await kclManager.executeAstMock(newAst, { updates: 'code' })
sceneInfra.setCallbacks({
onClick: () => {},
onDrag: () => {},
})
},
'animate-to-face': async (_, { data: { plane, normal } }) => {
@ -208,7 +274,7 @@ export const ModelingMachineProvider = ({
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await sceneInfra.tweenCameraToQuaternion(quaternion)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
@ -222,7 +288,7 @@ export const ModelingMachineProvider = ({
sketchPathToNode || [],
sketchNormalBackUp
)
await sceneInfra.tweenCameraToQuaternion(quaternion)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({
selectionRanges,
@ -369,6 +435,7 @@ export const ModelingMachineProvider = ({
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
allCommandsRequireNetwork: true,
onCancel: () => modelingSend({ type: 'Cancel' }),
})

View File

@ -80,7 +80,7 @@ const overallConnectionStateIcon: Record<
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
}
export const NetworkHealthIndicator = () => {
export function useNetworkStatus() {
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>(
@ -118,18 +118,18 @@ export const NetworkHealthIndicator = () => {
}, [hasIssues, internetConnected])
useEffect(() => {
const cb1 = () => {
const onlineCallback = () => {
setSteps(initialConnectingTypeGroupState)
setInternetConnected(true)
}
const cb2 = () => {
const offlineCallback = () => {
setInternetConnected(false)
}
window.addEventListener('online', cb1)
window.addEventListener('offline', cb2)
window.addEventListener('online', onlineCallback)
window.addEventListener('offline', offlineCallback)
return () => {
window.removeEventListener('online', cb1)
window.removeEventListener('offline', cb2)
window.removeEventListener('online', onlineCallback)
window.removeEventListener('offline', offlineCallback)
}
}, [])
@ -183,6 +183,30 @@ export const NetworkHealthIndicator = () => {
)
}, [])
return {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
}
}
export const NetworkHealthIndicator = () => {
const {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
} = useNetworkStatus()
return (
<Popover className="relative">
<Popover.Button

View File

@ -3,8 +3,8 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants'
import { vi } from 'vitest'
const now = new Date()
const projectWellFormed = {
@ -41,11 +41,9 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} />
</GlobalStateProvider>
</CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} />
</GlobalStateProvider>
</BrowserRouter>
)
@ -62,11 +60,9 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders app name if given no project', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu />
</GlobalStateProvider>
</CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu />
</GlobalStateProvider>
</BrowserRouter>
)
@ -78,14 +74,9 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders as a link if set to do so', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu
project={projectWellFormed}
renderAsLink={true}
/>
</GlobalStateProvider>
</CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
</GlobalStateProvider>
</BrowserRouter>
)

View File

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

View File

@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
import {
AvailableVars,
addToInputHelper,
useCalc,
CalcResult,
CreateNewVariable,
} from './AvailableVarsHelpers'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = {
value: string
@ -55,7 +55,7 @@ export const SetAngleLengthModal = ({
setNewVariableName,
inputRef,
newVariableInsertIndex,
} = useCalc({
} = useCalculateKclExpression({
value,
initialVariableName: valueName,
})

View File

@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
import {
AvailableVars,
addToInputHelper,
useCalc,
CalcResult,
CreateNewVariable,
} from './AvailableVarsHelpers'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = {
value: string
@ -59,7 +59,7 @@ export const GetInfoModal = ({
newVariableName,
isNewVariableNameUnique,
newVariableInsertIndex,
} = useCalc({ value: value, initialVariableName })
} = useCalculateKclExpression({ value: value, initialVariableName })
return (
<Transition appear show={isOpen} as={Fragment}>

View File

@ -1,10 +1,11 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
import { CreateNewVariable } from './AvailableVarsHelpers'
import { ActionButton } from './ActionButton'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast'
import { type InstanceProps, create } from 'react-modal-promise'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
type ModalResolve = { variableName: string }
type ModalReject = boolean
@ -25,7 +26,7 @@ export const SetVarNameModal = ({
valueName,
}: SetVarNameModalProps) => {
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
useCalc({ value: '', initialVariableName: valueName })
useCalculateKclExpression({ value: '', initialVariableName: valueName })
return (
<Transition appear show={isOpen} as={Fragment}>

View File

@ -1,21 +1,15 @@
import {
MouseEventHandler,
WheelEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates, throttle } from '../lib/utils'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { useKclContext } from 'lang/KclSingleton'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true)
@ -35,9 +29,10 @@ export const Stream = ({ className = '' }: { className?: string }) => {
streamDimensions: s.streamDimensions,
}))
const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext()
const { isExecuting } = useKclContext()
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
useEffect(() => {
if (
@ -65,19 +60,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
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> = ({
clientX,
clientY,
@ -156,7 +138,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
muted
autoPlay
controls={false}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove}
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
@ -164,6 +145,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/>
<ClientSideScene cameraControls={settings.context.cameraControls} />
{!isNetworkOkay && !isLoading && (
<div className="text-center absolute inset-0">
<Loading>
<span data-testid="loading-stream">Stream disconnected</span>
</Loading>
</div>
)}
{isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading>

View File

@ -1,22 +1,23 @@
import { undo, redo } from '@codemirror/commands'
import ReactCodeMirror, {
Extension,
ViewUpdate,
keymap,
} from '@uiw/react-codemirror'
import { FromServer, IntoServer } from 'editor/lsp/codec'
import Server from '../editor/lsp/server'
import Client from '../editor/lsp/client'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Server from '../editor/plugins/lsp/server'
import Client from '../editor/plugins/lsp/client'
import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme'
import { useMemo, useRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { LanguageServerClient } from 'editor/plugins/lsp'
import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { EditorView, lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors'
@ -25,9 +26,14 @@ import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact'
import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useFileContext } from 'hooks/useFileContext'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { sceneInfra } from 'clientSideScene/sceneInfra'
import { copilotBundle } from 'editor/copilot'
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { isTauri } from 'lib/isTauri'
import type * as LSP from 'vscode-languageserver-protocol'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import { useHotkeys } from 'react-hotkeys-hook'
export const editorShortcutMeta = {
formatCode: {
@ -40,6 +46,15 @@ export const editorShortcutMeta = {
},
}
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
// We only use workspace folders in Tauri since that is where we use more than
// one file.
if (isTauri()) {
return [{ uri: 'file://', name: 'ProjectRoot' }]
}
return []
}
export const TextEditor = ({
theme,
}: {
@ -64,6 +79,28 @@ export const TextEditor = ({
}))
const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
useEffect(() => {
if (typeof window === 'undefined') return
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
window.addEventListener('online', onlineCallback)
return () => window.removeEventListener('online', onlineCallback)
}, [])
useHotkeys('mod+z', (e) => {
e.preventDefault()
if (editorView) {
undo(editorView)
}
})
useHotkeys('mod+shift+z', (e) => {
e.preventDefault()
if (editorView) {
redo(editorView)
}
})
const {
context: { selectionRanges, selectionRangeTypeMap },
@ -74,6 +111,9 @@ export const TextEditor = ({
const { settings: { context: { textWrapping } = {} } = {}, auth } =
useGlobalStateContext()
const { commandBarSend } = useCommandsContext()
const {
context: { project },
} = useFileContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
@ -91,12 +131,12 @@ export const TextEditor = ({
})
}
const lspClient = new LanguageServerClient({ client })
const lspClient = new LanguageServerClient({ client, name: 'kcl' })
return { lspClient }
}, [setIsKclLspServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// Now that we have multi-file support the name of the file is a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup because it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
@ -107,7 +147,7 @@ export const TextEditor = ({
const lsp = kclLanguage({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
workspaceFolders: getWorkspaceFolders(),
client: kclLspClient,
})
@ -128,7 +168,7 @@ export const TextEditor = ({
})
}
const lspClient = new LanguageServerClient({ client })
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
return { lspClient }
}, [setIsCopilotLspServerReady])
@ -141,10 +181,10 @@ export const TextEditor = ({
let plugin = null
if (isCopilotLspServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = copilotBundle({
const lsp = copilotPlugin({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
workspaceFolders: getWorkspaceFolders(),
client: copilotLspClient,
allowHTMLContent: true,
})
@ -152,11 +192,12 @@ export const TextEditor = ({
plugin = lsp
}
return plugin
}, [copilotLspClient, isCopilotLspServerReady])
}, [copilotLspClient, isCopilotLspServerReady, project])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (newCode: string) => {
kclManager.setCodeAndExecute(newCode)
const onChange = async (newCode: string) => {
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
else kclManager.setCode(newCode)
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {

View File

@ -27,6 +27,8 @@ describe('UserSidebarMenu tests', () => {
phone: '555-555-5555',
first_name: 'Test',
last_name: 'User',
can_train_on_data: false,
is_service_account: false,
}
render(
@ -57,6 +59,8 @@ describe('UserSidebarMenu tests', () => {
first_name: '',
last_name: '',
name: '',
can_train_on_data: false,
is_service_account: false,
}
render(
@ -84,6 +88,8 @@ describe('UserSidebarMenu tests', () => {
first_name: 'Test',
last_name: 'User',
image: '',
can_train_on_data: false,
is_service_account: false,
}
render(

View File

@ -117,7 +117,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<div className="p-4 flex flex-col gap-2">
<ActionButton
Element="button"
icon={{ icon: 'gear' }}
icon={{ icon: 'settings' }}
className="border-transparent dark:border-transparent hover:bg-transparent"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
@ -142,7 +142,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</ActionButton>
<ActionButton
Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/issues/new"
to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent"
>

View File

@ -65,6 +65,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
afterInitializedHooks: (() => Promise<void>)[] = []
#fromServer: FromServer
private serverCapabilities: LSP.ServerCapabilities<any> = {}
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
constructor(fromServer: FromServer, intoServer: IntoServer) {
super(
@ -167,9 +168,15 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
return this.serverCapabilities
}
setNotifyFn(fn: (message: LSP.NotificationMessage) => void): void {
this.notifyFn = fn
}
async processNotifications(): Promise<void> {
for await (const notification of this.#fromServer.notifications) {
await this.receiveAndSend(notification)
if (this.notifyFn) {
this.notifyFn(notification)
}
}
}

View File

@ -11,30 +11,20 @@ import {
Annotation,
EditorState,
Extension,
Facet,
Prec,
StateEffect,
StateField,
Transaction,
} from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete'
import { docPathFacet, offsetToPos, posToOffset } from 'editor/lsp/util'
import { LanguageServerPlugin } from 'editor/lsp/plugin'
import { LanguageServerOptions } from 'editor/lsp/plugin'
import { LanguageServerClient } from 'editor/lsp'
// Create Facet for the current docPath
export const docPath = Facet.define<string, string>({
combine(value: readonly string[]) {
return value[value.length - 1]
},
})
export const relDocPath = Facet.define<string, string>({
combine(value: readonly string[]) {
return value[value.length - 1]
},
})
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
import {
LanguageServerPlugin,
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
@ -361,9 +351,9 @@ const completionRequester = (client: LanguageServerClient) => {
const pos = state.selection.main.head
const source = state.doc.toString()
const path = state.facet(docPath)
const relativePath = state.facet(relDocPath)
const languageId = 'kcl'
const dUri = state.facet(documentUri)
const path = dUri.split('/').pop()!
const relativePath = dUri.replace('file://', '')
// Set a new timeout to request completion
timeout = setTimeout(async () => {
@ -378,9 +368,9 @@ const completionRequester = (client: LanguageServerClient) => {
indentSize: 1,
insertSpaces: true,
path,
uri: `file://${path}`,
uri: dUri,
relativePath,
languageId,
languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos),
},
})
@ -483,21 +473,24 @@ const completionRequester = (client: LanguageServerClient) => {
})
}
export function copilotServer(options: LanguageServerOptions) {
let plugin: LanguageServerPlugin
return ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
)
}
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: LanguageServerPlugin | null = null
export const copilotBundle = (options: LanguageServerOptions): Extension => [
docPath.of(options.documentUri.split('/').pop()!),
docPathFacet.of(options.documentUri.split('/').pop()!),
relDocPath.of(options.documentUri.replace('file://', '')),
completionDecoration,
Prec.highest(completionPlugin(options.client)),
Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client),
copilotServer(options),
]
return [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
),
completionDecoration,
Prec.highest(completionPlugin(options.client)),
Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client),
]
}

View File

@ -1,7 +1,7 @@
import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client'
import { LanguageServerPlugin } from './plugin'
import { SemanticToken, deserializeTokens } from './semantic_tokens'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
export interface CopilotGetCompletionsParams {
doc: {
@ -90,26 +90,22 @@ interface LSPNotifyMap {
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
}
// Server to client
interface LSPEventMap {
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
}
export type Notification = {
[key in keyof LSPEventMap]: {
jsonrpc: '2.0'
id?: null | undefined
method: key
params: LSPEventMap[key]
}
}[keyof LSPEventMap]
export interface LanguageServerClientOptions {
client: Client
name: string
}
export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
}
export class LanguageServerClient {
private client: Client
private name: string
public ready: boolean
@ -124,6 +120,7 @@ export class LanguageServerClient {
constructor(options: LanguageServerClientOptions) {
this.plugins = []
this.client = options.client
this.name = options.name
this.ready = false
@ -133,11 +130,16 @@ export class LanguageServerClient {
async initialize() {
// Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this))
this.client.start()
this.ready = true
}
getName(): string {
return this.name
}
getServerCapabilities(): LSP.ServerCapabilities<any> {
return this.client.getServerCapabilities()
}
@ -156,6 +158,11 @@ export class LanguageServerClient {
}
async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) {
return
}
// Make sure we can only run, if we aren't already running.
if (!this.isUpdatingSemanticTokens) {
this.isUpdatingSemanticTokens = true
@ -180,10 +187,18 @@ export class LanguageServerClient {
}
async textDocumentHover(params: LSP.HoverParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.hoverProvider) {
return
}
return await this.request('textDocument/hover', params)
}
async textDocumentCompletion(params: LSP.CompletionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.completionProvider) {
return
}
return await this.request('textDocument/completion', params)
}
@ -234,11 +249,12 @@ export class LanguageServerClient {
async acceptCompletion(params: CopilotAcceptCompletionParams) {
return await this.request('notifyAccepted', params)
}
async rejectCompletions(params: CopilotRejectCompletionParams) {
return await this.request('notifyRejected', params)
}
private processNotification(notification: Notification) {
private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) plugin.processNotification(notification)
}
}

View File

@ -0,0 +1,85 @@
import { autocompletion } from '@codemirror/autocomplete'
import { Extension } from '@codemirror/state'
import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util'
import { LanguageServerOptions } from 'editor/plugins/lsp'
import { syntaxTree } from '@codemirror/language'
import {
LanguageServerPlugin,
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
export function kclPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
return [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
autocompletion({
override: [
async (context) => {
if (plugin == null) return null
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)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}

View File

@ -5,8 +5,8 @@ import {
defineLanguageFacet,
LanguageSupport,
} from '@codemirror/language'
import { LanguageServerClient } from '.'
import { kclPlugin } from './plugin'
import { LanguageServerClient } from 'editor/plugins/lsp'
import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror'
@ -14,7 +14,7 @@ import { EditorState } from '@uiw/react-codemirror'
const data = defineLanguageFacet({})
export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] | null
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
client: LanguageServerClient
}

View File

@ -9,8 +9,8 @@ import {
NodeType,
NodeSet,
} from '@lezer/common'
import { LanguageServerClient } from '.'
import { posToOffset } from 'editor/lsp/util'
import { LanguageServerClient } from 'editor/plugins/lsp'
import { posToOffset } from 'editor/plugins/lsp/util'
import { SemanticToken } from './semantic_tokens'
import { DocInput } from '@codemirror/language'
import { tags, styleTags } from '@lezer/highlight'

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