Compare commits

...

105 Commits

Author SHA1 Message Date
c26709c06a Merge remote-tracking branch 'origin' into kurt-circle 2024-09-10 21:03:22 +10:00
fa580d4035 clean up by grouping segment labels 2024-09-10 17:30:21 +10:00
93d3df4877 more renaming 2024-09-10 17:23:11 +10:00
9fafc90c57 remove surplus | 'radius's 2024-09-10 13:23:21 +10:00
04e82bf728 comments and naming tweaks 2024-09-10 13:15:11 +10:00
dab96577a7 fix: users shouldn't have to press down arrow twice to select an option (#3809)
* fix: users shouldn't have to press down arrow twice to select an option

* add regression test for cmd bar arrow

* tweak
2024-09-10 02:10:14 +00:00
0abe4c4082 some xstate v5 stuff that got missed from merge with main 2024-09-10 11:51:03 +10:00
266c601fd4 Merge remote-tracking branch 'origin' into kurt-circle 2024-09-10 11:34:42 +10:00
25443eba31 internal: Add lints for promises (#3733)
* Add lints for floating and misued promises

* Add logging async errors in main

* Add async error catch in test-utils

* Change any to unknown

* Trap promise errors and ignore more await warnings

* Add more ignores and toSync helper

* Fix more lint warnings

* Add more ignores and fixes

* Add more reject reporting

* Add accepting arbitrary parameters to toSync()

* Fix more lints

* Revert unintentional change to non-arrow function

* Revert unintentional change to use arrow function

* Fix new warnings in main with auto updater

* Fix formatting

* Change lints to error

This is what the recommended type checked rules do.

* Fix to properly report promise rejections

* Fix formatting

* Fix formatting

* Remove unused import

* Remove unused convenience function

* Move type helpers

* Fix to not return promise when caller doesn't expect it

* Add ignores to lsp code
2024-09-10 08:17:45 +10:00
0a72d7a39a Remove ill-advised CSS added during #3794 (#3844) 2024-09-09 17:23:01 -04:00
5f8d4f8294 Migrate to XState v5 (#3735)
* migrate settingsMachine

* Guard events with properties instead

* migrate settingsMachine

* Migrate auth machine

* Migrate file machine

* Migrate depracated types

* Migrate home machine

* Migrate command bar machine

* Version fixes

* Migrate command bar machine

* Migrate modeling machine

* Migrate types, state.can, state.matches and state.nextEvents

* Fix syntax

* Pass in modelingState into editor manager instead of modeling event

* Fix issue with missing command bar provider

* Fix state transition

* Fix type issue in Home

* Make sure no guards rely on event type

* Fix up command bar submission logic

* Home machine tweaks to get things running

* Fix AST fillet function args

* Handle "Set selection" when it is called by actor onDone

* Remove unused imports

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

* Fix injectin project to the fileTree machine

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

This reverts commit 4b43ff69d1.

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

* Re-run CI

* Restore success toasts on file/folder deletion

* Replace casting with guarding against event.type

* Remove console.log

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

* Replace all instances of event casting with guards against event.type

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-09-09 12:59:36 -04:00
28a63194c7 more clean up 2024-09-10 01:49:07 +10:00
a6aff874bb more types clean up 2024-09-10 01:43:34 +10:00
25080e9895 clean up create call back double function stuff 2024-09-10 01:08:57 +10:00
99ffc82ffa change types that that effected a lot of places 2024-09-09 22:58:36 +10:00
4219a2c31d more minor clean up renaming 2024-09-09 22:38:02 +10:00
bb265ca833 few more minor renames 2024-09-09 22:10:02 +10:00
63dea715bc some renames 2024-09-09 22:04:43 +10:00
15871e6245 cargo fmt 2024-09-09 20:32:57 +10:00
7ab9b3ee46 Merge remote-tracking branch 'origin' into kurt-circle 2024-09-09 20:29:47 +10:00
e794c5b0e9 Update some circle function calls and twenty twenty stuff 2024-09-09 20:28:24 +10:00
max
7c2cfba0ac Extract updateAstAndFocus from Main Function (#3832)
refactor: pull out updateAst and focus
2024-09-09 12:15:16 +02:00
a1dd884013 stdlib docs update 2024-09-09 19:34:12 +10:00
994f71bce5 make clippy happy 2024-09-09 19:30:51 +10:00
b55652f9b7 fix circle line thickness and handles scale correctly on zoom 2024-09-09 17:10:40 +10:00
9d6a09766c A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) 2024-09-09 06:55:27 +00:00
840e75e5a1 fix dragging circle circumference error 2024-09-09 16:37:41 +10:00
466511ac46 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) 2024-09-09 16:35:54 +10:00
85abcde086 make sure behaviour is sane betwen center and radius clicks 2024-09-09 16:28:22 +10:00
48d347b4de add e2e tests 2024-09-09 16:18:39 +10:00
b3c4aec8db try increas playwright timeout 2024-09-09 14:36:00 +10:00
a59de4efa3 hopefully fix e2e tests 2024-09-09 13:43:52 +10:00
57a460f57a update samples 2024-09-09 13:23:34 +10:00
59284ffa50 rely a little less on arbitary indexs 2024-09-09 12:45:59 +10:00
5592185371 Merge remote-tracking branch 'origin' into kurt-circle 2024-09-09 12:00:32 +10:00
998b194712 fix unit tests 2024-09-09 11:52:23 +10:00
5ee43bda22 Move recast functions to new unparser module (#3824)
This just moves code.  Nothing else was changed.
2024-09-07 12:51:35 -04:00
6753a9e9f8 make lint happy 2024-09-07 15:40:59 +10:00
a1b6bbac7e Replace msi with exe/nsis in download endpoint generation (#3828) 2024-09-06 20:42:47 -04:00
e61516f3c3 Cut release v0.25.1 (#3807)
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-09-06 20:02:14 -04:00
abb8b95b88 Merge remote-tracking branch 'origin' into kurt-circle 2024-09-07 09:09:33 +10:00
94b0510521 big refactor of var values for astMods 2024-09-07 09:04:07 +10:00
e2eeec37ad Don't paste in code for "Closing settings modal should..." test (#3827) 2024-09-06 18:15:13 -04:00
d7fcc128aa KCL: Break face-analyzing map into its own function (#3792) 2024-09-06 17:02:04 -05:00
cf266b17c1 Batch extrudes (#3764)
Adds a new KCL executor benchmark which builds a `10` wide by `n` tall lego, with varying `n`. The benchmark runs a n = 1, 2, 3 etc build, so we can get an idea of how the speed changes with size. 

This change improves execution speed by 25-36% depending on how many bumps there are. Tested by:

* Rust unit tests
* Open up modeling app, sketch a square, use the command palette to extrude it
* Open up the Bambu printer "poop chute" model, it all extrudes and works fine

Also fixes a bug: extrude, loft, revolve all trigger a GetExtrusionFaceInfo command. Due to a bug, the GetExtrusionFaceInfo command reused the Command ID from the previous extrude/loft/revolve. Fixed that.
2024-09-06 16:55:24 -05:00
b3a1796da9 Move cursor with large files (#3825)
* Make code editor go zoom again (reason: parsing is slow)

* Never build wasm bundle in dev mode

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-06 14:52:52 -07:00
39b9a6b2c4 Get playwright green (#3823)
* Fix "Text-to-CAD functionality" electron test

* Make "Closing settings modal should go back..." test not run order-dependent

* Seeing if paneOpen is part of the run order-dependent story

* Fix mistake in selector for text-to-cad test

* Add resiliency to a couple flakes

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-09-06 17:14:02 -04:00
6ba4fa305c Fix double locator issue with text-to-cad test (#3816) 2024-09-06 15:24:59 -04:00
1d043899c8 Bump dashmap from 6.0.1 to 6.1.0 in /src/wasm-lib (#3820)
Bumps [dashmap](https://github.com/xacrimon/dashmap) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v6.0.1...v6.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 10:07:51 -07:00
cb8a087d89 Bump winnow (#3822) 2024-09-06 09:42:11 -05:00
f2eb7b57b8 Update offset plane test images (#3821)
Update offset plane tests
2024-09-06 08:40:29 -05:00
eba653930f Update loft docs + images (#3815) 2024-09-06 07:21:08 -05:00
3deb5c689a The electron-builder updater on 0.25.0 is pointing to the wrong directory (#3818)
Contributes to fixing #3817
2024-09-05 23:22:48 -04:00
11ebe11111 Remove "circle reveal" animation on load of sign-in page (#3802)
* Remove "circle reveal" animation on load of sign-in page

* Remove CSS selector that broke text rendering when OS was dark and app theme is light
2024-09-05 18:59:20 -07:00
9538ffb8ec remove tauri shit (#3812)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-05 18:05:19 -07:00
55d1da226f default to 2 in lofts (#3813)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-05 17:29:07 -07:00
2bfde64bf1 add loft example (#3810)
* add loft example

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

* add docs in between

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-05 16:18:03 -07:00
7cb9a2efd9 Revert "Update package.json to remove tauri dep" (#3811)
Revert "Update package.json to remove tauri dep (#3804)"

This reverts commit dbc5f7b11f.
2024-09-05 15:42:50 -07:00
57e85d7fd0 Upgrade to rust 1.81.0 (#3797)
* Upgrade to rust 1.81.0

* Fix new clippy warnings upgrading to 1.81.0
2024-09-05 21:44:57 +00:00
ca4a442cce Mark Loft as "KCL only" in toolbar, add a link to docs (#3798) 2024-09-05 20:48:03 +00:00
46eef39d53 Set BASE_URL so auth works against dev (#3793)
* Set BASE_URL so auth works against dev

* RM setBaseUrl as it didn't do anything

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-09-05 20:36:13 +00:00
dbc5f7b11f Update package.json to remove tauri dep (#3804)
* Update package.json

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-05 13:14:31 -07:00
6797331c9d Bump clap from 4.5.16 to 4.5.17 in /src/wasm-lib (#3782)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.16 to 4.5.17.
- [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.16...clap_complete-v4.5.17)

---
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-09-05 13:14:06 -07:00
cc80a2da3d Bump serde_json from 1.0.127 to 1.0.128 in /src/wasm-lib (#3783)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.127 to 1.0.128.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/1.0.127...1.0.128)

---
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-09-05 12:41:35 -07:00
54fb9c903a Add no-drag to Toolbar tooltips in Desktop, open links in default browser (#3800)
Fixes #3781
2024-09-05 19:20:51 +00:00
e63597458a Add an Electron app drag handle to sign-in page (#3795)
* Add Electron app drag handle to sign-in page

* Don't assign drag regions in web from JSX, results in dev-only console errors about unsupported style values
2024-09-05 12:18:32 -07:00
e15c38fa23 Fix flash of white background in electron dark mode (#3794) 2024-09-05 11:33:41 -07:00
906ca65611 chore: Implemented electron playwright test to swap between a small cube and large lego (#3732)
* chore: Implemented electron playwright test to swap between a small cube and large lego

* fix: updating comment

* fix: added debug panel and execution done calls

* fix: yarn tsc, fmt, lint

* fix: updating lint warnings

* fix: removing testing line of code that hangs forever :(

* fix: trying a longer timeout

* fix: narrowing the scope to not get multiple elements

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-09-05 10:19:07 -07:00
805b9f48e5 KCL: small refactors to Extrude (#3768)
- Make post-extrude code more functional
 - impl Copy for Point3d
2024-09-04 23:27:12 -05:00
a762d741a5 Generate download endpoint with linux links (#3778) 2024-09-04 20:03:55 -07:00
4b8ca7f61f Double click multiple windows fixes (#3777)
* better way of doing on file open

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

* fixes

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

* updates

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

* cleanuer

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-09-05 02:18:33 +00:00
31b0a8af12 Cut release v0.25.0 (#3750)
* Cut release v0.25.0

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

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

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-09-04 21:33:22 -04:00
74b4cb9e08 quit macos all close (#3775)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 15:47:04 -07:00
e7c6dd3698 fix macos double click (#3774)
* fixmacos

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

* fix

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>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 15:32:34 -07:00
aa9abbe83f Loft (#3681)
* add loft

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

* add offsetPlane as well

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

* fix offset

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

* change to 2

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>

* fixes

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

* fixes

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

* fixes

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

* add-docs

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

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 14:34:33 -07:00
b19f3bbdb0 chore: added new command log for export done (#3770) 2024-09-04 14:03:21 -07:00
892e856471 fix export logic (#3769)
* fix export logic

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-09-04 13:12:16 -07:00
84fae12cdd Bump react-hotkeys-hook from 4.5.0 to 4.5.1 (#3759)
Bumps [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: react-hotkeys-hook
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-04 12:37:51 -07:00
3d67781039 File associations (#3765)
* do windows

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

* info.plist conversion

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-09-04 12:03:44 -07:00
114c3a2580 Bump eslint-plugin-import from 2.29.1 to 2.30.0 (#3761)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.29.1 to 2.30.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...v2.30.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 11:29:52 -07:00
02b4aa0476 Bump google-github-actions/upload-cloud-storage from 2.1.1 to 2.2.0 (#3762)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.1...v2.2.0)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 11:29:21 -07:00
57f4e1b79c internal: Add ArtifactId type (#3752)
* Add ArtifactId type

* Use ArtifactId type in more places

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-09-04 11:49:13 -04:00
35f9b82a65 Set a default initial directory, handle an existing settings file with no project directory (#3734)
* Fix the project directory setting assignment from file

* Fix default project directory value initialization

* Add a couple tests for loading the app without project directory settings

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

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

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

* trigger CI

* Object merging logic was bad, blew away other app settings if they existed

* Update silly little export file size expectation numbers

* Make rename timeout in test way shorter

* Fix silly little test issues

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-09-04 11:30:09 -04:00
cbddb3553d Remove building scene, add code execution loading indicator to bottom-right controls (#3691)
* Remove isFirstRender because isExecuting means the same and remove building scene

* add small makefile util

* Remove waiting for building scene prompt

* Add a new model state indicator

* fmt lint tsc

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-09-04 08:35:40 -04:00
dd754c78ab Remove license from nsis (one less click) (#3763) 2024-09-04 07:02:49 -04:00
150f56b47a Bump electron-updater from 6.2.1 to 6.3.0 (#3748)
Bumps [electron-updater](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-updater) from 6.2.1 to 6.3.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-updater@6.3.0/packages/electron-updater)

---
updated-dependencies:
- dependency-name: electron-updater
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-04 06:23:25 -04:00
72e522dd51 change circle args 2024-09-04 18:55:05 +10:00
0eef6ab7d3 Some colors and export file size changes occurred engine-side, update tests to match (#3753)
* Some colors and export file size changes occurred engine-side, update tests to match

* Fix up focusrite example after adjacent edge switch

* Yay now the export file is yet another new size

* Update bracket example code and some test colors that broke

* Use a shorter KCL sample so we don't experience weird locator behavior

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

* bump electron playwright timeout

* Relax color expectations a little bit, as they can be different on web and electron

* fix double export test

* unused var

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

* Re-run CI

* Bump getGreatestPixDiff comparisons from 10 to 15 in projects.spec.ts

* bump another pix diff to 15

* make retries work

* update expect numbers

* remove some logs

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-09-04 16:55:15 +10:00
91d3ba3fce bump all the rust things (#3755)
bump all the rust things;

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-03 16:26:35 -07:00
7165aa1b41 Bump google-github-actions/upload-cloud-storage from 2.1.3 to 2.2.0 (#3673)
* Bump google-github-actions/upload-cloud-storage from 2.1.3 to 2.2.0

Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.3 to 2.2.0.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.3...v2.2.0)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-03 16:17:22 -07:00
706af591c5 Merge remote-tracking branch 'origin' into kurt-circle 2024-09-02 20:35:39 +10:00
a1f8ac4548 Merge remote-tracking branch 'origin' into kurt-circle 2024-08-29 15:05:33 +10:00
0d5a8aea93 change start end angle for draft circle so that it animates from the oposite side to the handl 2024-08-29 15:05:22 +10:00
3e4316b614 Merge remote-tracking branch 'origin' into kurt-circle 2024-08-28 20:07:37 +10:00
c6577a5ae6 remove error when circle has second click 2024-08-28 20:03:36 +10:00
379960561d fix weird tear down issue 2024-08-28 19:57:26 +10:00
8d912fa62a make sure cursor is consistent with other tool tips 2024-08-28 19:41:16 +10:00
bfd92a626b remove extra segment handle 2024-08-28 19:40:59 +10:00
44e07ca6e5 fix draft arc not being complete 2024-08-28 19:23:05 +10:00
22c854815a can use circle tool tip 2024-08-28 19:07:26 +10:00
3409aa5cfd Merge remote-tracking branch 'origin' into kurt-circle 2024-08-27 21:26:59 +10:00
c56c446e15 add circle segment to searches 2024-08-24 21:59:35 +10:00
1988888699 update is editing existing sketch 2024-08-24 21:43:39 +10:00
acfa95f728 basic circle edit working 2024-08-24 21:09:21 +10:00
b7c71c01cf xstate start 2024-08-24 20:36:07 +10:00
212 changed files with 16801 additions and 7470 deletions

View File

@ -2,7 +2,9 @@ NODE_ENV=development
DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
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_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"

View File

@ -13,6 +13,8 @@
"plugin:css-modules/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"semi": [
"error",
"never"
@ -24,7 +26,6 @@
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": {
"@typescript-eslint/no-floating-promises": "warn",
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off"

View File

@ -44,7 +44,7 @@ jobs:
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
run: "yarn build:wasm"
- name: Set nightly version
if: github.event_name == 'schedule'
@ -81,8 +81,6 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
@ -142,37 +140,12 @@ jobs:
- name: List artifacts in out/
run: ls -R out
- name: Prepare the tauri update bundles (macOS)
if: ${{ env.BUILD_RELEASE && matrix.os == 'macos-14' }}
run: |
for ARCH in arm64 x64; do
TAURI_DIR=out/tauri/$VERSION/macos
TEMP_DIR=temp/$ARCH
mkdir -p $TAURI_DIR
mkdir -p $TEMP_DIR
unzip out/*-$ARCH-mac.zip -d $TEMP_DIR
tar -czvf "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" -C $TEMP_DIR "Zoo Modeling App.app"
yarn tauri signer sign "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz"
done
ls -R out
- name: Prepare the tauri update bundles (Windows)
if: ${{ env.BUILD_RELEASE && matrix.os == 'windows-2022' }}
run: |
$env:TAURI_DIR="out/tauri/${env:VERSION}/nsis"
mkdir -p ${env:TAURI_DIR}
$env:OUT_FILE="${env:TAURI_DIR}/Zoo Modeling App_${env:VERSION_NO_V}_x64-setup.nsis.zip"
7z a -mm=Copy "${env:OUT_FILE}" ./out/*-x64-win.exe
yarn tauri signer sign "${env:OUT_FILE}"
ls -R out
- uses: actions/upload-artifact@v3
with:
name: out-${{ matrix.os }}
path: |
out/Zoo*.*
out/latest*.yml
out/tauri
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
@ -192,8 +165,6 @@ jobs:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
BUCKET_DIR_TAURI: 'dl.kittycad.io/releases/modeling-app/tauri-compat'
WEBSITE_DIR_TAURI: 'dl.zoo.dev/releases/modeling-app/tauri-compat'
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/checkout@v4
@ -212,7 +183,7 @@ jobs:
with:
name: out-ubuntu-22.04
path: out
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}
@ -222,8 +193,10 @@ jobs:
--arg notes "${NOTES}" \
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.msi" \
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.msi" \
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \
'{
"version": $version,
"pub_date": $pub_date,
@ -235,54 +208,22 @@ jobs:
"dmg-x64": {
"url": $mac_x64_url
},
"msi-arm64": {
"exe-arm64": {
"url": $windows_arm64_url
},
"msi-x64": {
"exe-x64": {
"url": $windows_x64_url
},
"appimage-arm64": {
"url": $linux_arm64_url
},
"appimage-x64": {
"url": $linux_x64_url
}
}
}' > last_download.json
cat last_download.json
- name: Generate the update static endpoint for tauri
run: |
TAURI_DIR=out/tauri/$VERSION
MAC_ARM64_SIG=`cat $TAURI_DIR/macos/*-arm64.app.tar.gz.sig`
MAC_X64_SIG=`cat $TAURI_DIR/macos/*-x64.app.tar.gz.sig`
WINDOWS_SIG=`cat $TAURI_DIR/nsis/*.nsis.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR_TAURI}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg mac_arm64_sig "$MAC_ARM64_SIG" \
--arg mac_arm64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-arm64.app.tar.gz" \
--arg mac_x64_sig "$MAC_X64_SIG" \
--arg mac_x64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-x64.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/nsis/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64-setup.nsis.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $mac_x64_sig,
"url": $mac_x64_url
},
"darwin-aarch64": {
"signature": $mac_arm64_sig,
"url": $mac_arm64_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
}
}
}' > last_update.json
cat last_update.json
- name: List artifacts
run: "ls -R out"
@ -297,39 +238,43 @@ jobs:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload release files to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'Zoo*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'latest*'
parent: false
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload update endpoint to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: "out/tauri/${{ env.VERSION }}"
glob: '*/Zoo*'
path: out
glob: 'latest*'
parent: false
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload update endpoint to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: last_update.json
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github

View File

@ -28,6 +28,7 @@ jobs:
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
@ -41,7 +42,7 @@ jobs:
- name: Run clippy
run: |
cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings
just lint
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating

View File

@ -262,7 +262,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 30
timeout-minutes: 60
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
@ -381,7 +381,7 @@ jobs:
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi

View File

@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
yarn start
# I'm sorry this is so specific to my setup you may as well ignore this.
# This is so you don't have to deal with electron windows popping up constantly.
# It should work for you other Linux users.
lee-electron-test:
Xephyr -br -ac -noreset -screen 1200x500 :2 &
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
killall Xephyr
$(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)'

View File

@ -351,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details>
### Tauri e2e tests
#### Windows (local only until the CI edge version mismatch is fixed)
```
yarn install
yarn build:wasm-dev
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
yarn vite build --mode development
yarn tauri build --debug -b
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
$env:E2E_TAURI_ENABLED="true"
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
Stop-Process -Name msedgedriver
yarn wdio run wdio.conf.ts
```
## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

View File

@ -270,6 +270,26 @@ const extrusion = extrude(5, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -479,6 +499,26 @@ const extrusion = extrude(5, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -274,6 +274,26 @@ const extrusion = extrude(5, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -483,6 +503,26 @@ const extrusion = extrude(5, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -189,6 +189,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -398,6 +418,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -609,6 +649,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -818,6 +878,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -188,6 +188,26 @@ const extrusion = extrude(10, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -397,6 +417,26 @@ const extrusion = extrude(10, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -608,6 +648,26 @@ const extrusion = extrude(10, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -817,6 +877,26 @@ const extrusion = extrude(10, sketch001)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -190,6 +190,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -399,6 +419,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -610,6 +650,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -819,6 +879,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -282,6 +282,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -491,6 +511,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -702,6 +742,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -911,6 +971,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -187,6 +187,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -396,6 +416,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -607,6 +647,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -816,6 +876,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -187,6 +187,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -396,6 +416,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -607,6 +647,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -816,6 +876,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -200,6 +200,26 @@ const exampleSketch = startSketchOn('XZ')
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -409,6 +429,26 @@ const exampleSketch = startSketchOn('XZ')
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -620,6 +660,26 @@ const exampleSketch = startSketchOn('XZ')
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -829,6 +889,26 @@ const exampleSketch = startSketchOn('XZ')
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -193,6 +193,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -402,6 +422,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -613,6 +653,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -822,6 +882,26 @@ const example = extrude(10, exampleSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -415,6 +415,26 @@ const mountingPlate = extrude(thickness, mountingPlateSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],
@ -819,6 +839,26 @@ const mountingPlate = extrude(thickness, mountingPlateSketch)
to: [number, number],
type: "TangentialArc",
} |
{
// arc's direction
ccw: bool,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// the arc's radius
radius: number,
// The tag of the path.
tag: {
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
end: number,
start: number,
value: string,
},
// The to point.
to: [number, number],
type: "Circle",
} |
{
// The from point.
from: [number, number],

View File

@ -56,6 +56,7 @@ layout: manual
* [`line`](kcl/line)
* [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log)
* [`log10`](kcl/log10)
* [`log2`](kcl/log2)
@ -63,6 +64,7 @@ layout: manual
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d)

516
docs/kcl/loft.md Normal file

File diff suppressed because one or more lines are too long

138
docs/kcl/offsetPlane.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, bracket)
await page.addInitScript(() => {
localStorage.setItem(
'persistCode',
`// Extruded Triangle
const sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([10, 0], %)
|> line([-5, 10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
@ -261,10 +271,7 @@ test(
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await u.waitForPageLoad()
})
// If they're open by default, we're not actually testing anything.
@ -292,16 +299,7 @@ test(
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
})
await test.step('All panes opened before should be visible', async () => {

View File

@ -43,12 +43,6 @@ test(
// open the project
await page.getByText(`bracket`).click()
// wait for the project to load
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -56,6 +50,12 @@ test(
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Wait for the model to finish loading
const modelStateIndicator = page.getByTestId(
'model-state-indicator-execution-done'
)
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
@ -104,7 +104,7 @@ test(
},
{ timeout: 15_000 }
)
.toBe(477327)
.toBe(477481)
// clean up output.gltf
await fsp.rm('output.gltf')

View File

@ -558,7 +558,7 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> circle({ center: [0, 0], radius: dia/2 }, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)`)

View File

@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
})
const {
panesOpen,
openKclCodePanel,
openFilePanel,
createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
@ -201,4 +202,78 @@ test.describe('when using the file tree to', () => {
await electronApp.close()
}
)
test(
'loading small file, then large, then back to small',
{
tag: '@electron',
},
async ({ browser: _ }, testInfo) => {
const { page } = await setupElectron({
testInfo,
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
createNewFile,
openDebugPanel,
closeDebugPanel,
expectCmdLog,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// Create a small file
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
// pasted into main.kcl
await pasteCodeInEditor(kclCube)
// Create a large lego file
await createNewFile('lego')
const legoFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'lego.kcl' }),
})
await expect(legoFile).toBeVisible({ timeout: 60_000 })
await legoFile.click()
const kclLego = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl',
'utf-8'
)
await pasteCodeInEditor(kclLego)
const mainFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'main.kcl' }),
})
// Open settings and enable the debug panel
await page
.getByRole('link', {
name: 'settings Settings',
})
.click()
await page.locator('#showDebugPanel').getByText('OffOn').click()
await page.getByTestId('settings-close-button').click()
await test.step('swap between small and large files', async () => {
await openDebugPanel()
// Previously created a file so we need to start back at main.kcl
await mainFile.click()
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
// Click the large file
await legoFile.click()
// Once it is building, click back to the smaller file
await mainFile.click()
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
await closeDebugPanel()
})
}
)
})

View File

@ -147,9 +147,6 @@ test.describe('Can export from electron app', () => {
const u = await getUtils(page)
page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
const pointOnModel = { x: 630, y: 280 }
@ -173,10 +170,10 @@ test.describe('Can export from electron app', () => {
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
const exportLocations: Array<Paths> = []
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
},
{ timeout: 15_000 }
)
.toBe(477327)
.toBe(477481)
// clean up output.gltf
await fsp.rm('output.gltf')
@ -495,10 +492,6 @@ test(
await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
)
@ -856,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`)
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 })
@ -867,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(10)
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
.toBeLessThan(15)
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click()
@ -942,24 +935,15 @@ test(
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -976,24 +960,15 @@ test(
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await u.waitForPageLoad()
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(10)
.toBeLessThan(15)
})
await test.step('Opening the router-template project should load the stream', async () => {
@ -1744,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
})
await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await page.waitForTimeout(2000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()

View File

@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
const crypticErrorText = `ApiError`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
})
test('user should not have to press down twice in cmdbar', async ({
page,
}) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch2 = startSketchOn("XY")
const sketch001 = startSketchAt([-0, -0])
|> line([0, 0], %)
|> line([-4.84, -5.29], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForPageLoad()
await test.step('Check arrow down works', async () => {
await page.getByTestId('command-bar-open-button').click()
await page
.getByRole('option', { name: 'floppy disk arrow Export' })
.click()
// press arrow down key twice
await page.keyboard.press('ArrowDown')
await page.waitForTimeout(100)
await page.keyboard.press('ArrowDown')
// STL is the third option, which makes sense for two arrow downs
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
'STL'
)
await page.keyboard.press('Escape')
await page.waitForTimeout(200)
await page.keyboard.press('Escape')
await page.waitForTimeout(200)
})
await test.step('Check arrow up works', async () => {
// theme in test is dark, which is the second option, which means we can test arrow up
await page.getByTestId('command-bar-open-button').click()
await page.getByText('The overall appearance of the').click()
await page.keyboard.press('ArrowUp')
await page.waitForTimeout(100)
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
'light'
)
})
})
test('executes on load', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -358,6 +419,7 @@ const sketch001 = startSketchAt([-0, -0])
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{
code: bracket,
@ -393,20 +455,22 @@ const sketch001 = startSketchAt([-0, -0])
await test.step('The second export is blocked', async () => {
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).toBeVisible()
await page.waitForTimeout(1000)
await Promise.all([
expect(exportingToastMessage.first()).toBeVisible(),
expect(alreadyExportingToastMessage).toBeVisible(),
])
})
await test.step('The first export still succeeds', async () => {
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(successToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000,
}),
])
})
})
@ -419,10 +483,12 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible()
})

View File

@ -149,14 +149,16 @@ test.describe('Sketch tests', () => {
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(100)
await page.mouse.click(700, 200)
await expect(async () => {
await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode)
.toBe(`const sketch001 = startSketchOn('XZ')
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %)
|> line([-12.34, 12.34], %)
`)
}).toPass({ timeout: 40_000, intervals: [1_000] })
})
test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes
@ -344,6 +346,92 @@ test.describe('Sketch tests', () => {
})
})
test('Can edit a circle center and radius by dragging its handles', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> circle({ center: [4.61, -5.01], radius: 8 }, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [667, 325]
const dragPX = 40
await page
.getByText('circle({ center: [4.61, -5.01], radius: 8 }, %)')
.click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
await test.step('drag circle center handle', async () => {
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
await test.step('drag circle radius handle', async () => {
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> circle({ center: [7.26, -2.37], radius: 11.79 }, %)
`)
})
test('Can edit a sketch that has been extruded in the same pipe', async ({
page,
}) => {

View File

@ -532,6 +532,64 @@ test(
})
}
)
test(
'Draft circle should look right',
{ tag: '@snapshot' },
async ({ page, context }) => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
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 sketch001 = startSketchOn('XZ')`
)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.closeDebugPanel()
const startXPx = 600
// Equip the rectangle tool
// await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.getByTestId('circle-center').click()
// Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')
|> circle({ center: [14.44, -2.44], radius: 1 }, %)`
)
}
)
test.describe(
'Client side scene scale should match engine scale',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -365,10 +365,10 @@ const box = startSketchOn('XY')
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle([
|> circle({ center: [
width / 2,
height - (keychainHoleSize + 1.5)
], keychainHoleSize, %)
], radius: keychainHoleSize }, %)
|> extrude(-thickness, %)`
export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `const thing = 1`

View File

@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
type TestColor = [number, number, number]
export const TEST_COLORS = {
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
}
return maxDiff
},
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
new Promise(async (resolve) => {
await page.screenshot({
path: './e2e/playwright/temp1.png',
fullPage: true,
})
await fn()
const isImageDiff = async () => {
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
new Promise<boolean>((resolve) => {
;(async () => {
await page.screenshot({
path: './e2e/playwright/temp2.png',
path: './e2e/playwright/temp1.png',
fullPage: true,
})
const screenshot1 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp1.png')
)
const screenshot2 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp2.png')
)
const actualDiffCount = pixelMatch(
screenshot1.data,
screenshot2.data,
null,
screenshot1.width,
screenshot2.height
)
return actualDiffCount > diffCount
}
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
let count = 0
const interval = setInterval(async () => {
count++
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
await fn()
const isImageDiff = async () => {
await page.screenshot({
path: './e2e/playwright/temp2.png',
fullPage: true,
})
const screenshot1 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp1.png')
)
const screenshot2 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp2.png')
)
const actualDiffCount = pixelMatch(
screenshot1.data,
screenshot2.data,
null,
screenshot1.width,
screenshot2.height
)
return actualDiffCount > diffCount
}
}, 50)
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
let count = 0
const interval = setInterval(() => {
;(async () => {
count++
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
}
})().catch(reportRejection)
}, 50)
})().catch(reportRejection)
}),
emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
@ -548,13 +553,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
await page
const newFile = page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
await expect(newFile).toBeVisible()
await newFile.click()
})
},
@ -585,6 +593,15 @@ export async function getUtils(page: Page, test_?: typeof test) {
})
},
/**
* @deprecated Sorry I don't have time to fix this right now, but runs like
* the one linked below show me that setting the open panes in this manner is not reliable.
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
* or you can imperatively open the panes with functions like {openKclCodePanel}
* (or we can make a general openPane function that takes a paneId).,
* but having a separate initScript does not seem to work reliably.
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
*/
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
@ -852,10 +869,12 @@ export async function setupElectron({
testInfo,
folderSetupFn,
cleanProjectDir = true,
appSettings,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -889,15 +908,19 @@ export async function setupElectron({
if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({
...TEST_SETTINGS,
settings: {
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
},
},
})
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
settings: {
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}

View File

@ -774,6 +774,80 @@ const part001 = startSketchOn('XZ')
locator: '[data-overlay-toolbar-index="12"]',
})
})
test('for segment [circle]', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XZ')
|> circle({ center: [1 + 0, 0], radius: 8 }, %)
`
)
localStorage.setItem('disableAxis', 'true')
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page
.getByText('circle({ center: [1 + 0, 0], radius: 8 }, %)')
.click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page)
const hoverPos = { x: 789, y: 114 } as const
let ang = await u.getAngle('[data-overlay-index="0"]')
console.log('angl', ang)
console.log('circle center x')
await clickConstrained({
hoverPos,
constraintType: 'xAbsolute',
expectBeforeUnconstrained:
'circle({ center: [1 + 0, 0], radius: 8 }, %)',
expectAfterUnconstrained: 'circle({ center: [1, 0], radius: 8 }, %)',
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
ang: ang + 105,
steps: 6,
locator: '[data-overlay-toolbar-index="0"]',
})
console.log('circle center y')
await clickUnconstrained({
hoverPos,
constraintType: 'yAbsolute',
expectBeforeUnconstrained:
'circle({ center: [xAbs001, 0], radius: 8 }, %)',
expectAfterUnconstrained:
'circle({ center: [xAbs001, yAbs001], radius: 8 }, %)',
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
ang: ang + 105,
steps: 10,
locator: '[data-overlay-toolbar-index="0"]',
})
console.log('circle radius')
await clickUnconstrained({
hoverPos,
constraintType: 'radius',
expectBeforeUnconstrained:
'circle({ center: [xAbs001, 0], radius: 8 }, %)',
expectAfterUnconstrained:
'circle({ center: [xAbs001, 0], radius: radius001 }, %)',
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
ang: ang + 105,
steps: 10,
locator: '[data-overlay-toolbar-index="0"]',
})
})
})
test.describe('Testing deleting a segment', () => {
const _deleteSegmentSequence =

View File

@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(1000)
let noHoverColor: [number, number, number] = [82, 82, 82]
let hoverColor: [number, number, number] = [116, 116, 116]
let selectColor: [number, number, number] = [144, 148, 97]
let noHoverColor: [number, number, number] = [92, 92, 92]
let hoverColor: [number, number, number] = [127, 127, 127]
let selectColor: [number, number, number] = [155, 155, 105]
const extrudeWall = { x: 670, y: 275 }
const extrudeText = `line([170.36, -121.61], %, $seg01)`
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
await expect
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(5)
.toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100)
await page.mouse.move(extrudeWall.x, extrudeWall.y)
@ -798,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(200)
await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(6)
).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face
noHoverColor = [104, 104, 104]
hoverColor = [134, 134, 134]
selectColor = [158, 162, 110]
noHoverColor = [115, 115, 115]
hoverColor = [145, 145, 145]
selectColor = [168, 168, 120]
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
await page.mouse.move(cap.x, cap.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText)
)
await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
})
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page,

View File

@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
})
await test.step('Refresh the application and see project setting applied', async () => {
await page.reload()
await page.reload({ waitUntil: 'domcontentloaded' })
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click()
@ -303,53 +303,109 @@ test.describe('Testing settings', () => {
}
)
test(
`Load desktop app with no settings file`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
// This is what makes no settings file get created
cleanProjectDir: false,
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Load desktop app with a settings file, but no project directory setting`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
themeColor: '259',
},
},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(bracketDir, '2.kcl')
)
},
})
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
const kclCylinder = await fsp.readFile(
executorInputPath('cylinder.kcl'),
'utf8'
)
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
clickPane,
createNewFileAndSelect,
openKclCodePanel,
openFilePanel,
waitForPageLoad,
selectFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen([])
await test.step('Precondition: No projects exist', async () => {
await test.step('Precondition: Open to second project file', async () => {
await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link')
await expect(projectLinksPre).toHaveCount(0)
await page.getByText('project-000').click()
await waitForPageLoad()
await openKclCodePanel()
await openFilePanel()
await editorTextMatches(kclCube)
await selectFile('2.kcl')
await editorTextMatches(kclCylinder)
})
await createAndSelectProject('project-000')
await clickPane('code')
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await clickPane('files')
await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
@ -357,6 +413,9 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => {
await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click()
})

View File

@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model.
@ -690,40 +690,53 @@ test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () =>
fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl'))
fs.existsSync(join(dir, projectName, textToCadFileName))
const { createAndSelectProject, panesOpen } = await getUtils(page, test)
const {
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
await panesOpen(['code', 'files'])
// Locators
const projectMenuButton = page.getByRole('button', { name: projectName })
const textToCadFileButton = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: textToCadFileName }),
})
const textToCadComment = page.getByText(
`// Generated by Text-to-CAD: ${prompt}`
)
// Create and navigate to the project
await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await waitForPageLoad()
await openFilePanel()
await openKclCodePanel()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4')
await sendPromptFromCommandBar(page, prompt)
// File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy()
})
await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
await expect(projectMenuButton).toContainText('main.kcl')
await textToCadFileButton.click()
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
await expect(kclComment).toBeVisible({ timeout: 20_000 })
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
await expect(projectMenuButton).toContainText(textToCadFileName)
})
await test.step(`Test file deletion on rejection`, async () => {
@ -737,6 +750,8 @@ test(
)
await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()

View File

@ -11,16 +11,23 @@ mac:
category: public.app-category.developer-tools
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:
- target: dmg
- target: dmg
arch:
- x64
- arm64
- target: zip
- target: zip
arch:
- x64
- arm64
notarize:
teamId: 92H8YB3B95
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
rank: Owner
win:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
@ -38,6 +45,12 @@ win:
sign: "./sign-win.js"
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico"
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
msi:
oneClick: false
@ -47,7 +60,6 @@ nsis:
oneClick: false
perMachine: true
allowElevation: true
license: "LICENSE"
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
@ -58,8 +70,14 @@ linux:
arch:
- x64
- arm64
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder
url: https://dl.zoo.dev/releases/modeling-app
channel: latest

2
interface.d.ts vendored
View File

@ -30,8 +30,6 @@ export interface IElectronAPI {
join: typeof path.join
sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: {
name: string
}

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.24.12",
"version": "0.25.1",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -34,12 +34,12 @@
"@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2",
"@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.2.1",
"electron-updater": "^6.3.0",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",
@ -51,7 +51,7 @@
"react": "^18.3.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"react-hotkeys-hook": "^4.5.1",
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
@ -64,7 +64,7 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2",
"xstate": "^4.38.2"
"xstate": "^5.17.4"
},
"scripts": {
"start": "vite",
@ -137,7 +137,6 @@
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1",
"@tauri-apps/cli": "^2.0.0-rc.9",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
@ -169,7 +168,7 @@
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",

View File

@ -72,6 +72,7 @@ export class LanguageServerClient {
async initialize() {
// Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this))
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.client.start()
this.ready = true
@ -195,6 +196,9 @@ export class LanguageServerClient {
}
private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) plugin.processNotification(notification)
for (const plugin of this.plugins) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.processNotification(notification)
}
}
}

View File

@ -12,6 +12,7 @@ export default function lspFormatExt(
run: (view: EditorView) => {
let value = view.plugin(plugin)
if (!value) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.requestFormatting()
return true
},

View File

@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification = options.processLspNotification
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initialize({
documentText: this.getDocText(),
})
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
}
async initialize({ documentText }: { documentText: string }) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (this.client.initializePromise) {
await this.client.initializePromise
}
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
},
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges()
}
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
contentChanges: [{ text: this.view.state.doc.toString() }],
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges()
} catch (e) {
console.error(e)

View File

@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
import { reportRejection } from 'lib/trap'
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
@ -80,7 +81,7 @@ export function App() {
useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager.sendSceneCommand(message)
engineCommandManager.sendSceneCommand(message).catch(reportRejection)
}, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
if (state.matches('Sketch')) {
@ -95,7 +96,7 @@ export function App() {
})
const newCmdId = uuidv4()
if (state.matches('idle.showPlanes')) return
if (state.matches({ idle: 'showPlanes' })) return
if (context.store?.buttonDownInStream !== undefined) return
debounceSocketSend({
type: 'modeling_cmd_req',
@ -122,11 +123,11 @@ export function App() {
// Override the electron window draggable region behavior as well
// when the button is down in the stream
style={
{
'-webkit-app-region': context.store?.buttonDownInStream
? 'no-drag'
: '',
} as React.CSSProperties
isDesktop() && context.store?.buttonDownInStream
? ({
'-webkit-app-region': 'no-drag',
} as React.CSSProperties)
: {}
}
project={{ project, file }}
enableMenu={true}

View File

@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -69,19 +70,6 @@ const router = createRouter([
path: PATHS.INDEX,
loader: async () => {
const onDesktop = isDesktop()
if (onDesktop) {
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
}
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
@ -186,21 +174,23 @@ function CoreDump() {
[]
)
useHotkeyWrapper(['mod + shift + .'], () => {
toast.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
}
)
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch(reportRejection)
})
return null
}

View File

@ -20,6 +20,8 @@ import {
ToolbarItemResolved,
ToolbarModeName,
} from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function Toolbar({
className = '',
@ -68,12 +70,12 @@ export function Toolbar({
*/
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
() => ({
modelingStateMatches: state.matches,
modelingState: state,
modelingSend: send,
commandBarSend,
sketchPathId,
}),
[state.matches, send, commandBarSend, sketchPathId]
[state, send, commandBarSend, sketchPathId]
)
/**
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
return (
<Tooltip
inert={false}
wrapperStyle={
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
@ -337,6 +344,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
<li key={link.label} className="contents">
<a
href={link.url}
onClick={openExternalBrowserIfDesktop(link.url)}
target="_blank"
rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"

View File

@ -22,11 +22,12 @@ import {
UnreliableSubscription,
} from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { toSync, uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30
@ -100,6 +101,7 @@ export class CameraControls {
camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera()
} else if (
camProps.type === 'orthographic' &&
@ -127,6 +129,7 @@ export class CameraControls {
}
throttledEngCmd = throttle((cmd: EngineCommand) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 30)
@ -139,6 +142,7 @@ export class CameraControls {
...convertThreeCamValuesToEngineCam(threeValues),
},
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15)
@ -151,6 +155,7 @@ export class CameraControls {
this.lastPerspectiveCmd &&
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
this.lastPerspectiveCmdTime = Date.now()
}
@ -218,6 +223,7 @@ export class CameraControls {
this.useOrthographicCamera()
}
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera()
}
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
@ -249,6 +255,7 @@ export class CameraControls {
const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -266,6 +273,7 @@ export class CameraControls {
const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -459,6 +467,7 @@ export class CameraControls {
this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
@ -929,6 +938,7 @@ export class CameraControls {
}
if (isReducedMotion()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
onComplete()
return
}
@ -937,7 +947,7 @@ export class CameraControls {
.to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete)
.onComplete(toSync(onComplete, reportRejection))
.start()
})
}
@ -962,6 +972,7 @@ export class CameraControls {
// Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) {
@ -991,6 +1002,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4
let currentFov = 4
const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera()
const tempVec = new Vector3()
@ -999,6 +1011,7 @@ export class CameraControls {
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
this.camera.up.copy(currentUp)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov)
}
@ -1027,6 +1040,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4
let currentFov = 4
const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera()
const tempVec = new Vector3()

View File

@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
import { throttle, toSync } from 'lib/utils'
import {
sceneInfra,
kclManager,
@ -34,17 +34,15 @@ import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes'
import { getConstraintInfo } from 'lang/std/sketch'
import { Dialog, Popover, Transition } from '@headlessui/react'
import { LineInputsType } from 'lang/std/sketchcombos'
import toast from 'react-hot-toast'
import { InstanceProps, create } from 'react-modal-promise'
import { executeAst } from 'lang/langHelpers'
import {
deleteSegmentFromPipeExpression,
makeRemoveSingleConstraintInput,
removeSingleConstraintInfo,
} from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton'
import { err, trap } from 'lib/trap'
import { err, reportRejection, trap } from 'lib/trap'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
@ -124,9 +122,10 @@ export const ClientSideScene = ({
} else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing'
} else if (
state.matches('Sketch.Line tool') ||
state.matches('Sketch.Tangential arc to') ||
state.matches('Sketch.Rectangle tool')
state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches({ Sketch: 'Circle tool' })
) {
cursor = 'crosshair'
} else {
@ -214,9 +213,9 @@ const Overlay = ({
overlay.visible &&
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
!(
state.matches('Sketch.Line tool') ||
state.matches('Sketch.Tangential arc to') ||
state.matches('Sketch.Rectangle tool')
state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' })
)
return (
@ -514,6 +513,11 @@ const ConstraintSymbol = ({
displayName: 'Intersection Offset',
iconName: 'intersection-offset',
},
radius: {
varName: 'radius',
displayName: 'Radius',
iconName: 'dimension',
},
// implicit constraints
vertical: {
@ -542,12 +546,10 @@ const ConstraintSymbol = ({
iconName: 'dimension',
},
}
const varName =
_type in varNameMap ? varNameMap[_type as LineInputsType].varName : 'var'
const name: CustomIconName = varNameMap[_type as LineInputsType].iconName
const displayName = varNameMap[_type as LineInputsType]?.displayName
const implicitDesc =
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
const varName = _type in varNameMap ? varNameMap[_type].varName : 'var'
const name: CustomIconName = varNameMap[_type].iconName
const displayName = varNameMap[_type]?.displayName
const implicitDesc = varNameMap[_type]?.implicitConstraintDesc
const _node = useMemo(
() => getNodeFromPath<Expr>(kclManager.ast, pathToNode),
@ -582,7 +584,7 @@ const ConstraintSymbol = ({
}}
// disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={async () => {
onClick={toSync(async () => {
if (!isConstrained) {
send({
type: 'Convert to variable',
@ -604,25 +606,23 @@ const ConstraintSymbol = ({
if (trap(_node1)) return Promise.reject(_node1)
const shallowPath = _node1.shallowPath
const input = makeRemoveSingleConstraintInput(
argPosition,
shallowPath
)
if (!input || !context.sketchDetails) return
if (!context.sketchDetails || !argPosition) return
const transform = removeSingleConstraintInfo(
input,
shallowPath,
argPosition,
kclManager.ast,
kclManager.programMemory
)
if (!transform) return
const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true)
} catch (e) {
console.log('error', e)
}
toast.success('Constraint removed')
}
}}
}, reportRejection)}
>
<CustomIcon name={name} />
</button>
@ -688,7 +688,7 @@ const ConstraintSymbol = ({
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
sceneInfra.camControls.dollyZoom(a.fov)
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
}
}, 1000 / 15)
@ -718,6 +718,7 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera()
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.usePerspectiveCamera(true)
}
}}
@ -725,7 +726,7 @@ export const CamDebugSettings = () => {
<div>
<button
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset Camera Position

View File

@ -62,9 +62,10 @@ import {
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst } from 'lang/langHelpers'
import {
circleSegment,
createArcGeometry,
dashedStraight,
profileStart,
createProfileStartHandle,
straightSegment,
tangentialArcToSegment,
} from './segments'
@ -72,6 +73,7 @@ import {
addCallExpressionsToPipe,
addCloseToPipe,
addNewSketchLn,
changeCircleArguments,
changeSketchArguments,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
@ -87,6 +89,7 @@ import {
createArrayExpression,
createCallExpressionStdLib,
createLiteral,
createObjectExpression,
createPipeExpression,
createPipeSubstitution,
findUniqueName,
@ -103,7 +106,7 @@ import {
updateRectangleSketch,
} from 'lib/rectangleTool'
import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap'
import { err, reportRejection, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
@ -119,9 +122,22 @@ export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed'
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const CIRCLE_SEGMENT = 'circle-segment'
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
export const CIRCLE_CENTER_HANDLE = 'circle-center-handle'
export const SEGMENT_WIDTH_PX = 1.6
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
export const SEGMENT_BODIES = [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
CIRCLE_SEGMENT,
]
export const SEGMENT_BODIES_PLUS_PROFILE_START = [
...SEGMENT_BODIES,
PROFILE_START,
]
type Vec3Array = [number, number, number]
@ -186,6 +202,26 @@ export class SceneEntities {
})
)
}
if (
segment.userData.from &&
segment.userData.to &&
segment.userData.center &&
segment.userData.radius &&
segment.userData.type === CIRCLE_SEGMENT
) {
callbacks.push(
this.updateCircleSegment({
prevSegment: segment.userData.prevSegment,
from: segment.userData.from,
to: segment.userData.to,
center: segment.userData.center,
radius: segment.userData.radius,
group: segment,
scale: factor,
})
)
}
if (segment.name === PROFILE_START) {
segment.scale.set(factor, factor, factor)
}
@ -324,6 +360,7 @@ export class SceneEntities {
)
}
sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
@ -421,19 +458,21 @@ export class SceneEntities {
maybeModdedAst,
sketchGroup.start.__geoMeta.sourceRange
)
const _profileStart = profileStart({
from: sketchGroup.start.from,
id: sketchGroup.start.__geoMeta.id,
pathToNode: segPathToNode,
scale: factor,
theme: sceneInfra._theme,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
group.add(_profileStart)
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
if (sketchGroup?.value?.[0].type !== 'Circle') {
const _profileStart = createProfileStartHandle({
from: sketchGroup.start.from,
id: sketchGroup.start.__geoMeta.id,
pathToNode: segPathToNode,
scale: factor,
theme: sceneInfra._theme,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
group.add(_profileStart)
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
}
const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
@ -498,6 +537,32 @@ export class SceneEntities {
scale: factor,
})
)
} else if (segment.type === 'Circle') {
seg = circleSegment({
prevSegment: sketchGroup.value[index - 1],
from: segment.from,
to: segment.to,
center: segment.center,
radius: segment.radius,
id: segment.__geoMeta.id,
pathToNode: segPathToNode,
isDraftSegment,
scale: factor,
texture: sceneInfra.extraSegmentTexture,
theme: sceneInfra._theme,
isSelected,
})
callbacks.push(
this.updateCircleSegment({
prevSegment: sketchGroup.value[index - 1],
from: segment.from,
to: segment.to,
center: segment.center,
radius: segment.radius,
group: seg,
scale: factor,
})
)
} else {
seg = straightSegment({
from: segment.from,
@ -602,16 +667,18 @@ export class SceneEntities {
kclManager.programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sg)) return sg
const lastSeg = sg.value?.slice(-1)[0] || sg.start
if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({
node: _ast,
programMemory: kclManager.programMemory,
to: [lastSeg.to[0], lastSeg.to[1]],
from: [lastSeg.to[0], lastSeg.to[1]],
input: {
type: 'straight-segment',
to: lastSeg.to,
from: lastSeg.to,
},
fnName: segmentName,
pathToNode: sketchPathToNode,
})
@ -634,6 +701,7 @@ export class SceneEntities {
draftExpressionsIndices,
})
sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => {
if (!args) return
if (args.mouseEvent.which !== 1) return
@ -681,8 +749,11 @@ export class SceneEntities {
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersection2d.x, intersection2d.y],
from: [lastSegment.to[0], lastSegment.to[1]],
input: {
type: 'straight-segment',
to: [intersection2d.x, intersection2d.y],
from: lastSegment.to,
},
fnName:
lastSegment.type === 'TangentialArcTo'
? 'tangentialArcTo'
@ -701,7 +772,7 @@ export class SceneEntities {
if (profileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' })
} else {
this.setUpDraftSegment(
await this.setUpDraftSegment(
sketchPathToNode,
forward,
up,
@ -746,6 +817,11 @@ export class SceneEntities {
const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const sg = sketchGroupFromKclValue(
kclManager.programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sg)) return sg
const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'),
findUniqueName(_ast, 'rectangleSegmentB'),
@ -771,6 +847,7 @@ export class SceneEntities {
})
sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onMove: async (args) => {
// Update the width and height of the draft rectangle
const pathToNodeTwo = structuredClone(sketchPathToNode)
@ -802,7 +879,7 @@ export class SceneEntities {
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketchGroup)) return sketchGroup
if (err(sketchGroup)) return Promise.reject(sketchGroup)
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -818,6 +895,7 @@ export class SceneEntities {
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
)
},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => {
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
@ -858,7 +936,7 @@ export class SceneEntities {
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketchGroup)) return sketchGroup
if (err(sketchGroup)) return Promise.reject(sketchGroup)
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -879,6 +957,151 @@ export class SceneEntities {
},
})
}
setupDraftCircle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
circleCenter: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
createCallExpressionStdLib('circle', [
createObjectExpression({
center: createArrayExpression([
createLiteral(roundOff(circleCenter[0])),
createLiteral(roundOff(circleCenter[1])),
]),
radius: createLiteral(1),
}),
createPipeSubstitution(),
]),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
// do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast)
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 0 },
})
sceneInfra.setCallbacks({
onMove: async (args) => {
const pathToNodeTwo = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
let modded = structuredClone(truncatedAst)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
if (sketchInit.type === 'PipeExpression') {
const moddedResult = changeCircleArguments(
modded,
kclManager.programMemory,
[..._node.deepPath, ['body', 'PipeExpression'], [1, 'index']],
circleCenter,
Math.sqrt(x ** 2 + y ** 2)
)
if (err(moddedResult)) return Promise.reject(moddedResult)
modded = moddedResult.modifiedAst
}
const { programMemory } = await executeAst({
ast: modded,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
})
this.sceneProgramMemory = programMemory
const sketchGroup = sketchGroupFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketchGroup)) return sketchGroup
const sgPaths = sketchGroup.value
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
sketchGroup.start,
0,
0,
_ast,
orthoFactor,
sketchGroup
)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
)
},
onClick: async (args) => {
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
const x = roundOff((cornerPoint.x || 0) - circleCenter[0])
const y = roundOff((cornerPoint.y || 0) - circleCenter[1])
const _node = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
let modded = structuredClone(_ast)
if (sketchInit.type === 'PipeExpression') {
const moddedResult = changeCircleArguments(
modded,
kclManager.programMemory,
[..._node.deepPath, ['body', 'PipeExpression'], [1, 'index']],
circleCenter,
Math.sqrt(x ** 2 + y ** 2)
)
if (err(moddedResult)) return Promise.reject(moddedResult)
modded = moddedResult.modifiedAst
let _recastAst = parse(recast(modded))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish circle' })
}
},
})
}
setupSketchIdleCallbacks = ({
pathToNode,
up,
@ -892,9 +1115,11 @@ export class SceneEntities {
}) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
@ -911,6 +1136,7 @@ export class SceneEntities {
})
}
},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDrag: async ({
selected,
intersectionPoint,
@ -944,8 +1170,11 @@ export class SceneEntities {
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: [prevSegment.from[0], prevSegment.from[1]],
input: {
type: 'straight-segment',
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: prevSegment.from,
},
// TODO assuming it's always a straight segments being added
// as this is easiest, and we'll need to add "tabbing" behavior
// to support other segment types
@ -958,6 +1187,7 @@ export class SceneEntities {
await kclManager.executeAstMock(mod.modifiedAst)
await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
@ -1043,11 +1273,8 @@ export class SceneEntities {
? new Vector2(profileStart.position.x, profileStart.position.y)
: _intersection2d
const group = getParentGroup(object, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
if (!group) return
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
const varDecIndex = pathToNode[1][0]
@ -1065,7 +1292,7 @@ export class SceneEntities {
group.userData.from[0],
group.userData.from[1],
]
const to: [number, number] = [intersection2d.x, intersection2d.y]
const dragTo: [number, number] = [intersection2d.x, intersection2d.y]
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
const _node = getNodeFromPath<CallExpression>(
@ -1088,17 +1315,59 @@ export class SceneEntities {
modded = updateStartProfileAtArgs({
node: modifiedAst,
pathToNode,
to,
from,
input: {
type: 'straight-segment',
to: dragTo,
from,
},
previousProgramMemory: kclManager.programMemory,
})
} else if (
group.name === CIRCLE_SEGMENT &&
// !subGroup treats grabbing the outer circumference of the circle
// as a drag of the center handle
(!subGroup || subGroup?.name === ARROWHEAD)
) {
// is dragging the radius handle
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
[node.start, node.end],
{
type: 'arc-segment',
from,
center: group.userData.center,
radius: Math.sqrt(
(group.userData.center[0] - dragTo[0]) ** 2 +
(group.userData.center[0] - dragTo[0]) ** 2
),
}
)
} else if (
group.name === CIRCLE_SEGMENT &&
subGroup?.name === CIRCLE_CENTER_HANDLE
) {
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
[node.start, node.end],
{
type: 'arc-segment',
from,
center: dragTo,
radius: group.userData.radius,
}
)
} else {
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
[node.start, node.end],
to,
from
{
type: 'straight-segment',
from,
to: dragTo,
}
)
}
if (trap(modded)) return
@ -1161,7 +1430,7 @@ export class SceneEntities {
)
)
sceneInfra.overlayCallbacks(callBacks)
})()
})().catch(reportRejection)
}
/**
@ -1216,6 +1485,20 @@ export class SceneEntities {
group,
scale: factor,
})
} else if (
type === CIRCLE_SEGMENT &&
'center' in segment &&
'radius' in segment
) {
return this.updateCircleSegment({
prevSegment: sgPaths[index - 1],
from: segment.from,
to: segment.to,
center: segment.center,
radius: segment.radius,
group,
scale: factor,
})
} else if (type === PROFILE_START) {
group.position.set(segment.from[0], segment.from[1], 0)
group.scale.set(factor, factor, factor)
@ -1241,6 +1524,9 @@ export class SceneEntities {
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (!prevSegment) {
console.trace('prevSegment is undefined')
}
const previousPoint =
prevSegment?.type === 'TangentialArcTo'
@ -1336,6 +1622,111 @@ export class SceneEntities {
angle,
})
}
updateCircleSegment({
prevSegment,
from,
to,
center,
radius,
group,
scale = 1,
}: {
prevSegment: SketchGroup['value'][number]
from: [number, number]
to: [number, number]
center: [number, number]
radius: number
group: Group
scale?: number
}): () => SegmentOverlayPayload | null {
group.userData.from = from
group.userData.to = to
group.userData.center = center
group.userData.radius = radius
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const circleCenterHandle = group.getObjectByName(
CIRCLE_CENTER_HANDLE
) as Group
const pxLength = (2 * radius * Math.PI) / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) {
arrowGroup.position.set(
center[0] + Math.cos(Math.PI / 4) * radius,
center[1] + Math.sin(Math.PI / 4) * radius,
0
)
const arrowheadAngle = Math.PI / 4
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
if (circleCenterHandle) {
circleCenterHandle.position.set(center[0], center[1], 0)
circleCenterHandle.scale.set(scale, scale, scale)
circleCenterHandle.visible = isHandlesVisible
}
const circleSegmentBody = group.children.find(
(child) => child.userData.type === CIRCLE_SEGMENT_BODY
) as Mesh
if (circleSegmentBody) {
const newGeo = createArcGeometry({
radius,
center,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
scale,
})
circleSegmentBody.geometry = newGeo
}
const circleSegmentBodyDashed = group.children.find(
(child) => child.userData.type === CIRCLE_SEGMENT_DASH
) as Mesh
if (circleSegmentBodyDashed) {
// consider throttling the whole updateTangentialArcToSegment
// if there are more perf considerations going forward
this.throttledUpdateDashedArcGeo({
// ...arcInfo,
center,
radius,
ccw: true,
// make the start end where the handle is
startAngle: Math.PI * 0.25,
endAngle: Math.PI * 2.25,
mesh: circleSegmentBodyDashed,
isDashed: true,
scale,
})
}
return () =>
sceneInfra.updateOverlayDetails({
arrowGroup,
group,
isHandlesVisible,
from: to,
to: [center[0], center[1]],
angle: Math.PI / 4,
})
}
throttledUpdateDashedArcGeo = throttle(
(
args: Parameters<typeof createArcGeometry>[0] & {
@ -1470,7 +1861,7 @@ export class SceneEntities {
}
private _tearDownSketch(
callDepth = 0,
resolve: (val: unknown) => void,
resolve: any,
reject: () => void,
{ removeAxis = true }: { removeAxis?: boolean }
) {
@ -1500,7 +1891,7 @@ export class SceneEntities {
this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis })
}, delay)
} else {
reject()
resolve(true)
}
}
sceneInfra.camControls.enableRotate = true
@ -1512,7 +1903,7 @@ export class SceneEntities {
removeAxis = true,
}: {
removeAxis?: boolean
} = {}) {
} = {}): Promise<void | Error> {
// I think promisifying this is mostly a side effect of not having
// "setupSketch" correctly capture a promise when it's done
// so we're effectively waiting for to be finished setting up the scene just to tear it down
@ -1530,11 +1921,10 @@ export class SceneEntities {
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,
])
const parent = getParentGroup(
selected,
SEGMENT_BODIES_PLUS_PROFILE_START
)
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
if (trap(updatedAst)) return
@ -1578,6 +1968,16 @@ export class SceneEntities {
group: parent,
scale: factor,
})
} else if (parent.name === CIRCLE_SEGMENT) {
this.updateCircleSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
center: parent.userData.center,
radius: parent.userData.radius,
group: parent,
scale: factor,
})
}
return
}
@ -1585,11 +1985,10 @@ export class SceneEntities {
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([[0, 0]])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const parent = getParentGroup(
selected,
SEGMENT_BODIES_PLUS_PROFILE_START
)
if (parent) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -1613,6 +2012,16 @@ export class SceneEntities {
group: parent,
scale: factor,
})
} else if (parent.name === CIRCLE_SEGMENT) {
this.updateCircleSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
center: parent.userData.center,
radius: parent.userData.radius,
group: parent,
scale: factor,
})
}
}
const isSelected = parent?.userData?.isSelected
@ -1775,7 +2184,7 @@ function prepareTruncatedMemoryAndAst(
export function getParentGroup(
object: any,
stopAt: string[] = [STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT]
stopAt: string[] = SEGMENT_BODIES
): Group | null {
if (stopAt.includes(object?.userData?.type)) {
return object
@ -1822,10 +2231,7 @@ function colorSegment(object: any, color: number) {
})
return
}
const straightSegmentBody = getParentGroup(object, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
])
const straightSegmentBody = getParentGroup(object, SEGMENT_BODIES)
if (straightSegmentBody) {
straightSegmentBody.traverse((child) => {
if (child instanceof Mesh && !child.userData.ignoreColorChange) {

View File

@ -111,21 +111,21 @@ export class SceneInfra {
}
extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' }
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onDragStartCallback: (arg: OnDragCallbackArgs) => any = () => {}
onDragEndCallback: (arg: OnDragCallbackArgs) => any = () => {}
onDragCallback: (arg: OnDragCallbackArgs) => any = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => any = () => {}
onClickCallback: (arg: OnClickCallbackArgs) => any = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => any = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => any = () => {}
setCallbacks = (callbacks: {
onDragStart?: (arg: OnDragCallbackArgs) => void
onDragEnd?: (arg: OnDragCallbackArgs) => void
onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg: OnClickCallbackArgs) => void
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
onDragStart?: (arg: OnDragCallbackArgs) => any
onDragEnd?: (arg: OnDragCallbackArgs) => any
onDrag?: (arg: OnDragCallbackArgs) => any
onMove?: (arg: OnMoveCallbackArgs) => any
onClick?: (arg: OnClickCallbackArgs) => any
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => any
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => any
}) => {
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback

View File

@ -24,6 +24,10 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import {
CIRCLE_CENTER_HANDLE,
CIRCLE_SEGMENT,
CIRCLE_SEGMENT_BODY,
CIRCLE_SEGMENT_DASH,
EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX,
HIDE_SEGMENT_LENGTH,
@ -46,7 +50,7 @@ import {
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { roundOff } from 'lib/utils'
export function profileStart({
export function createProfileStartHandle({
from,
id,
pathToNode,
@ -225,6 +229,28 @@ function createArrowhead(scale = 1, theme: Themes, color?: number): Group {
arrowGroup.scale.set(scale, scale, scale)
return arrowGroup
}
function createCircleCenterHandle(
scale = 1,
theme: Themes,
color?: number
): Group {
const circleCenterGroup = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const baseColor = getThemeColorForThreeJs(theme)
const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body)
circleCenterGroup.add(mesh)
circleCenterGroup.userData = {
type: CIRCLE_CENTER_HANDLE,
baseColor,
}
circleCenterGroup.name = CIRCLE_CENTER_HANDLE
circleCenterGroup.scale.set(scale, scale, scale)
return circleCenterGroup
}
function createExtraSegmentHandle(
scale: number,
@ -300,6 +326,86 @@ function createLengthIndicator({
return lengthIndicatorGroup
}
export function circleSegment({
prevSegment,
from,
to,
center,
radius,
id,
pathToNode,
isDraftSegment,
scale = 1,
texture,
theme,
isSelected,
}: {
prevSegment: SketchGroup['value'][number]
from: Coords2d
center: Coords2d
radius: number
to: Coords2d
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
texture: Texture
theme: Themes
isSelected?: boolean
}): Group {
const group = new Group()
const geometry = createArcGeometry({
center,
radius,
startAngle: 0,
endAngle: Math.PI * 2,
ccw: true,
isDashed: isDraftSegment,
scale,
})
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
const body = new MeshBasicMaterial({ color })
const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment
? CIRCLE_SEGMENT_DASH
: CIRCLE_SEGMENT_BODY
group.userData = {
type: CIRCLE_SEGMENT,
id,
from,
to,
radius,
center,
ccw: true,
prevSegment,
pathToNode,
isSelected,
baseColor,
}
group.name = CIRCLE_SEGMENT
const arrowGroup = createArrowhead(scale, theme, color)
arrowGroup.position.set(
center[0] + Math.cos(Math.PI / 4) * radius,
center[1] + Math.sin(Math.PI / 4) * radius,
0
)
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
circleCenterGroup.position.set(center[0], center[1], 0)
const arrowheadAngle = Math.PI / 4
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
group.add(mesh, arrowGroup, circleCenterGroup)
return group
}
export function tangentialArcToSegment({
prevSegment,
from,

View File

@ -6,6 +6,9 @@
grid-template-columns: 1fr auto 1fr;
user-select: none;
-webkit-user-select: none;
}
.header.desktopApp {
/* Make the header act as a handle to drag the electron app window,
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
* all interactive elements opt-out of this behavior by default in src/index.css

View File

@ -6,6 +6,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -32,7 +33,9 @@ export const AppHeader = ({
className={
'w-full grid ' +
styles.header +
' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
` ${
isDesktop() ? styles.desktopApp + ' ' : ''
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
className
}
style={style}

View File

@ -151,6 +151,7 @@ export function useCalc({
})
if (trap(error)) return
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
executeAst({
ast,
engineCommandManager,

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { throttle, isReducedMotion } from 'lib/utils'
import { reportRejection } from 'lib/trap'
const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
@ -16,8 +17,8 @@ export const CamToggle = () => {
useEffect(() => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
async () => {
sceneInfra.camControls.dollyZoom(fov)
() => {
sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
}
)
}, [])
@ -26,11 +27,11 @@ export const CamToggle = () => {
if (isPerspective) {
isReducedMotion()
? sceneInfra.camControls.useOrthographicCamera()
: sceneInfra.camControls.animateToOrthographic()
: sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
} else {
isReducedMotion()
? sceneInfra.camControls.usePerspectiveCamera()
: sceneInfra.camControls.animateToPerspective()
? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
: sceneInfra.camControls.animateToPerspective().catch(reportRejection)
}
setIsPerspective(!isPerspective)
}

View File

@ -71,6 +71,17 @@ function CommandArgOptionInput({
inputRef.current?.focus()
inputRef.current?.select()
}, [inputRef])
useEffect(() => {
// work around to make sure the user doesn't have to press the down arrow key to focus the first option
// instead this makes it move from the first hit
const downArrowEvent = new KeyboardEvent('keydown', {
key: 'ArrowDown',
keyCode: 40,
which: 40,
bubbles: true,
})
inputRef?.current?.dispatchEvent(downArrowEvent)
}, [])
// Filter the options based on the query,
// resetting the query when the options change

View File

@ -1,53 +1,43 @@
import { useMachine } from '@xstate/react'
import { createActorContext } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext, useEffect } from 'react'
import { EventFrom, StateFrom } from 'xstate'
import { useEffect } from 'react'
type CommandsContextType = {
commandBarState: StateFrom<typeof commandBarMachine>
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
}
export const CommandsContext = createContext<CommandsContextType>({
commandBarState: commandBarMachine.initialState,
commandBarSend: () => {},
})
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
devTools: true,
export const CommandsContext = createActorContext(
commandBarMachine.provide({
guards: {
'Command has no arguments': (context, _event) => {
'Command has no arguments': ({ context }) => {
return (
!context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0
)
},
'All arguments are skippable': (context, _event) => {
'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
},
})
)
useEffect(() => {
editorManager.setCommandBarSend(commandBarSend)
})
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
return (
<CommandsContext.Provider
value={{
commandBarState,
commandBarSend,
}}
>
{children}
<CommandsContext.Provider>
<CommandBarProviderInner>{children}</CommandBarProviderInner>
</CommandsContext.Provider>
)
}
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
const commandBarActor = CommandsContext.useActorRef()
useEffect(() => {
editorManager.setCommandBarSend(commandBarActor.send)
})
return children
}

View File

@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
e.preventDefault()
commandBarSend({
type: 'Submit command',
data: argumentsToSubmit,
output: argumentsToSubmit,
})
}

View File

@ -9,7 +9,7 @@ import {
getSelectionTypeDisplayText,
} from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const initSelectionsByType = useCallback(() => {
const selectionsByType = useMemo(() => {
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length
? 'none'
: getSelectionType(selection)
}, [selection, code])
const selectionsByType = initSelectionsByType()
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
canSubmitSelectionArg(selectionsByType, arg)
const canSubmitSelection = useMemo<boolean>(
() => canSubmitSelectionArg(selectionsByType, arg),
[selectionsByType]
)
useEffect(() => {
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
// Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already
useEffect(() => {
console.log('selection input effect', {
selectionsByType,
canSubmitSelection,
arg,
})
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
if (canSubmitSelection && arg.skip && argValue === undefined) {
handleSubmit({
preventDefault: () => {},
} as React.FormEvent<HTMLFormElement>)
handleSubmit()
}
}, [selectionsByType, arg])
}, [canSubmitSelection])
function handleChange() {
inputRef.current?.focus()
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
if (!canSubmitSelection) {
setHasSubmitted(true)

View File

@ -11,6 +11,7 @@ export function CommandBarOpenButton() {
<button
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
onClick={() => commandBarSend({ type: 'Open' })}
data-testid="command-bar-open-button"
>
<span>Commands</span>
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">

View File

@ -1,8 +1,9 @@
import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { useState, useEffect } from 'react'
function useEngineCommands(): [CommandLog[], () => void] {
export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
engineCommandManager.commandLogs
)
@ -77,9 +78,11 @@ export const EngineCommands = () => {
/>
<button
data-testid="custom-cmd-send-button"
onClick={() =>
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
}
onClick={() => {
engineCommandManager
.sendSceneCommand(JSON.parse(customCmd))
.catch(reportRejection)
}}
>
Send custom command
</button>

View File

@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
import React, { createContext } from 'react'
import { toast } from 'react-hot-toast'
import {
Actor,
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
Prop,
StateFrom,
assign,
fromPromise,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
send: Prop<Actor<T>, 'send'>
}
export const FileContext = createContext(
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
const { commandBarSend } = useCommandsContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
context: {
project,
selectedDirectory: project,
},
actions: {
navigateToFile: (context, event) => {
if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
window.electron.path.sep +
event.data.name
)}`
const [state, send] = useMachine(
fileMachine.provide({
actions: {
renameToastSuccess: ({ event }) => {
if (event.type !== 'xstate.done.actor.rename-file') return
toast.success(event.output.message)
},
createToastSuccess: ({ event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
toast.success(event.output.message)
},
toastSuccess: ({ event }) => {
if (
event.type !== 'xstate.done.actor.rename-file' &&
event.type !== 'xstate.done.actor.delete-file'
)
} else if (
event.data &&
'path' in event.data &&
event.data.path.endsWith(FILE_EXT)
) {
// Don't navigate to newly created directories
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
}
return
toast.success(event.output.message)
},
toastError: ({ event }) => {
if (event.type !== 'xstate.done.actor.rename-file') return
toast.error(event.output.message)
},
navigateToFile: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file') return
if (event.output && 'name' in event.output) {
commandBarSend({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
window.electron.path.sep +
event.output.name
)}`
)
} else if (
event.output &&
'path' in event.output &&
event.output.path.endsWith(FILE_EXT)
) {
// Don't navigate to newly created directories
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
}
},
},
addFileToRenamingQueue: assign({
itemsBeingRenamed: (context, event) => [
...context.itemsBeingRenamed,
event.data.path,
],
}),
removeFileFromRenamingQueue: assign({
itemsBeingRenamed: (
context,
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
) =>
context.itemsBeingRenamed.filter(
(path) => path !== event.data.oldPath
),
}),
renameToastSuccess: (_, event) => toast.success(event.data.message),
createToastSuccess: (_, event) => toast.success(event.data.message),
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isDesktop()
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,
children: newFiles,
}
},
createAndOpenFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
},
createFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
path: createdPath,
}
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
const name = newName
? newName.endsWith(FILE_EXT) || isDir
? newName
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
context.selectedDirectory.path,
oldName
)
const newPath = window.electron.path.join(
context.selectedDirectory.path,
name
)
// no-op
if (oldPath === newPath) {
actors: {
readFiles: fromPromise(async ({ input }) => {
const newFiles =
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
[]
return {
message: `Old is the same as new.`,
...input,
children: newFiles,
}
}),
createAndOpenFile: fromPromise(async ({ input }) => {
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, input.content ?? '')
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
}),
createFile: fromPromise(async ({ input }) => {
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, input.content ?? '')
}
return {
path: createdPath,
}
}),
renameFile: fromPromise(async ({ input }) => {
const { oldName, newName, isDir } = input
const name = newName
? newName.endsWith(FILE_EXT) || isDir
? newName
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
input.selectedDirectory.path,
oldName
)
const newPath = window.electron.path.join(
input.selectedDirectory.path,
name
)
// no-op
if (oldPath === newPath) {
return {
message: `Old is the same as new.`,
newPath,
oldPath,
}
}
// if there are any siblings with the same name, report error.
const entries = await window.electron.readdir(
window.electron.path.dirname(newPath)
)
for (let entry of entries) {
if (entry === newName) {
return Promise.reject(new Error('Filename already exists.'))
}
}
window.electron.rename(oldPath, newPath)
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
if (oldPath === file.path && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
} else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
file.path.replace(oldPath, newPath)
)}`
)
}
return {
message: `Successfully renamed "${oldName}" to "${name}"`,
newPath,
oldPath,
}
}
}),
deleteFile: fromPromise(async ({ input }) => {
const isDir = !!input.children
// if there are any siblings with the same name, report error.
const entries = await window.electron.readdir(
window.electron.path.dirname(newPath)
)
for (let entry of entries) {
if (entry === newName) {
return Promise.reject(new Error('Filename already exists.'))
if (isDir) {
await window.electron
.rm(input.path, {
recursive: true,
})
.catch((e) => console.error('Error deleting directory', e))
} else {
await window.electron
.rm(input.path)
.catch((e) => console.error('Error deleting file', e))
}
}
window.electron.rename(oldPath, newPath)
// If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
const entries = await window.electron.readdir(project.path)
const hasKclEntries =
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return {
message: 'No more files in project, created main.kcl',
}
}
if (oldPath === file.path && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
} else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
file.path.replace(oldPath, newPath)
)}`
)
}
// If we just deleted the current file or one of its parent directories,
// navigate to the project root
if (
(input.path === file?.path || file?.path.includes(input.path)) &&
project?.path
) {
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
}
return {
message: `Successfully renamed "${oldName}" to "${name}"`,
newPath,
oldPath,
}
return {
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
input.name
}"`,
}
}),
},
deleteFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Delete file'>
) => {
const isDir = !!event.data.children
if (isDir) {
await window.electron
.rm(event.data.path, {
recursive: true,
})
.catch((e) => console.error('Error deleting directory', e))
} else {
await window.electron
.rm(event.data.path)
.catch((e) => console.error('Error deleting file', e))
}
// If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
const entries = await window.electron.readdir(project.path)
const hasKclEntries =
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return
}
// If we just deleted the current file or one of its parent directories,
// navigate to the project root
if (
(event.data.path === file?.path ||
file?.path.includes(event.data.path)) &&
project?.path
) {
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`
}),
{
input: {
project,
selectedDirectory: project,
},
},
guards: {
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
'Is not silent': (_, event) => !event.data?.silent,
},
})
}
)
return (
<FileContext.Provider

View File

@ -176,13 +176,12 @@ const FileTreeItem = ({
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
codeManager.code
)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true)
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)
@ -246,13 +245,13 @@ const FileTreeItem = ({
onClickCapture={(e) =>
fileSend({
type: 'Set selected directory',
data: fileOrDir,
directory: fileOrDir,
})
}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
data: fileOrDir,
directory: fileOrDir,
})
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
@ -299,13 +298,13 @@ const FileTreeItem = ({
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
data: fileOrDir,
directory: fileOrDir,
})
}}
onFocusCapture={(e) =>
fileSend({
type: 'Set selected directory',
data: fileOrDir,
directory: fileOrDir,
})
}
>
@ -391,14 +390,14 @@ interface FileTreeProps {
export const FileTreeMenu = () => {
const { send } = useFileContext()
async function createFile() {
function createFile() {
send({
type: 'Create file',
data: { name: '', makeDir: false },
})
}
async function createFolder() {
function createFolder() {
send({
type: 'Create file',
data: { name: '', makeDir: true },
@ -485,7 +484,7 @@ export const FileTreeInner = ({
onClickCapture={(e) => {
fileSend({
type: 'Set selected directory',
data: fileContext.project,
directory: fileContext.project,
})
}}
>

View File

@ -27,6 +27,7 @@ import {
} from './ContextMenu'
import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
@ -67,7 +68,9 @@ export default function Gizmo() {
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}}
>
{axisSemantic} view
@ -75,7 +78,7 @@ export default function Gizmo() {
)),
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}}
>
Reset view
@ -299,7 +302,7 @@ const initializeMouseEvents = (
const handleClick = () => {
if (raycasterIntersect.current) {
const axisName = raycasterIntersect.current.object.name as AxisNames
sceneInfra.camControls.updateCameraToAxis(axisName)
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
}
}

View File

@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) {
navigate(filePath + PATHS.ONBOARDING.INDEX)
} else {
createAndOpenNewProject({ onProjectOpen, navigate })
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
reportRejection
)
}
}}
>

View File

@ -11,6 +11,8 @@ import {
import { engineCommandManager } from '../lib/singletons'
import { Spinner } from './Spinner'
const Loading = ({ children }: React.PropsWithChildren) => {
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
@ -65,17 +67,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
className="body-bg flex flex-col items-center justify-center h-screen"
data-testid="loading"
>
<svg viewBox="0 0 10 10" className="w-8 h-8">
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
<Spinner />
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
<p
className={

View File

@ -11,6 +11,8 @@ import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator'
import { reportRejection } from 'lib/trap'
export function LowerRightControls({
children,
@ -24,7 +26,7 @@ export function LowerRightControls({
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
async function reportbug(event: {
function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
@ -33,7 +35,9 @@ export function LowerRightControls({
if (!coreDumpManager) {
// open default reporting option
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else {
toast
.promise(
@ -55,7 +59,7 @@ export function LowerRightControls({
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
)
).catch(reportRejection)
}
})
}
@ -65,6 +69,7 @@ export function LowerRightControls({
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`

View File

@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
// Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService.
// When they do we can delete this.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.updateFoldingRanges()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.requestSemanticTokens()
break
case 'kcl/memoryUpdated':

View File

@ -0,0 +1,45 @@
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 '
let icon = <Spinner className={className} />
let dataTestId = 'model-state-indicator'
if (lastCommandType === 'receive-reliable') {
className +=
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = (
<CustomIcon
data-testid={dataTestId + '-receive-reliable'}
name="checkmark"
/>
)
} else if (lastCommandType === 'execution-done') {
className +=
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = (
<CustomIcon
data-testid={dataTestId + '-execution-done'}
name="checkmark"
/>
)
} else if (lastCommandType === 'export-done') {
className +=
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
icon = (
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
)
}
return (
<div className={className} data-testid="model-state-indicator">
{icon}
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
{convertToVarEnabled && (
<Menu.Item>
<button
onClick={() => handleConvertToVarClick()}
onClick={() => {
handleConvertToVarClick().catch(reportRejection)
}}
className={styles.button}
>
<span>Convert to Variable</span>

View File

@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
icon: 'printer3d',
iconClassName: '!p-0',
keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
commandBarSend({
type: 'Find and select command',

View File

@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
import { useNetworkContext } from '../hooks/useNetworkContext'
import { NetworkHealthState } from '../hooks/useNetworkStatus'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected',
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
</div>
{issues[name as ConnectingTypeGroup] && (
<button
onClick={async () => {
onClick={toSync(async () => {
await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || ''
)
setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000)
}}
}, reportRejection)}
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
>
{hasCopied ? 'Copied' : 'Copy Error'}

View File

@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'lib/project'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
function ProjectCard({
project,
@ -165,10 +167,10 @@ function ProjectCard({
{isConfirmingDelete && (
<DeleteConfirmationDialog
title="Delete Project"
onConfirm={async () => {
onConfirm={toSync(async () => {
await handleDeleteProject(project)
setIsConfirmingDelete(false)
}}
}, reportRejection)}
onDismiss={() => setIsConfirmingDelete(false)}
>
<p className="my-4">

View File

@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
import toast from 'react-hot-toast'
import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
// Window may not be available in some environments
window?.location.reload()
})
.catch(reportRejection)
}
return (
<button
onClick={refresh}
onClick={toSync(refresh, reportRejection)}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
>
<CustomIcon name="exclamationMark" className="w-5 h-5" />

View File

@ -20,6 +20,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel
@ -54,7 +56,7 @@ export const AllSettingsFields = forwardRef(
)
: undefined
async function restartOnboarding() {
function restartOnboarding() {
send({
type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' },
@ -82,6 +84,7 @@ export const AllSettingsFields = forwardRef(
}
}
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigateToOnboardingStart()
}, [isFileSettings, navigate, state])
@ -190,7 +193,7 @@ export const AllSettingsFields = forwardRef(
{isDesktop() && (
<ActionButton
Element="button"
onClick={async () => {
onClick={toSync(async () => {
const paths = await getSettingsFolderPaths(
projectPath ? decodeURIComponent(projectPath) : undefined
)
@ -199,7 +202,7 @@ export const AllSettingsFields = forwardRef(
return new Error('finalPath undefined')
}
window.electron.showInFolder(finalPath)
}}
}, reportRejection)}
iconStart={{
icon: 'folder',
size: 'sm',
@ -211,14 +214,14 @@ export const AllSettingsFields = forwardRef(
)}
<ActionButton
Element="button"
onClick={async () => {
onClick={toSync(async () => {
const defaultDirectory = await getInitialDefaultDir()
send({
type: 'Reset settings',
defaultDirectory,
})
toast.success('Settings restored to default')
}}
}, reportRejection)}
iconStart={{
icon: 'refresh',
size: 'sm',

View File

@ -8,7 +8,7 @@ import {
} from 'lib/settings/settingsTypes'
import { getSettingInputType } from 'lib/settings/settingsUtils'
import { useMemo } from 'react'
import { Event } from 'xstate'
import { EventFrom } from 'xstate'
interface SettingsFieldInputProps {
// We don't need the fancy types here,
@ -59,7 +59,7 @@ export function SettingsFieldInput({
level: settingsLevel,
value: newValue,
},
} as unknown as Event<WildcardSetEvent>)
} as unknown as EventFrom<WildcardSetEvent>)
}}
/>
)
@ -103,7 +103,7 @@ export function SettingsFieldInput({
level: settingsLevel,
value: e.target.value,
},
} as unknown as Event<WildcardSetEvent>)
} as unknown as EventFrom<WildcardSetEvent>)
}
>
{options &&
@ -137,7 +137,7 @@ export function SettingsFieldInput({
level: settingsLevel,
value: e.target.value,
},
} as unknown as Event<WildcardSetEvent>)
} as unknown as EventFrom<WildcardSetEvent>)
}
}}
/>

View File

@ -14,13 +14,7 @@ import {
Themes,
} from 'lib/theme'
import decamelize from 'decamelize'
import {
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { isDesktop } from 'lib/isDesktop'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
@ -39,7 +33,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
send: Prop<Actor<T>, 'send'>
}
type SettingsAuthContextType = {
@ -50,7 +44,7 @@ type SettingsAuthContextType = {
// 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
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
export const getSettingsState = () => settingsStateRef
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -101,21 +95,20 @@ export const SettingsAuthProviderBase = ({
const { commandBarSend } = useCommandsContext()
const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine,
{
context: loadedSettings,
settingsMachine.provide({
actions: {
//TODO: batch all these and if that's difficult to do from tsx,
// make it easy to do
setClientSideSceneUnits: (context, event) => {
setClientSideSceneUnits: ({ context, event }) => {
const newBaseUnit =
event.type === 'set.modeling.defaultUnit'
? (event.data.value as BaseUnit)
: context.modeling.defaultUnit.current
sceneInfra.baseUnit = newBaseUnit
},
setEngineTheme: (context) => {
setEngineTheme: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
@ -126,6 +119,7 @@ export const SettingsAuthProviderBase = ({
})
const opposingTheme = getOppositeTheme(context.app.theme.current)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
@ -135,16 +129,17 @@ export const SettingsAuthProviderBase = ({
},
})
},
setEngineScaleGridVisibility: (context) => {
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: (context) => {
setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme
},
setEngineEdges: (context) => {
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
@ -154,7 +149,8 @@ export const SettingsAuthProviderBase = ({
},
})
},
toastSuccess: (_, event) => {
toastSuccess: ({ event }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [
keyof typeof settings,
string
@ -176,7 +172,7 @@ export const SettingsAuthProviderBase = ({
id: `${event.type}.success`,
})
},
'Execute AST': (context, event) => {
'Execute AST': ({ context, event }) => {
try {
const allSettingsIncludesUnitChange =
event.type === 'Set all settings' &&
@ -193,10 +189,8 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange
) {
// Unit changes requires a re-exec of code
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true)
} else {
// For any future logging we'd like to do
// console.log(
@ -207,12 +201,13 @@ export const SettingsAuthProviderBase = ({
console.error('Error executing AST after settings change', e)
}
},
persistSettings: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
saveSettings(context, loadedProject?.project?.path)
},
},
services: {
'Persist settings': (context) =>
saveSettings(context, loadedProject?.project?.path),
},
}
}),
{ input: loadedSettings }
)
settingsStateRef = settingsState.context
@ -295,19 +290,22 @@ export const SettingsAuthProviderBase = ({
}, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(authMachine, {
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
logout()
const [authState, authSend, authActor] = useMachine(
authMachine.provide({
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
})
})
)
useStateMachineCommands({
machineId: 'auth',
@ -339,13 +337,11 @@ export const SettingsAuthProviderBase = ({
export default SettingsAuthProvider
export function logout() {
export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
return (
!isDesktop() &&
fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -0,0 +1,17 @@
import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
<circle
cx="5"
cy="5"
r="4"
stroke="var(--primary)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
)
}

View File

@ -53,13 +53,12 @@ export const Stream = () => {
* executed. If we can find a way to do this from a more
* central place, we can move this code there.
*/
async function executeCodeAndPlayStream() {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => {
function executeCodeAndPlayStream() {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode(true).then(async () => {
await videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
kclManager.isFirstRender = false
setStreamState(StreamState.Playing)
})
}
@ -219,15 +218,15 @@ export const Stream = () => {
* Play the vid
*/
useEffect(() => {
if (!kclManager.isFirstRender) {
setTimeout(() =>
if (!kclManager.isExecuting) {
setTimeout(() => {
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
)
})
}
}, [kclManager.isFirstRender])
}, [kclManager.isExecuting])
useEffect(() => {
if (
@ -289,9 +288,10 @@ export const Stream = () => {
},
})
if (state.matches('Sketch')) return
if (state.matches('idle.showPlanes')) return
if (state.matches({ idle: 'showPlanes' })) return
if (!context.store?.didDragInStream && btnName(e).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(
e,
videoRef.current,
@ -382,15 +382,15 @@ export const Stream = () => {
</div>
</div>
)}
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
{(!isNetworkOkay || isLoading) && (
<div className="text-center absolute inset-0">
<Loading>
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
{!isNetworkOkay && !isLoading ? (
<span data-testid="loading-stream">Stream disconnected...</span>
) : !isLoading && kclManager.isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span>
) : (
<span data-testid="loading-stream">Loading stream...</span>
!isLoading && (
<span data-testid="loading-stream">Loading stream...</span>
)
)}
</Loading>
</div>

View File

@ -26,8 +26,9 @@ import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine'
import { EventData, EventFrom } from 'xstate'
import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'
const CANVAS_SIZE = 128
const PROMPT_TRUNCATE_LENGTH = 128
@ -45,7 +46,7 @@ export function ToastTextToCadError({
prompt: string
commandBarSend: (
event: EventFrom<typeof commandBarMachine>,
data?: EventData
data?: unknown
) => void
}) {
return (
@ -112,7 +113,7 @@ export function ToastTextToCadSuccess({
token?: string
fileMachineSend: (
event: EventFrom<typeof fileMachine>,
data?: EventData
data?: unknown
) => void
settings: {
theme: Themes
@ -297,7 +298,7 @@ export function ToastTextToCadSuccess({
name={hasCopied ? 'Close' : 'Reject'}
onClick={() => {
if (!hasCopied) {
sendTelemetry(modelId, 'rejected', token)
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
}
if (isDesktop()) {
// Delete the file from the project
@ -323,6 +324,7 @@ export function ToastTextToCadSuccess({
}}
name="Accept"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token)
navigate(
`${PATHS.FILE}/${encodeURIComponent(
@ -342,7 +344,9 @@ export function ToastTextToCadSuccess({
}}
name="Copy to clipboard"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigator.clipboard.writeText(data.code || '// no code found')
setShowCopiedUi(true)
setHasCopied(true)

View File

@ -10,10 +10,10 @@ import {
transformSecondarySketchLinesTagFirst,
getTransformInfos,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { TransformInfo } from 'lang/std/stdTypes'
export function equalAngleInfo({
selectionRanges,

View File

@ -10,8 +10,8 @@ import {
transformSecondarySketchLinesTagFirst,
getTransformInfos,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'

View File

@ -9,8 +9,8 @@ import {
PathToNodeMap,
getTransformInfos,
transformAstSketchLines,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'

View File

@ -11,8 +11,8 @@ import {
transformSecondarySketchLinesTagFirst,
getTransformInfos,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'

View File

@ -9,8 +9,8 @@ import {
PathToNodeMap,
getRemoveConstraintsTransforms,
transformAstSketchLines,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'

View File

@ -9,8 +9,8 @@ import {
getTransformInfos,
transformAstSketchLines,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import {
SetAngleLengthModal,
createSetAngleLengthModal,

View File

@ -10,8 +10,8 @@ import {
transformSecondarySketchLinesTagFirst,
getTransformInfos,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'

View File

@ -9,8 +9,8 @@ import {
transformSecondarySketchLinesTagFirst,
getTransformInfos,
PathToNodeMap,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'

View File

@ -9,8 +9,8 @@ import {
PathToNodeMap,
getTransformInfos,
transformAstSketchLines,
TransformInfo,
} from '../../lang/std/sketchcombos'
import { TransformInfo } from 'lang/std/stdTypes'
import {
SetAngleLengthModal,
createSetAngleLengthModal,

View File

@ -12,6 +12,7 @@ interface TooltipProps extends React.PropsWithChildren {
position?: TooltipPosition
wrapperClassName?: string
contentClassName?: string
wrapperStyle?: React.CSSProperties
delay?: number
hoverOnly?: boolean
inert?: boolean
@ -22,6 +23,7 @@ export default function Tooltip({
position = 'top',
wrapperClassName: className,
contentClassName,
wrapperStyle = {},
delay = 200,
hoverOnly = false,
inert = true,
@ -36,7 +38,10 @@ export default function Tooltip({
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
styles[position]
} ${className}`}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
style={Object.assign(
{ '--_delay': delay + 'ms' } as React.CSSProperties,
wrapperStyle
)}
>
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
{children}

View File

@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
Element: 'button',
'data-testid': 'user-sidebar-sign-out',
children: 'Sign out',
onClick: () => send('Log out'),
onClick: () => send({ type: 'Log out' }),
className: '', // Just making TS's filter type coercion happy 😠
},
].filter(

View File

@ -1,7 +1,7 @@
import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
@ -11,6 +11,7 @@ import {
forEachDiagnostic,
setDiagnosticsEffect,
} from '@codemirror/lint'
import { StateFrom } from 'xstate'
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
@ -38,7 +39,7 @@ export default class EditorManager {
private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingEvent: ModelingMachineEvent | null = null
private _modelingState: StateFrom<typeof modelingMachine> | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {}
@ -80,8 +81,8 @@ export default class EditorManager {
this._modelingSend = send
}
set modelingEvent(event: ModelingMachineEvent) {
this._modelingEvent = event
set modelingState(state: StateFrom<typeof modelingMachine>) {
this._modelingState = state
}
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
@ -248,13 +249,11 @@ export default class EditorManager {
return
}
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
if (!this._modelingEvent) {
if (!this._modelingState) {
return
}
if (ignoreEvents.includes(this._modelingEvent.type)) {
if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
return
}
@ -286,8 +285,9 @@ export default class EditorManager {
this._lastEvent = { event: stringEvent, time: Date.now() }
this._modelingSend(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) =>
eventInfo.engineEvents.forEach((event) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand(event)
)
})
}
}

View File

@ -36,6 +36,7 @@ import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompleti
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { editorManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
const copilotPluginAnnotation = Annotation.define<boolean>()
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
if (!this.client.ready) return
try {
this.requestCompletions()
this.requestCompletions().catch(reportRejection)
} catch (e) {
console.error(e)
}
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
})
this.accept(ghostText.uuid)
this.accept(ghostText.uuid).catch(reportRejection)
return true
}
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
],
})
this.reject()
this.reject().catch(reportRejection)
return false
}

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