Compare commits

...

59 Commits

Author SHA1 Message Date
c03357d897 Halfway, broken impl of patch from our controls to OrbitControls 2024-09-11 14:21:24 -04:00
292f89859f Make settings reset button only reset current level (#3855)
* Update test to expect new behavior (failing)

* Update behavior to match new test expectations

* Make reset button more clear

* Fix eslint issue

* Fix up separate test that relied on old reset logic
2024-09-11 09:39:10 -04:00
a00800bddc add more shell samples (#3861)
* add more shell sampels

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-10 19:50:34 -07:00
78ceba6d20 fixing the position and display of the segment labels during sketch mode (#3796)
* bug: fixing the position and display of the segment labels during sketch mode

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

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

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

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

* fix: minor visual tweaks

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

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

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

* fix: adding border styling

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

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

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

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

* feat: aligned the text to the slope of the line drawn

* fix: tsc, lint, fmt

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

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

* fix: linter warnings for unused variable

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-10 16:22:16 -05:00
6776a350af Update KNOWN-ISSUES.md 2024-09-10 13:19:49 -07:00
dd75f06f77 Add linting to codemirror-lsp-client (#3850) 2024-09-10 20:18:31 +00:00
394872d84e remove debug log (#3853) 2024-09-10 20:09:39 +00:00
f9eef6397f Do not read in the "theme" setting at the project level (#3845)
* Do not read in the "theme" setting at the project level

* Add a couple unit tests
2024-09-10 15:15:20 -04:00
900bac999c Bugfix: update sketch mode colors on theme change (#3849)
* Update client-side scene mesh base colors properly

* Add E2E test

* Remove use of `as`
2024-09-10 13:30:39 -04:00
5b2738f826 Fixing bug in convertThreeCamValuesToEngineCam (#3806)
fix: we need the up of the camera, not a hard coded up
2024-09-10 11:45:32 -05: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
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
max
7c2cfba0ac Extract updateAstAndFocus from Main Function (#3832)
refactor: pull out updateAst and focus
2024-09-09 12:15:16 +02: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
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
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
161 changed files with 13777 additions and 6245 deletions

View File

@ -2,7 +2,9 @@ NODE_ENV=development
DEV=true DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev 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_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_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" "plugin:css-modules/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"semi": [ "semi": [
"error", "error",
"never" "never"
@ -24,7 +26,6 @@
{ {
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn",
"suggest-no-throw/suggest-no-throw": "off", "suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off", "testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "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 # TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm - name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}" run: "yarn build:wasm"
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
@ -81,8 +81,6 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true 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: ${{ 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 }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
@ -142,37 +140,12 @@ jobs:
- name: List artifacts in out/ - name: List artifacts in out/
run: ls -R 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 - uses: actions/upload-artifact@v3
with: with:
name: out-${{ matrix.os }} name: out-${{ matrix.os }}
path: | path: |
out/Zoo*.* out/Zoo*.*
out/latest*.yml out/latest*.yml
out/tauri
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # 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) }} 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' }} 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' }} 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' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -212,7 +183,7 @@ jobs:
with: with:
name: out-ubuntu-22.04 name: out-ubuntu-22.04
path: out path: out
- name: Generate the download static endpoint - name: Generate the download static endpoint
run: | run: |
RELEASE_DIR=https://${WEBSITE_DIR} RELEASE_DIR=https://${WEBSITE_DIR}
@ -222,8 +193,10 @@ jobs:
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \ --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 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_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.msi" \ --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, "version": $version,
"pub_date": $pub_date, "pub_date": $pub_date,
@ -235,54 +208,22 @@ jobs:
"dmg-x64": { "dmg-x64": {
"url": $mac_x64_url "url": $mac_x64_url
}, },
"msi-arm64": { "exe-arm64": {
"url": $windows_arm64_url "url": $windows_arm64_url
}, },
"msi-x64": { "exe-x64": {
"url": $windows_x64_url "url": $windows_x64_url
},
"appimage-arm64": {
"url": $linux_arm64_url
},
"appimage-x64": {
"url": $linux_x64_url
} }
} }
}' > last_download.json }' > last_download.json
cat 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 - name: List artifacts
run: "ls -R out" run: "ls -R out"
@ -304,13 +245,31 @@ jobs:
parent: false parent: false
destination: ${{ env.BUCKET_DIR }} 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 - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.2.0
with: with:
path: out path: out
glob: 'latest*' glob: 'latest*'
parent: false parent: false
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
# 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
glob: 'latest*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload download endpoint to public bucket - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.2.0
@ -318,20 +277,6 @@ jobs:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: "out/tauri/${{ env.VERSION }}"
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@ -45,7 +45,7 @@ jobs:
- run: yarn xstate:typegen - run: yarn xstate:typegen
- run: yarn tsc - run: yarn tsc
- name: Lint - name: Lint
run: yarn eslint --max-warnings 0 src e2e run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client
check-typos: check-typos:

View File

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

View File

@ -351,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details> </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 ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/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

@ -24,6 +24,3 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently. chamfer cases work currently.
Sketching on the chamfered face does not currently work. Sketching on the chamfered face does not currently work.
- **Shell**: Shell sometimes does not work when arcs or fillets are involved.
We are tracking the engine side bug on this.

View File

@ -56,6 +56,7 @@ layout: manual
* [`line`](kcl/line) * [`line`](kcl/line)
* [`lineTo`](kcl/lineTo) * [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln) * [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log) * [`log`](kcl/log)
* [`log10`](kcl/log10) * [`log10`](kcl/log10)
* [`log2`](kcl/log2) * [`log2`](kcl/log2)
@ -63,6 +64,7 @@ layout: manual
* [`max`](kcl/max) * [`max`](kcl/max)
* [`min`](kcl/min) * [`min`](kcl/min)
* [`mm`](kcl/mm) * [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d) * [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d) * [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d) * [`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 one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -271,10 +271,7 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
}) })
// If they're open by default, we're not actually testing anything. // If they're open by default, we're not actually testing anything.
@ -302,16 +299,7 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
}) })
await test.step('All panes opened before should be visible', async () => { await test.step('All panes opened before should be visible', async () => {

View File

@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
}) })
const { const {
panesOpen, openKclCodePanel,
openFilePanel,
createAndSelectProject, createAndSelectProject,
pasteCodeInEditor, pasteCodeInEditor,
createNewFileAndSelect, createNewFileAndSelect,
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents // File the main.kcl with contents
const kclCube = await fsp.readFile( const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl', 'src/wasm-lib/tests/executor/inputs/cube.kcl',
@ -201,4 +202,78 @@ test.describe('when using the file tree to', () => {
await electronApp.close() 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) const u = await getUtils(page)
page.on('console', console.log) page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
const pointOnModel = { x: 630, y: 280 } const pointOnModel = { x: 630, y: 280 }
@ -938,16 +935,7 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
@ -972,16 +960,7 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached() await u.waitForPageLoad()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
@ -1740,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
}) })
await test.step('Rename the folder', async () => { await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000) await page.waitForTimeout(2000)
await folderToRename.click({ button: 'right' }) await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible() await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click() await renameMenuItem.click()

View File

@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
const crypticErrorText = `ApiError` const crypticErrorText = `ApiError`
await expect(page.getByText(crypticErrorText).first()).toBeVisible() 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 }) => { test('executes on load', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 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: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 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: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants' import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
type TestColor = [number, number, number] type TestColor = [number, number, number]
export const TEST_COLORS = { export const TEST_COLORS = {
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
} }
return maxDiff return maxDiff
}, },
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) => doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
new Promise(async (resolve) => { new Promise<boolean>((resolve) => {
await page.screenshot({ ;(async () => {
path: './e2e/playwright/temp1.png',
fullPage: true,
})
await fn()
const isImageDiff = async () => {
await page.screenshot({ await page.screenshot({
path: './e2e/playwright/temp2.png', path: './e2e/playwright/temp1.png',
fullPage: true, fullPage: true,
}) })
const screenshot1 = PNG.sync.read( await fn()
await fsp.readFile('./e2e/playwright/temp1.png') const isImageDiff = async () => {
) await page.screenshot({
const screenshot2 = PNG.sync.read( path: './e2e/playwright/temp2.png',
await fsp.readFile('./e2e/playwright/temp2.png') fullPage: true,
) })
const actualDiffCount = pixelMatch( const screenshot1 = PNG.sync.read(
screenshot1.data, await fsp.readFile('./e2e/playwright/temp1.png')
screenshot2.data, )
null, const screenshot2 = PNG.sync.read(
screenshot1.width, await fsp.readFile('./e2e/playwright/temp2.png')
screenshot2.height )
) const actualDiffCount = pixelMatch(
return actualDiffCount > diffCount screenshot1.data,
} screenshot2.data,
null,
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times) screenshot1.width,
let count = 0 screenshot2.height
const interval = setInterval(async () => { )
count++ return actualDiffCount > diffCount
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
} }
}, 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 ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters
@ -548,13 +553,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page const newFile = page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name }) .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[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
@ -852,10 +869,12 @@ export async function setupElectron({
testInfo, testInfo,
folderSetupFn, folderSetupFn,
cleanProjectDir = true, cleanProjectDir = true,
appSettings,
}: { }: {
testInfo: TestInfo testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) { }) {
// create or otherwise clear the folder // create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -889,15 +908,19 @@ export async function setupElectron({
if (cleanProjectDir) { if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({ const settingsOverrides = TOML.stringify(
...TEST_SETTINGS, appSettings
settings: { ? { settings: appSettings }
app: { : {
...TEST_SETTINGS.app, ...TEST_SETTINGS,
projectDirectory: projectDirName, settings: {
}, app: {
}, ...TEST_SETTINGS.app,
}) projectDirectory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides) await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
} }

View File

@ -8,7 +8,7 @@ import {
tearDown, tearDown,
executorInputPath, executorInputPath,
} from './test-utils' } from './test-utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates' import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
@ -154,29 +154,33 @@ test.describe('Testing settings', () => {
test('Project and user settings can be reset', async ({ page }) => { test('Project and user settings can be reset', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await test.step(`Setup`, async () => {
await u.waitForAuthSkipAppStart() await page.setViewportSize({ width: 1200, height: 500 })
await page await u.waitForAuthSkipAppStart()
.getByRole('button', { name: 'Start Sketch' }) })
.waitFor({ state: 'visible' })
// Selectors and constants
const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' }) const userSettingsTab = page.getByRole('radio', { name: 'User' })
const resetButton = page.getByRole('button', { const resetButton = (level: SettingsLevel) =>
name: 'Restore default settings', page.getByRole('button', {
}) name: `Reset ${level}-level settings`,
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider') const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const settingValues = { const settingValues = {
default: '259', default: '259',
user: '120', user: '120',
project: '50', project: '50',
} }
const resetToast = (level: SettingsLevel) =>
page.getByText(`${level}-level settings were reset`)
// Open the settings modal with lower-right button await test.step(`Open the settings modal`, async () => {
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
await expect( await expect(
page.getByRole('heading', { name: 'Settings', exact: true }) page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible() ).toBeVisible()
})
await test.step('Set up theme color', async () => { await test.step('Set up theme color', async () => {
// Verify we're looking at the project-level settings, // Verify we're looking at the project-level settings,
@ -195,37 +199,40 @@ test.describe('Testing settings', () => {
await test.step('Reset project settings', async () => { await test.step('Reset project settings', async () => {
// Click the reset settings button. // Click the reset settings button.
await resetButton.click() await resetButton('project').click()
await expect(page.getByText('Settings restored to default')).toBeVisible() await expect(resetToast('project')).toBeVisible()
await expect( await expect(resetToast('project')).not.toBeVisible()
page.getByText('Settings restored to default')
).not.toBeVisible()
// Verify it is now set to the inherited user value // Verify it is now set to the inherited user value
await expect(themeColorSetting).toHaveValue(settingValues.default) await expect(themeColorSetting).toHaveValue(settingValues.user)
// Check that the user setting also rolled back await test.step(`Check that the user settings did not change`, async () => {
await userSettingsTab.click() await userSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.default) await expect(themeColorSetting).toHaveValue(settingValues.user)
await projectSettingsTab.click() })
// Set project-level value to 50 again to test the user-level reset await test.step(`Set project-level again to test the user-level reset`, async () => {
await themeColorSetting.fill(settingValues.project) await projectSettingsTab.click()
await userSettingsTab.click() await themeColorSetting.fill(settingValues.project)
await userSettingsTab.click()
})
}) })
await test.step('Reset user settings', async () => { await test.step('Reset user settings', async () => {
// Change the setting and click the reset settings button. // Click the reset settings button.
await themeColorSetting.fill(settingValues.user) await resetButton('user').click()
await resetButton.click()
await expect(resetToast('user')).toBeVisible()
await expect(resetToast('user')).not.toBeVisible()
// Verify it is now set to the default value // Verify it is now set to the default value
await expect(themeColorSetting).toHaveValue(settingValues.default) await expect(themeColorSetting).toHaveValue(settingValues.default)
// Check that the project setting also changed await test.step(`Check that the project settings did not change`, async () => {
await projectSettingsTab.click() await projectSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.default) await expect(themeColorSetting).toHaveValue(settingValues.project)
})
}) })
}) })
@ -288,7 +295,7 @@ test.describe('Testing settings', () => {
}) })
await test.step('Refresh the application and see project setting applied', async () => { 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 expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click() await settingsCloseButton.click()
@ -303,53 +310,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( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, 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 { const {
panesOpen, openKclCodePanel,
createAndSelectProject, openFilePanel,
pasteCodeInEditor, waitForPageLoad,
clickPane, selectFile,
createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen([]) await test.step('Precondition: Open to second project file', async () => {
await test.step('Precondition: No projects exist', async () => {
await expect(page.getByTestId('home-section')).toBeVisible() await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link') await page.getByText('project-000').click()
await expect(projectLinksPre).toHaveCount(0) 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', { const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
@ -357,6 +420,9 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => { await test.step('Open and close settings', async () => {
await settingsOpenButton.click() await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })
@ -370,25 +436,37 @@ test.describe('Testing settings', () => {
test('Changing modeling default unit', async ({ page }) => { test('Changing modeling default unit', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await test.step(`Test setup`, async () => {
await u.waitForAuthSkipAppStart() await page.setViewportSize({ width: 1200, height: 500 })
await page await u.waitForAuthSkipAppStart()
.getByRole('button', { name: 'Start Sketch' }) await page
.waitFor({ state: 'visible' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
const userSettingsTab = page.getByRole('radio', { name: 'User' }) })
// Open the settings modal with lower-right button // Selectors and constants
await page.getByRole('link', { name: 'Settings' }).last().click() const userSettingsTab = page.getByRole('radio', { name: 'User' })
await expect( const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
page.getByRole('heading', { name: 'Settings', exact: true }) const defaultUnitSection = page.getByText(
).toBeVisible() 'default unitRoll back default unitRoll back to match'
)
const resetButton = page.getByRole('button', { const defaultUnitRollbackButton = page.getByRole('button', {
name: 'Restore default settings', name: 'Roll back default unit',
})
await test.step(`Open the settings modal`, async () => {
await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
})
await test.step(`Reset unit setting`, async () => {
await userSettingsTab.click()
await defaultUnitSection.hover()
await defaultUnitRollbackButton.click()
await projectSettingsTab.click()
}) })
// Default unit should be mm
await resetButton.click()
await test.step('Change modeling default unit within project tab', async () => { await test.step('Change modeling default unit within project tab', async () => {
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => { const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
@ -493,4 +571,70 @@ test.describe('Testing settings', () => {
await changeUnitOfMeasureInGizmo('m', 'Meters') await changeUnitOfMeasureInGizmo('m', 'Meters')
}) })
}) })
test('Changing theme in sketch mode', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(() => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 0], %)
|> line([0, 5], %)
|> line([-5, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)
`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
const lineToolButton = page.getByTestId('line')
const segmentOverlays = page.getByTestId('segment-overlay')
const sketchOriginLocation = { x: 600, y: 250 }
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
await test.step(`Get into sketch mode`, async () => {
await u.waitForAuthSkipAppStart()
await page.mouse.click(700, 200)
await expect(editSketchButton).toBeVisible()
await editSketchButton.click()
// We use the line tool as a proxy for sketch mode
await expect(lineToolButton).toBeVisible()
await expect(segmentOverlays).toHaveCount(4)
// but we allow more time to pass for animating to the sketch
await page.waitForTimeout(1000)
})
await test.step(`Check the sketch line color before`, async () => {
await expect
.poll(() =>
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
)
.toBeLessThan(15)
})
await test.step(`Change theme to light using command palette`, async () => {
await page.keyboard.press('ControlOrMeta+K')
await page.getByRole('option', { name: 'theme' }).click()
await page.getByRole('option', { name: 'light' }).click()
await expect(page.getByText('theme to "light"')).toBeVisible()
// Make sure we haven't left sketch mode
await expect(lineToolButton).toBeVisible()
})
await test.step(`Check the sketch line color after`, async () => {
await expect
.poll(() =>
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
)
.toBeLessThan(15)
})
})
}) })

View File

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

View File

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

2
interface.d.ts vendored
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.24.12", "version": "0.25.1",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -34,7 +34,7 @@
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1", "bonjour-service": "^1.2.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
@ -51,7 +51,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "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-json-view": "^1.21.3",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
@ -64,7 +64,7 @@
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8", "vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"xstate": "^4.38.2" "xstate": "^5.17.4"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -88,7 +88,7 @@
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", "wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e", "lint": "eslint --fix src e2e packages/codemirror-lsp-client",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
@ -137,7 +137,6 @@
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@tauri-apps/cli": "^2.0.0-rc.9",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10", "@types/d3-force": "^3.0.10",
@ -169,7 +168,7 @@
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "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", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1", "http-server": "^14.1.1",

View File

@ -72,6 +72,7 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this)) this.client.setNotifyFn(this.processNotifications.bind(this))
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.client.start() this.client.start()
this.ready = true this.ready = true
@ -195,6 +196,9 @@ export class LanguageServerClient {
} }
private processNotifications(notification: LSP.NotificationMessage) { 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) => { run: (view: EditorView) => {
let value = view.plugin(plugin) let value = view.plugin(plugin)
if (!value) return false if (!value) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.requestFormatting() value.requestFormatting()
return true return true
}, },

View File

@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification = options.processLspNotification this.processLspNotification = options.processLspNotification
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initialize({ this.initialize({
documentText: this.getDocText(), documentText: this.getDocText(),
}) })
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
async initialize({ documentText }: { documentText: string }) { async initialize({ documentText }: { documentText: string }) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (this.client.initializePromise) { if (this.client.initializePromise) {
await 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() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} }
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
contentChanges: [{ text: this.view.state.doc.toString() }], contentChanges: [{ text: this.view.state.doc.toString() }],
}) })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -526,7 +532,9 @@ export class LanguageServerPlugin implements PluginValue {
processDiagnostics(params: PublishDiagnosticsParams) { processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.getDocUri()) return if (params.uri !== this.getDocUri()) return
const diagnostics = params.diagnostics // Commented to avoid the lint. See TODO below.
// const diagnostics =
params.diagnostics
.map(({ range, message, severity }) => ({ .map(({ range, message, severity }) => ({
from: posToOffset(this.view.state.doc, range.start)!, from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!, to: posToOffset(this.view.state.doc, range.end)!,

View File

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

View File

@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import { useMemo } from 'react' import { useMemo } from 'react'
import { AppStateProvider } from 'AppState' import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -69,19 +70,6 @@ const router = createRouter([
path: PATHS.INDEX, path: PATHS.INDEX,
loader: async () => { loader: async () => {
const onDesktop = isDesktop() 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 return onDesktop
? redirect(PATHS.HOME) ? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME) : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
@ -186,21 +174,23 @@ function CoreDump() {
[] []
) )
useHotkeyWrapper(['mod + shift + .'], () => { useHotkeyWrapper(['mod + shift + .'], () => {
toast.promise( toast
coreDump(coreDumpManager, true), .promise(
{ coreDump(coreDumpManager, true),
loading: 'Starting core dump...', {
success: 'Core dump completed successfully', loading: 'Starting core dump...',
error: 'Error while exporting 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,
}, },
} {
) 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 return null
} }

View File

@ -20,6 +20,8 @@ import {
ToolbarItemResolved, ToolbarItemResolved,
ToolbarModeName, ToolbarModeName,
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -68,12 +70,12 @@ export function Toolbar({
*/ */
const configCallbackProps: ToolbarItemCallbackProps = useMemo( const configCallbackProps: ToolbarItemCallbackProps = useMemo(
() => ({ () => ({
modelingStateMatches: state.matches, modelingState: state,
modelingSend: send, modelingSend: send,
commandBarSend, commandBarSend,
sketchPathId, sketchPathId,
}), }),
[state.matches, send, commandBarSend, sketchPathId] [state, send, commandBarSend, sketchPathId]
) )
/** /**
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
return ( return (
<Tooltip <Tooltip
inert={false} inert={false}
wrapperStyle={
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
position="bottom" position="bottom"
wrapperClassName="!p-4 !pointer-events-auto" 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" 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"> <li key={link.label} className="contents">
<a <a
href={link.url} href={link.url}
onClick={openExternalBrowserIfDesktop(link.url)}
target="_blank" target="_blank"
rel="noreferrer" 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" 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,18 +22,19 @@ import {
UnreliableSubscription, UnreliableSubscription,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { toSync, uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers' import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
const ORTHOGRAPHIC_CAMERA_SIZE = 20 const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30 const FRAMES_TO_ANIMATE_IN = 30
const tempQuaternion = new Quaternion() // just used for maths const tempQuaternion = new Quaternion() // just used for maths
type interactionType = 'pan' | 'rotate' | 'zoom' export type CameraInteractionType = 'pan' | 'rotate' | 'zoom'
interface ThreeCamValues { interface ThreeCamValues {
position: Vector3 position: Vector3
@ -100,6 +101,7 @@ export class CameraControls {
camProps.type === 'perspective' && camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera this.camera instanceof OrthographicCamera
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} else if ( } else if (
camProps.type === 'orthographic' && camProps.type === 'orthographic' &&
@ -127,6 +129,7 @@ export class CameraControls {
} }
throttledEngCmd = throttle((cmd: EngineCommand) => { throttledEngCmd = throttle((cmd: EngineCommand) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 30) }, 1000 / 30)
@ -139,6 +142,7 @@ export class CameraControls {
...convertThreeCamValuesToEngineCam(threeValues), ...convertThreeCamValuesToEngineCam(threeValues),
}, },
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15) }, 1000 / 15)
@ -151,6 +155,7 @@ export class CameraControls {
this.lastPerspectiveCmd && this.lastPerspectiveCmd &&
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true) this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
this.lastPerspectiveCmdTime = Date.now() this.lastPerspectiveCmdTime = Date.now()
} }
@ -218,6 +223,7 @@ export class CameraControls {
this.useOrthographicCamera() this.useOrthographicCamera()
} }
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) { if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} }
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) { if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
@ -249,6 +255,7 @@ export class CameraControls {
const doZoom = () => { const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) { if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart() this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -266,6 +273,7 @@ export class CameraControls {
const doMove = () => { const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) { if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -459,6 +467,7 @@ export class CameraControls {
this.camera.quaternion.set(qx, qy, qz, qw) this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -541,7 +550,7 @@ export class CameraControls {
const oldFov = this.camera.fov const oldFov = this.camera.fov
const viewHeightFactor = (fov: number) => { const viewHeightFactor = (fov: number) => {
/* * /* *
/| /|
/ | / |
/ | / |
@ -929,6 +938,7 @@ export class CameraControls {
} }
if (isReducedMotion()) { if (isReducedMotion()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
onComplete() onComplete()
return return
} }
@ -937,7 +947,7 @@ export class CameraControls {
.to({ t: tweenEnd }, duration) .to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut) .easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t)) .onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete) .onComplete(toSync(onComplete, reportRejection))
.start() .start()
}) })
} }
@ -962,6 +972,7 @@ export class CameraControls {
// Decrease the FOV // Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov) currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) { } else if (frameWaitOnFinish > 0) {
@ -991,6 +1002,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()
@ -999,6 +1011,7 @@ export class CameraControls {
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t) const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
this.camera.up.copy(currentUp) this.camera.up.copy(currentUp)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
} }
@ -1027,6 +1040,7 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()
@ -1175,7 +1189,7 @@ function convertThreeCamValuesToEngineCam({
const lookAt = buildLookAt(64 / zoom, target, position) const lookAt = buildLookAt(64 / zoom, target, position)
return { return {
center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z), center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z),
up: new Vector3(0, 0, 1), up: new Vector3(upVector.x, upVector.y, upVector.z),
vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z), vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z),
} }
} }
@ -1205,8 +1219,8 @@ function _getInteractionType(
enablePan: boolean, enablePan: boolean,
enableRotate: boolean, enableRotate: boolean,
enableZoom: boolean enableZoom: boolean
): interactionType | 'none' { ): CameraInteractionType | 'none' {
let state: interactionType | 'none' = 'none' let state: CameraInteractionType | 'none' = 'none'
if (enablePan && interactionGuards.pan.callback(event)) return 'pan' if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate' if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom' if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'

View File

@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle, toSync } from 'lib/utils'
import { import {
sceneInfra, sceneInfra,
kclManager, kclManager,
@ -44,7 +44,7 @@ import {
removeSingleConstraintInfo, removeSingleConstraintInfo,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { err, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -124,9 +124,9 @@ export const ClientSideScene = ({
} else if (context.mouseState.type === 'isDragging') { } else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing' cursor = 'grabbing'
} else if ( } else if (
state.matches('Sketch.Line tool') || state.matches({ Sketch: 'Line tool' }) ||
state.matches('Sketch.Tangential arc to') || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches('Sketch.Rectangle tool') state.matches({ Sketch: 'Rectangle tool' })
) { ) {
cursor = 'crosshair' cursor = 'crosshair'
} else { } else {
@ -214,9 +214,9 @@ const Overlay = ({
overlay.visible && overlay.visible &&
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' && typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
!( !(
state.matches('Sketch.Line tool') || state.matches({ Sketch: 'Line tool' }) ||
state.matches('Sketch.Tangential arc to') || state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches('Sketch.Rectangle tool') state.matches({ Sketch: 'Rectangle tool' })
) )
return ( return (
@ -582,7 +582,7 @@ const ConstraintSymbol = ({
}} }}
// disabled={isConstrained || !convertToVarEnabled} // disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
send({ send({
type: 'Convert to variable', type: 'Convert to variable',
@ -616,13 +616,14 @@ const ConstraintSymbol = ({
) )
if (!transform) return if (!transform) return
const { modifiedAst } = transform const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true) kclManager.updateAst(modifiedAst, true)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }
toast.success('Constraint removed') toast.success('Constraint removed')
} }
}} }, reportRejection)}
> >
<CustomIcon name={name} /> <CustomIcon name={name} />
</button> </button>
@ -688,7 +689,7 @@ const ConstraintSymbol = ({
const throttled = throttle((a: ReactCameraProperties) => { const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) { if (a.type === 'perspective' && a.fov) {
sceneInfra.camControls.dollyZoom(a.fov) sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
} }
}, 1000 / 15) }, 1000 / 15)
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.camControls.useOrthographicCamera()
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.usePerspectiveCamera(true) sceneInfra.camControls.usePerspectiveCamera(true)
} }
}} }}
@ -725,7 +727,7 @@ export const CamDebugSettings = () => {
<div> <div>
<button <button
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition() sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}} }}
> >
Reset Camera Position Reset Camera Position

View File

@ -28,7 +28,6 @@ import {
OnMouseEnterLeaveArgs, OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE, RAYCASTABLE_PLANE,
SEGMENT_LENGTH_LABEL, SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT, SEGMENT_LENGTH_LABEL_TEXT,
SKETCH_GROUP_SEGMENTS, SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER, SKETCH_LAYER,
@ -102,8 +101,8 @@ import {
getRectangleCallExpressions, getRectangleCallExpressions,
updateRectangleSketch, updateRectangleSketch,
} from 'lib/rectangleTool' } from 'lib/rectangleTool'
import { getThemeColorForThreeJs } from 'lib/theme' import { getThemeColorForThreeJs, Themes } 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 { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
@ -324,6 +323,7 @@ export class SceneEntities {
) )
} }
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
@ -634,6 +634,7 @@ export class SceneEntities {
draftExpressionsIndices, draftExpressionsIndices,
}) })
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
@ -701,7 +702,7 @@ export class SceneEntities {
if (profileStart) { if (profileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' }) sceneInfra.modelingSend({ type: 'CancelSketch' })
} else { } else {
this.setUpDraftSegment( await this.setUpDraftSegment(
sketchPathToNode, sketchPathToNode,
forward, forward,
up, up,
@ -771,6 +772,7 @@ export class SceneEntities {
}) })
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onMove: async (args) => { onMove: async (args) => {
// Update the width and height of the draft rectangle // Update the width and height of the draft rectangle
const pathToNodeTwo = structuredClone(sketchPathToNode) const pathToNodeTwo = structuredClone(sketchPathToNode)
@ -818,6 +820,7 @@ export class SceneEntities {
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup) this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
) )
}, },
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick: async (args) => { onClick: async (args) => {
// Commit the rectangle to the full AST/code and return to sketch.idle // Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD const cornerPoint = args.intersectionPoint?.twoD
@ -892,9 +895,11 @@ export class SceneEntities {
}) => { }) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing' let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDragEnd: async () => { onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') { if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({ this.setupSketch({
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
@ -911,6 +916,7 @@ export class SceneEntities {
}) })
} }
}, },
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onDrag: async ({ onDrag: async ({
selected, selected,
intersectionPoint, intersectionPoint,
@ -958,6 +964,7 @@ export class SceneEntities {
await kclManager.executeAstMock(mod.modifiedAst) await kclManager.executeAstMock(mod.modifiedAst)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupSketch({ this.setupSketch({
sketchPathToNode: pathToNode, sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
@ -1161,7 +1168,7 @@ export class SceneEntities {
) )
) )
sceneInfra.overlayCallbacks(callBacks) sceneInfra.overlayCallbacks(callBacks)
})() })().catch(reportRejection)
} }
/** /**
@ -1414,20 +1421,14 @@ export class SceneEntities {
) as CSS2DObject ) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(length)}${sceneInfra._baseUnit}` label.innerText = `${roundOff(length)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT) label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
const offsetFromMidpoint = new Vector2(to[0] - from[0], to[1] - from[1]) const slope = (to[1] - from[1]) / (to[0] - from[0])
.normalize() let slopeAngle = ((Math.atan(slope) * 180) / Math.PI) * -1
.rotateAround(new Vector2(0, 0), Math.PI / 2) label.style.setProperty('--degree', `${slopeAngle}deg`)
.multiplyScalar(SEGMENT_LENGTH_LABEL_OFFSET_PX * scale) label.style.setProperty('--x', `0px`)
label.style.setProperty('--x', `${offsetFromMidpoint.x}px`) label.style.setProperty('--y', `0px`)
label.style.setProperty('--y', `${offsetFromMidpoint.y}px`) labelWrapper.position.set((from[0] + to[0]) / 2, (from[1] + to[1]) / 2, 0)
labelWrapper.position.set(
(from[0] + to[0]) / 2 + offsetFromMidpoint.x,
(from[1] + to[1]) / 2 + offsetFromMidpoint.y,
0
)
labelGroup.visible = isHandlesVisible labelGroup.visible = isHandlesVisible
} }
@ -1465,6 +1466,25 @@ export class SceneEntities {
to, to,
}) })
} }
/**
* Update the base color of each of the THREEjs meshes
* that represent each of the sketch segments, to get the
* latest value from `sceneInfra._theme`
*/
updateSegmentBaseColor(newColor: Themes.Light | Themes.Dark) {
const newColorThreeJs = getThemeColorForThreeJs(newColor)
Object.values(this.activeSegments).forEach((group) => {
group.userData.baseColor = newColorThreeJs
group.traverse((child) => {
if (
child instanceof Mesh &&
child.material instanceof MeshBasicMaterial
) {
child.material.color.set(newColorThreeJs)
}
})
})
}
removeSketchGrid() { removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup) if (this.axisGroup) this.scene.remove(this.axisGroup)
} }

View File

@ -6,6 +6,9 @@
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
}
.header.desktopApp {
/* Make the header act as a handle to drag the electron app window, /* 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 * 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 * 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 styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton' import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton' import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -32,7 +33,9 @@ export const AppHeader = ({
className={ className={
'w-full grid ' + 'w-full grid ' +
styles.header + 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 className
} }
style={style} style={style}

View File

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

View File

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

View File

@ -71,6 +71,17 @@ function CommandArgOptionInput({
inputRef.current?.focus() inputRef.current?.focus()
inputRef.current?.select() inputRef.current?.select()
}, [inputRef]) }, [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, // Filter the options based on the query,
// resetting the query when the options change // 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 { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext, useEffect } from 'react' import { useEffect } from 'react'
import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { export const CommandsContext = createActorContext(
commandBarState: StateFrom<typeof commandBarMachine> commandBarMachine.provide({
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,
guards: { guards: {
'Command has no arguments': (context, _event) => { 'Command has no arguments': ({ context }) => {
return ( return (
!context.selectedCommand?.args || !context.selectedCommand?.args ||
Object.keys(context.selectedCommand?.args).length === 0 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( return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip (argConfig) => argConfig.skip
) )
}, },
}, },
}) })
)
useEffect(() => { export const CommandBarProvider = ({
editorManager.setCommandBarSend(commandBarSend) children,
}) }: {
children: React.ReactNode
}) => {
return ( return (
<CommandsContext.Provider <CommandsContext.Provider>
value={{ <CommandBarProviderInner>{children}</CommandBarProviderInner>
commandBarState,
commandBarSend,
}}
>
{children}
</CommandsContext.Provider> </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() e.preventDefault()
commandBarSend({ commandBarSend({
type: 'Submit command', type: 'Submit command',
data: argumentsToSubmit, output: argumentsToSubmit,
}) })
} }

View File

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

View File

@ -11,6 +11,7 @@ export function CommandBarOpenButton() {
<button <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" 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' })} onClick={() => commandBarSend({ type: 'Open' })}
data-testid="command-bar-open-button"
> >
<span>Commands</span> <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"> <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,5 +1,6 @@
import { CommandLog } from 'lang/std/engineConnection' import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
export function useEngineCommands(): [CommandLog[], () => void] { export function useEngineCommands(): [CommandLog[], () => void] {
@ -77,9 +78,11 @@ export const EngineCommands = () => {
/> />
<button <button
data-testid="custom-cmd-send-button" data-testid="custom-cmd-send-button"
onClick={() => onClick={() => {
engineCommandManager.sendSceneCommand(JSON.parse(customCmd)) engineCommandManager
} .sendSceneCommand(JSON.parse(customCmd))
.catch(reportRejection)
}}
> >
Send custom command Send custom command
</button> </button>

View File

@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
import React, { createContext } from 'react' import React, { createContext } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
Actor,
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
EventFrom,
InterpreterFrom,
Prop, Prop,
StateFrom, StateFrom,
assign, fromPromise,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
context: ContextFrom<T> context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'> send: Prop<Actor<T>, 'send'>
} }
export const FileContext = createContext( export const FileContext = createContext(
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, { const [state, send] = useMachine(
context: { fileMachine.provide({
project, actions: {
selectedDirectory: project, renameToastSuccess: ({ event }) => {
}, if (event.type !== 'xstate.done.actor.rename-file') return
actions: { toast.success(event.output.message)
navigateToFile: (context, event) => { },
if (event.data && 'name' in event.data) { createToastSuccess: ({ event }) => {
commandBarSend({ type: 'Close' }) if (event.type !== 'xstate.done.actor.create-and-open-file') return
navigate( toast.success(event.output.message)
`..${PATHS.FILE}/${encodeURIComponent( },
context.selectedDirectory + toastSuccess: ({ event }) => {
window.electron.path.sep + if (
event.data.name event.type !== 'xstate.done.actor.rename-file' &&
)}` event.type !== 'xstate.done.actor.delete-file'
) )
} else if ( return
event.data && toast.success(event.output.message)
'path' in event.data && },
event.data.path.endsWith(FILE_EXT) toastError: ({ event }) => {
) { if (event.type !== 'xstate.done.actor.rename-file') return
// Don't navigate to newly created directories toast.error(event.output.message)
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`) },
} 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({ actors: {
itemsBeingRenamed: (context, event) => [ readFiles: fromPromise(async ({ input }) => {
...context.itemsBeingRenamed, const newFiles =
event.data.path, (isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
], []
}),
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) {
return { 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, newPath,
oldPath, oldPath,
} }
} }),
deleteFile: fromPromise(async ({ input }) => {
const isDir = !!input.children
// if there are any siblings with the same name, report error. if (isDir) {
const entries = await window.electron.readdir( await window.electron
window.electron.path.dirname(newPath) .rm(input.path, {
) recursive: true,
for (let entry of entries) { })
if (entry === newName) { .catch((e) => console.error('Error deleting directory', e))
return Promise.reject(new Error('Filename already exists.')) } 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) { const entries = await window.electron.readdir(project.path)
return Promise.reject(new Error('file is not defined')) 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 deleted the current file or one of its parent directories,
// If we just renamed the current file, navigate to the new path // navigate to the project root
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`) if (
} else if (file?.path.includes(oldPath)) { (input.path === file?.path || file?.path.includes(input.path)) &&
// If we just renamed a directory that the current file is in, navigate to the new path project?.path
navigate( ) {
`..${PATHS.FILE}/${encodeURIComponent( navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
file.path.replace(oldPath, newPath) }
)}`
)
}
return { return {
message: `Successfully renamed "${oldName}" to "${name}"`, message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
newPath, input.name
oldPath, }"`,
} }
}),
}, },
deleteFile: async ( }),
context: ContextFrom<typeof fileMachine>, {
event: EventFrom<typeof fileMachine, 'Delete file'> input: {
) => { project,
const isDir = !!event.data.children selectedDirectory: project,
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
}"`
}, },
}, }
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 ( return (
<FileContext.Provider <FileContext.Provider

View File

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

View File

@ -27,6 +27,7 @@ import {
} from './ContextMenu' } from './ContextMenu'
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap'
const CANVAS_SIZE = 80 const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5 const FRUSTUM_SIZE = 0.5
@ -67,7 +68,9 @@ export default function Gizmo() {
<ContextMenuItem <ContextMenuItem
key={axisName} key={axisName}
onClick={() => { onClick={() => {
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames) sceneInfra.camControls
.updateCameraToAxis(axisName as AxisNames)
.catch(reportRejection)
}} }}
> >
{axisSemantic} view {axisSemantic} view
@ -75,7 +78,7 @@ export default function Gizmo() {
)), )),
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition() sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
}} }}
> >
Reset view Reset view
@ -299,7 +302,7 @@ const initializeMouseEvents = (
const handleClick = () => { const handleClick = () => {
if (raycasterIntersect.current) { if (raycasterIntersect.current) {
const axisName = raycasterIntersect.current.object.name as AxisNames 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 { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
const HelpMenuDivider = () => ( const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" /> <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) { if (isInProject) {
navigate(filePath + PATHS.ONBOARDING.INDEX) navigate(filePath + PATHS.ONBOARDING.INDEX)
} else { } else {
createAndOpenNewProject({ onProjectOpen, navigate }) createAndOpenNewProject({ onProjectOpen, navigate }).catch(
reportRejection
)
} }
}} }}
> >

View File

@ -12,6 +12,7 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow' import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator' import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator' import { ModelStateIndicator } from './ModelStateIndicator'
import { reportRejection } from 'lib/trap'
export function LowerRightControls({ export function LowerRightControls({
children, children,
@ -25,7 +26,7 @@ export function LowerRightControls({
const linkOverrideClassName = const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30' '!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 preventDefault: () => void
stopPropagation: () => void stopPropagation: () => void
}) { }) {
@ -34,7 +35,9 @@ export function LowerRightControls({
if (!coreDumpManager) { if (!coreDumpManager) {
// open default reporting option // 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 { } else {
toast toast
.promise( .promise(
@ -56,7 +59,7 @@ export function LowerRightControls({
if (err) { if (err) {
openWindow( openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose' 'https://github.com/KittyCAD/modeling-app/issues/new/choose'
) ).catch(reportRejection)
} }
}) })
} }

View File

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

View File

@ -29,6 +29,12 @@ export const ModelStateIndicator = () => {
name="checkmark" 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 ( return (

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

View File

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

View File

@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
import { ConnectingTypeGroup } from '../lang/std/engineConnection' import { ConnectingTypeGroup } from '../lang/std/engineConnection'
import { useNetworkContext } from '../hooks/useNetworkContext' import { useNetworkContext } from '../hooks/useNetworkContext'
import { NetworkHealthState } from '../hooks/useNetworkStatus' import { NetworkHealthState } from '../hooks/useNetworkStatus'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected', [NetworkHealthState.Ok]: 'Connected',
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
</div> </div>
{issues[name as ConnectingTypeGroup] && ( {issues[name as ConnectingTypeGroup] && (
<button <button
onClick={async () => { onClick={toSync(async () => {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || '' JSON.stringify(error, null, 2) || ''
) )
setHasCopied(true) setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000) 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" 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'} {hasCopied ? 'Copied' : 'Copy Error'}

View File

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

View File

@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
// Window may not be available in some environments // Window may not be available in some environments
window?.location.reload() window?.location.reload()
}) })
.catch(reportRejection)
} }
return ( return (
<button <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" 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" /> <CustomIcon name="exclamationMark" className="w-5 h-5" />

View File

@ -12,7 +12,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput' import { SettingsFieldInput } from './SettingsFieldInput'
import { getInitialDefaultDir } from 'lib/desktop'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings' import { APP_VERSION } from 'routes/Settings'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
@ -20,6 +19,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash' import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { ForwardedRef, forwardRef, useEffect } from 'react' import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
interface AllSettingsFieldsProps { interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel searchParamTab: SettingsLevel
@ -54,7 +55,7 @@ export const AllSettingsFields = forwardRef(
) )
: undefined : undefined
async function restartOnboarding() { function restartOnboarding() {
send({ send({
type: `set.app.onboardingStatus`, type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' }, data: { level: 'user', value: '' },
@ -82,6 +83,7 @@ export const AllSettingsFields = forwardRef(
} }
} }
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigateToOnboardingStart() navigateToOnboardingStart()
}, [isFileSettings, navigate, state]) }, [isFileSettings, navigate, state])
@ -190,7 +192,7 @@ export const AllSettingsFields = forwardRef(
{isDesktop() && ( {isDesktop() && (
<ActionButton <ActionButton
Element="button" Element="button"
onClick={async () => { onClick={toSync(async () => {
const paths = await getSettingsFolderPaths( const paths = await getSettingsFolderPaths(
projectPath ? decodeURIComponent(projectPath) : undefined projectPath ? decodeURIComponent(projectPath) : undefined
) )
@ -199,7 +201,7 @@ export const AllSettingsFields = forwardRef(
return new Error('finalPath undefined') return new Error('finalPath undefined')
} }
window.electron.showInFolder(finalPath) window.electron.showInFolder(finalPath)
}} }, reportRejection)}
iconStart={{ iconStart={{
icon: 'folder', icon: 'folder',
size: 'sm', size: 'sm',
@ -211,13 +213,14 @@ export const AllSettingsFields = forwardRef(
)} )}
<ActionButton <ActionButton
Element="button" Element="button"
onClick={async () => { onClick={() => {
const defaultDirectory = await getInitialDefaultDir()
send({ send({
type: 'Reset settings', type: 'Reset settings',
defaultDirectory, level: searchParamTab,
}) })
toast.success('Settings restored to default') toast.success(
`Your ${searchParamTab}-level settings were reset`
)
}} }}
iconStart={{ iconStart={{
icon: 'refresh', icon: 'refresh',
@ -226,7 +229,7 @@ export const AllSettingsFields = forwardRef(
bgClassName: 'bg-destroy-70', bgClassName: 'bg-destroy-70',
}} }}
> >
Restore default settings Reset {searchParamTab}-level settings
</ActionButton> </ActionButton>
</div> </div>
</SettingsSection> </SettingsSection>

View File

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

View File

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

View File

@ -53,9 +53,10 @@ export const Stream = () => {
* executed. If we can find a way to do this from a more * executed. If we can find a way to do this from a more
* central place, we can move this code there. * central place, we can move this code there.
*/ */
async function executeCodeAndPlayStream() { function executeCodeAndPlayStream() {
kclManager.executeCode(true).then(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises
videoRef.current?.play().catch((e) => { kclManager.executeCode(true).then(async () => {
await videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current) console.warn('Video playing was prevented', e, videoRef.current)
}) })
setStreamState(StreamState.Playing) setStreamState(StreamState.Playing)
@ -218,12 +219,12 @@ export const Stream = () => {
*/ */
useEffect(() => { useEffect(() => {
if (!kclManager.isExecuting) { if (!kclManager.isExecuting) {
setTimeout(() => setTimeout(() => {
// execute in the next event loop // execute in the next event loop
videoRef.current?.play().catch((e) => { videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current) console.warn('Video playing was prevented', e, videoRef.current)
}) })
) })
} }
}, [kclManager.isExecuting]) }, [kclManager.isExecuting])
@ -287,9 +288,10 @@ export const Stream = () => {
}, },
}) })
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('idle.showPlanes')) return if (state.matches({ idle: 'showPlanes' })) return
if (!context.store?.didDragInStream && btnName(e).left) { if (!context.store?.didDragInStream && btnName(e).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine( sendSelectEventToEngine(
e, e,
videoRef.current, videoRef.current,

View File

@ -5,7 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models' import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { import {
Box3, Box3,
Color, Color,
@ -15,6 +15,7 @@ import {
LineSegments, LineSegments,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
MOUSE,
OrthographicCamera, OrthographicCamera,
Scene, Scene,
Vector3, Vector3,
@ -26,8 +27,18 @@ import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { EventData, EventFrom } from 'xstate' import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'
import {
CameraControls,
CameraInteractionType,
} from 'clientSideScene/CameraControls'
import {
cameraMouseDragGuards,
CameraSystem,
MouseGuard,
} from 'lib/cameraControls'
const CANVAS_SIZE = 128 const CANVAS_SIZE = 128
const PROMPT_TRUNCATE_LENGTH = 128 const PROMPT_TRUNCATE_LENGTH = 128
@ -45,7 +56,7 @@ export function ToastTextToCadError({
prompt: string prompt: string
commandBarSend: ( commandBarSend: (
event: EventFrom<typeof commandBarMachine>, event: EventFrom<typeof commandBarMachine>,
data?: EventData data?: unknown
) => void ) => void
}) { }) {
return ( return (
@ -112,13 +123,19 @@ export function ToastTextToCadSuccess({
token?: string token?: string
fileMachineSend: ( fileMachineSend: (
event: EventFrom<typeof fileMachine>, event: EventFrom<typeof fileMachine>,
data?: EventData data?: unknown
) => void ) => void
settings: { settings: {
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
mouseControls: CameraSystem
} }
}) { }) {
const interactionGuards = useMemo(
() => cameraMouseDragGuards[settings.mouseControls],
[settings.mouseControls]
)
const controlsRef = useRef<OrbitControls | null>(null)
const wrapperRef = useRef<HTMLDivElement | null>(null) const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null) const canvasRef = useRef<HTMLCanvasElement | null>(null)
const animationRequestRef = useRef<number>() const animationRequestRef = useRef<number>()
@ -167,8 +184,59 @@ export function ToastTextToCadSuccess({
const ambientLight = new DirectionalLight(new Color('white'), 8.0) const ambientLight = new DirectionalLight(new Color('white'), 8.0)
scene.add(ambientLight) scene.add(ambientLight)
const camera = createCamera() const camera = createCamera()
// Because this listener is registered before the OrbitControls are created,
// it runs first and can block the OrbitControls from working.
renderer.domElement.addEventListener('pointerdown', (e) => {
if (!controlsRef.current) return
const newInteractionType = getCameraInteractionType({
interactionGuards,
event: e,
})
console.log('newInteractionType', newInteractionType)
if (newInteractionType === 'none') {
e.stopImmediatePropagation()
}
/**
* Update the OrbitControls to enable only the current interaction type.
* This is a hack to override the interaction types of the OrbitControls
* to match ours. In the future, we should roll our own class based on OrbitControls,
* which can handle interaction guards that are more complex than just mouse buttons.
*/
if (newInteractionType === 'pan') {
controlsRef.current.enablePan = true
controlsRef.current.enableZoom = false
controlsRef.current.enableRotate = false
controlsRef.current.mouseButtons = {
LEFT: MOUSE.PAN,
MIDDLE: MOUSE.PAN,
RIGHT: MOUSE.PAN,
}
} else if (newInteractionType === 'zoom') {
controlsRef.current.enablePan = false
controlsRef.current.enableZoom = true
controlsRef.current.enableRotate = false
controlsRef.current.mouseButtons = {
LEFT: MOUSE.DOLLY,
MIDDLE: MOUSE.DOLLY,
RIGHT: MOUSE.DOLLY,
}
} else if (newInteractionType === 'rotate') {
controlsRef.current.enablePan = false
controlsRef.current.enableZoom = false
controlsRef.current.enableRotate = true
controlsRef.current.mouseButtons = {
LEFT: MOUSE.ROTATE,
MIDDLE: MOUSE.ROTATE,
RIGHT: MOUSE.ROTATE,
}
}
controls.update()
})
const controls = new OrbitControls(camera, renderer.domElement) const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true controlsRef.current = controls
const loader = new GLTFLoader() const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader() const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/') dracoLoader.setDecoderPath('/examples/jsm/libs/draco/')
@ -269,7 +337,7 @@ export function ToastTextToCadSuccess({
}, []) }, [])
return ( return (
<div className="flex gap-4 min-w-80" ref={wrapperRef}> <div className="flex gap-4 min-w-80 user-select-none" ref={wrapperRef}>
<div <div
className="flex-none overflow-hidden" className="flex-none overflow-hidden"
style={{ width: CANVAS_SIZE + 'px', height: CANVAS_SIZE + 'px' }} style={{ width: CANVAS_SIZE + 'px', height: CANVAS_SIZE + 'px' }}
@ -297,7 +365,7 @@ export function ToastTextToCadSuccess({
name={hasCopied ? 'Close' : 'Reject'} name={hasCopied ? 'Close' : 'Reject'}
onClick={() => { onClick={() => {
if (!hasCopied) { if (!hasCopied) {
sendTelemetry(modelId, 'rejected', token) sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
} }
if (isDesktop()) { if (isDesktop()) {
// Delete the file from the project // Delete the file from the project
@ -323,6 +391,7 @@ export function ToastTextToCadSuccess({
}} }}
name="Accept" name="Accept"
onClick={() => { onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token) sendTelemetry(modelId, 'accepted', token)
navigate( navigate(
`${PATHS.FILE}/${encodeURIComponent( `${PATHS.FILE}/${encodeURIComponent(
@ -342,7 +411,9 @@ export function ToastTextToCadSuccess({
}} }}
name="Copy to clipboard" name="Copy to clipboard"
onClick={() => { onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendTelemetry(modelId, 'accepted', token) sendTelemetry(modelId, 'accepted', token)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navigator.clipboard.writeText(data.code || '// no code found') navigator.clipboard.writeText(data.code || '// no code found')
setShowCopiedUi(true) setShowCopiedUi(true)
setHasCopied(true) setHasCopied(true)
@ -407,3 +478,22 @@ function traverseSceneToStyleObjects({
} }
}) })
} }
function getCameraInteractionType({
interactionGuards,
event,
}: {
interactionGuards: MouseGuard
event: MouseEvent
}): CameraInteractionType | 'none' {
if (interactionGuards.pan.callback(event)) {
return 'pan'
}
if (interactionGuards.zoom.dragCallback(event)) {
return 'zoom'
}
if (interactionGuards.rotate.callback(event)) {
return 'rotate'
}
return 'none'
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state' import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
@ -11,6 +11,7 @@ import {
forEachDiagnostic, forEachDiagnostic,
setDiagnosticsEffect, setDiagnosticsEffect,
} from '@codemirror/lint' } from '@codemirror/lint'
import { StateFrom } from 'xstate'
const updateOutsideEditorAnnotation = Annotation.define<boolean>() const updateOutsideEditorAnnotation = Annotation.define<boolean>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true) export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
@ -38,7 +39,7 @@ export default class EditorManager {
private _lastEvent: { event: string; time: number } | null = null private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingEvent: ModelingMachineEvent | null = null private _modelingState: StateFrom<typeof modelingMachine> | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void = private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {} () => {}
@ -80,8 +81,8 @@ export default class EditorManager {
this._modelingSend = send this._modelingSend = send
} }
set modelingEvent(event: ModelingMachineEvent) { set modelingState(state: StateFrom<typeof modelingMachine>) {
this._modelingEvent = event this._modelingState = state
} }
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) { setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
@ -248,13 +249,11 @@ export default class EditorManager {
return return
} }
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool'] if (!this._modelingState) {
if (!this._modelingEvent) {
return return
} }
if (ignoreEvents.includes(this._modelingEvent.type)) { if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
return return
} }
@ -286,8 +285,9 @@ export default class EditorManager {
this._lastEvent = { event: stringEvent, time: Date.now() } this._lastEvent = { event: stringEvent, time: Date.now() }
this._modelingSend(eventInfo.modelingEvent) this._modelingSend(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) => eventInfo.engineEvents.forEach((event) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand(event) 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 { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams' import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
const copilotPluginAnnotation = Annotation.define<boolean>() const copilotPluginAnnotation = Annotation.define<boolean>()
export const copilotPluginEvent = copilotPluginAnnotation.of(true) export const copilotPluginEvent = copilotPluginAnnotation.of(true)
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
if (!this.client.ready) return if (!this.client.ready) return
try { try {
this.requestCompletions() this.requestCompletions().catch(reportRejection)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
}) })
this.accept(ghostText.uuid) this.accept(ghostText.uuid).catch(reportRejection)
return true return true
} }
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
], ],
}) })
this.reject() this.reject().catch(reportRejection)
return false return false
} }

View File

@ -96,6 +96,7 @@ export class KclPlugin implements PluginValue {
const newCode = viewUpdate.state.doc.toString() const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode codeManager.code = newCode
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile() codeManager.writeToFile()
this.scheduleUpdateDoc() this.scheduleUpdateDoc()
@ -117,6 +118,7 @@ export class KclPlugin implements PluginValue {
} }
if (!this.client.ready) return if (!this.client.ready) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.executeCode() kclManager.executeCode()
} }

View File

@ -18,7 +18,7 @@ import {
CopilotWorkerOptions, CopilotWorkerOptions,
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
@ -60,7 +60,8 @@ export async function kclLspRun(
} }
} }
onmessage = function (event) { // WebWorker message handler.
onmessage = function (event: MessageEvent) {
if (err(fromServer)) return if (err(fromServer)) return
const { worker, eventType, eventData }: LspWorkerEvent = event.data const { worker, eventType, eventData }: LspWorkerEvent = event.data
@ -70,7 +71,7 @@ onmessage = function (event) {
| KclWorkerOptions | KclWorkerOptions
| CopilotWorkerOptions | CopilotWorkerOptions
initialise(wasmUrl) initialise(wasmUrl)
.then((instantiatedModule) => { .then(async (instantiatedModule) => {
console.log('Worker: WASM module loaded', worker, instantiatedModule) console.log('Worker: WASM module loaded', worker, instantiatedModule)
const config = new ServerConfig( const config = new ServerConfig(
intoServer, intoServer,
@ -81,7 +82,7 @@ onmessage = function (event) {
switch (worker) { switch (worker) {
case LspWorker.Kcl: case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions const kclData = eventData as KclWorkerOptions
kclLspRun( await kclLspRun(
config, config,
null, null,
kclData.token, kclData.token,
@ -91,7 +92,11 @@ onmessage = function (event) {
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl) await copilotLspRun(
config,
copilotData.token,
copilotData.apiBaseUrl
)
break break
} }
}) })
@ -104,7 +109,7 @@ onmessage = function (event) {
intoServer.enqueue(data) intoServer.enqueue(data)
const json: jsrpc.JSONRPCRequest = Codec.decode(data) const json: jsrpc.JSONRPCRequest = Codec.decode(data)
if (null != json.id) { if (null != json.id) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
fromServer.responses.get(json.id)!.then((response) => { fromServer.responses.get(json.id)!.then((response) => {
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse) const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
postMessage(encoded) postMessage(encoded)
@ -115,19 +120,17 @@ onmessage = function (event) {
console.error('Worker: Unknown message type', worker, eventType) console.error('Worker: Unknown message type', worker, eventType)
} }
} }
;(async () => {
new Promise<void>(async (resolve) => {
if (err(fromServer)) return if (err(fromServer)) return
for await (const requests of fromServer.requests) { for await (const requests of fromServer.requests) {
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest) const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
postMessage(encoded) postMessage(encoded)
} }
}) })().catch(reportRejection)
;(async () => {
new Promise<void>(async (resolve) => {
if (err(fromServer)) return if (err(fromServer)) return
for await (const notification of fromServer.notifications) { for await (const notification of fromServer.notifications) {
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest) const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
postMessage(encoded) postMessage(encoded)
} }
}) })().catch(reportRejection)

View File

@ -1,6 +1,10 @@
import { CommandsContext } from 'components/CommandBar/CommandBarProvider' import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { useContext } from 'react'
export const useCommandsContext = () => { export const useCommandsContext = () => {
return useContext(CommandsContext) const commandBarActor = CommandsContext.useActorRef()
const commandBarState = CommandsContext.useSelector((state) => state)
return {
commandBarSend: commandBarActor.send,
commandBarState,
}
} }

View File

@ -14,7 +14,7 @@ import {
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
@ -86,9 +86,11 @@ export function useEngineConnectionSubscriptions() {
}) })
const unSubClick = engineCommandManager.subscribeTo({ const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point', event: 'select_with_point',
callback: async (engineEvent) => { callback: (engineEvent) => {
const event = await getEventForSelectWithPoint(engineEvent) ;(async () => {
event && send(event) const event = await getEventForSelectWithPoint(engineEvent)
event && send(event)
})().catch(reportRejection)
}, },
}) })
return () => { return () => {
@ -101,118 +103,120 @@ export function useEngineConnectionSubscriptions() {
const unSub = engineCommandManager.subscribeTo({ const unSub = engineCommandManager.subscribeTo({
event: 'select_with_point', event: 'select_with_point',
callback: state.matches('Sketch no face') callback: state.matches('Sketch no face')
? async ({ data }) => { ? ({ data }) => {
let planeOrFaceId = data.entity_id ;(async () => {
if (!planeOrFaceId) return let planeOrFaceId = data.entity_id
if ( if (!planeOrFaceId) return
engineCommandManager.defaultPlanes?.xy === planeOrFaceId || if (
engineCommandManager.defaultPlanes?.xz === planeOrFaceId || engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId || engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId || engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId || engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
) { engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
let planeId = planeOrFaceId ) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = { let planeId = planeOrFaceId
[engineCommandManager.defaultPlanes.xy]: 'XY', const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xz]: 'XZ', [engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.yz]: 'YZ', [engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY', [engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXz]: '-XZ', [engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negYz]: '-YZ', [engineCommandManager.defaultPlanes.negXz]: '-XZ',
} [engineCommandManager.defaultPlanes.negYz]: '-YZ',
// TODO can we get this information from rust land when it creates the default planes? }
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs) // TODO can we get this information from rust land when it creates the default planes?
let zAxis: [number, number, number] = [0, 0, 1] // maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let yAxis: [number, number, number] = [0, 1, 0] let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// get unit vector from camera position to target // get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position const camVector = sceneInfra.camControls.camera.position
.clone() .clone()
.sub(sceneInfra.camControls.target) .sub(sceneInfra.camControls.target)
if (engineCommandManager.defaultPlanes?.xy === planeId) { if (engineCommandManager.defaultPlanes?.xy === planeId) {
zAxis = [0, 0, 1] zAxis = [0, 0, 1]
yAxis = [0, 1, 0] yAxis = [0, 1, 0]
if (camVector.z < 0) { if (camVector.z < 0) {
zAxis = [0, 0, -1] zAxis = [0, 0, -1]
planeId = engineCommandManager.defaultPlanes?.negXy || '' planeId = engineCommandManager.defaultPlanes?.negXy || ''
} }
} else if (engineCommandManager.defaultPlanes?.yz === planeId) { } else if (engineCommandManager.defaultPlanes?.yz === planeId) {
zAxis = [1, 0, 0] zAxis = [1, 0, 0]
yAxis = [0, 0, 1] yAxis = [0, 0, 1]
if (camVector.x < 0) { if (camVector.x < 0) {
zAxis = [-1, 0, 0] zAxis = [-1, 0, 0]
planeId = engineCommandManager.defaultPlanes?.negYz || '' planeId = engineCommandManager.defaultPlanes?.negYz || ''
} }
} else if (engineCommandManager.defaultPlanes?.xz === planeId) { } else if (engineCommandManager.defaultPlanes?.xz === planeId) {
zAxis = [0, 1, 0] zAxis = [0, 1, 0]
yAxis = [0, 0, 1] yAxis = [0, 0, 1]
planeId = engineCommandManager.defaultPlanes?.negXz || '' planeId = engineCommandManager.defaultPlanes?.negXz || ''
if (camVector.y < 0) { if (camVector.y < 0) {
zAxis = [0, -1, 0] zAxis = [0, -1, 0]
planeId = engineCommandManager.defaultPlanes?.xz || '' planeId = engineCommandManager.defaultPlanes?.xz || ''
}
} }
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
planeId: planeId,
plane: defaultPlaneStrMap[planeId],
zAxis,
yAxis,
},
})
return
} }
const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
faceId,
engineCommandManager.artifactGraph
)
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
const codeRef =
artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range
)
const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange(
kclManager.ast,
extrusion.codeRef.range
)
: []
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Select default plane', type: 'Select default plane',
data: { data: {
type: 'defaultPlane', type: 'extrudeFace',
planeId: planeId, zAxis: [z_axis.x, z_axis.y, z_axis.z],
plane: defaultPlaneStrMap[planeId], yAxis: [y_axis.x, y_axis.y, y_axis.z],
zAxis, position: [origin.x, origin.y, origin.z].map(
yAxis, (num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none',
faceId: faceId,
}, },
}) })
return return
} })().catch(reportRejection)
const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
faceId,
engineCommandManager.artifactGraph
)
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
const codeRef =
artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
err(codeRef) ? [0, 0] : codeRef.range
)
const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange(
kclManager.ast,
extrusion.codeRef.range
)
: []
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none',
faceId: faceId,
},
})
return
} }
: () => {}, : () => {},
}) })

View File

@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
} }
useEffect(() => { useEffect(() => {
ctx.settings.send('Set all settings', { ctx.settings.send({
type: 'Set all settings',
settings: routeData, settings: routeData,
}) })
}, []) }, [])

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate' import { AnyStateMachine, Actor, StateFrom } from 'xstate'
import { createMachineCommand } from '../lib/createMachineCommand' import { createMachineCommand } from '../lib/createMachineCommand'
import { useCommandsContext } from './useCommandsContext' import { useCommandsContext } from './useCommandsContext'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { useAppState } from 'AppState' import { useAppState } from 'AppState'
import { getActorNextEvents } from 'lib/utils'
// This might not be necessary, AnyStateMachine from xstate is working // This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines = export type AllMachines =
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
machineId: T['id'] machineId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean allCommandsRequireNetwork?: boolean
onCancel?: () => void onCancel?: () => void
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
overallState !== NetworkHealthState.Weak) || overallState !== NetworkHealthState.Weak) ||
isExecuting || isExecuting ||
!isStreamReady !isStreamReady
const newCommands = state.nextEvents const newCommands = getActorNextEvents(state)
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons) .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.flatMap((type) => .flatMap((type) =>

View File

@ -3,13 +3,14 @@ import {
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { trap } from 'lib/trap' import { reportRejection, trap } from 'lib/trap'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm' import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -23,8 +24,7 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
const parsed = parse(recast(ast)) const parsed = ast
if (trap(parsed)) return
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(
parsed, parsed,
@ -63,7 +63,7 @@ export function useConvertToVariable(range?: SourceRange) {
} }
} }
editorManager.convertToVariableCallback = handleClick editorManager.convertToVariableCallback = toSync(handleClick, reportRejection)
return { enable, handleClick } return { enable, handleClick }
} }

View File

@ -267,7 +267,8 @@ code {
} }
.segment-length-label-text { .segment-length-label-text {
transform: translate(var(--x, 0), var(--y, 0)); transform: translate(var(--x, 0), var(--y, 0)) rotate(var(--degree, 0));
@apply font-mono body-bg px-2 pt-0.5 rounded-sm border border-chalkboard-110 dark:border-chalkboard-10;
} }
@layer components { @layer components {
@ -275,7 +276,7 @@ code {
@apply font-mono text-xs inline-block px-0.5 py-[2px] rounded; @apply font-mono text-xs inline-block px-0.5 py-[2px] rounded;
/* This is the only place in our code where layout is impacted by theme. /* This is the only place in our code where layout is impacted by theme.
* We may not want that later, if hotkeys are possibly visible * We may not want that later, if hotkeys are possibly visible
* while switching theme, but more padding feels better in dark mode. * while switching theme, but more padding feels better in dark mode.
*/ */
@apply dark:px-1; @apply dark:px-1;
@ -287,32 +288,11 @@ code {
} }
@layer utilities { @layer utilities {
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */ /*
@keyframes circle-in-hesitate { This is where your own custom Tailwind utility classes can go,
0% { which lets you use them with @apply in your CSS, and get
clip-path: circle( autocomplete in classNames in your JSX.
var(--circle-size-start, 0%) at var(--circle-x, 50%) */
var(--circle-y, 50%)
);
}
40% {
clip-path: circle(
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
);
}
100% {
clip-path: circle(
var(--circle-size-end, 125%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
}
.in-circle-hesitate {
animation: var(--circle-duration, 2.5s)
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
both;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

@ -129,8 +129,8 @@ export class KclManager {
if (!isExecuting && this.executeIsStale) { if (!isExecuting && this.executeIsStale) {
const args = this.executeIsStale const args = this.executeIsStale
this.executeIsStale = null this.executeIsStale = null
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.executeAst(args) this.executeAst(args)
} else {
} }
this._isExecutingCallback(isExecuting) this._isExecutingCallback(isExecuting)
} }
@ -154,6 +154,7 @@ export class KclManager {
constructor(engineCommandManager: EngineCommandManager) { constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(() => { this.ensureWasmInit().then(() => {
this.ast = this.safeParse(codeManager.code) || this.ast this.ast = this.safeParse(codeManager.code) || this.ast
}) })
@ -400,9 +401,11 @@ export class KclManager {
// Update the code state and the editor. // Update the code state and the editor.
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
// Write back to the file system. // Write back to the file system.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile() codeManager.writeToFile()
// execute the code. // execute the code.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.executeCode() this.executeCode()
} }
// There's overlapping responsibility between updateAst and executeAst. // There's overlapping responsibility between updateAst and executeAst.
@ -541,6 +544,7 @@ function defaultSelectionFilter(
programMemory: ProgramMemory, programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
programMemory.hasSketchOrExtrudeGroup() && programMemory.hasSketchOrExtrudeGroup() &&
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

@ -64,6 +64,7 @@ export async function executeAst({
try { try {
if (!useFakeExecutor) { if (!useFakeExecutor) {
engineCommandManager.endSession() engineCommandManager.endSession()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
} }
const programMemory = await (useFakeExecutor const programMemory = await (useFakeExecutor

View File

@ -1,5 +1,5 @@
import { Selection } from 'lib/selections' import { Selection } from 'lib/selections'
import { err, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { import {
Program, Program,
CallExpression, CallExpression,
@ -938,115 +938,119 @@ export async function deleteFromSelection(
const expressionIndex = pathToNode[1][0] as number const expressionIndex = pathToNode[1][0] as number
astClone.body.splice(expressionIndex, 1) astClone.body.splice(expressionIndex, 1)
if (extrudeNameToDelete) { if (extrudeNameToDelete) {
await new Promise(async (resolve) => { await new Promise((resolve) => {
let currentVariableName = '' ;(async () => {
const pathsDependingOnExtrude: Array<{ let currentVariableName = ''
path: PathToNode const pathsDependingOnExtrude: Array<{
sketchName: string path: PathToNode
}> = [] sketchName: string
traverse(astClone, { }> = []
leave: (node) => { traverse(astClone, {
if (node.type === 'VariableDeclaration') { leave: (node) => {
currentVariableName = '' if (node.type === 'VariableDeclaration') {
} currentVariableName = ''
}, }
enter: async (node, path) => { },
if (node.type === 'VariableDeclaration') { enter: (node, path) => {
currentVariableName = node.declarations[0].id.name ;(async () => {
} if (node.type === 'VariableDeclaration') {
if ( currentVariableName = node.declarations[0].id.name
// match startSketchOn(${extrudeNameToDelete}) }
node.type === 'CallExpression' && if (
node.callee.name === 'startSketchOn' && // match startSketchOn(${extrudeNameToDelete})
node.arguments[0].type === 'Identifier' && node.type === 'CallExpression' &&
node.arguments[0].name === extrudeNameToDelete node.callee.name === 'startSketchOn' &&
) { node.arguments[0].type === 'Identifier' &&
pathsDependingOnExtrude.push({ node.arguments[0].name === extrudeNameToDelete
path, ) {
sketchName: currentVariableName, pathsDependingOnExtrude.push({
}) path,
} sketchName: currentVariableName,
}, })
}) }
const roundLiteral = (x: number) => createLiteral(roundOff(x)) })().catch(reportRejection)
const modificationDetails: { },
parent: PipeExpression['body']
faceDetails: Models['FaceIsPlanar_type']
lastKey: number
}[] = []
for (const { path, sketchName } of pathsDependingOnExtrude) {
const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
)
if (err(parent)) {
return
}
const sketchToPreserve = sketchGroupFromKclValue(
programMemory.get(sketchName),
sketchName
)
if (err(sketchToPreserve)) return sketchToPreserve
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
return
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
faceDetails,
lastKey,
}) })
} const roundLiteral = (x: number) => createLiteral(roundOff(x))
for (const { parent, faceDetails, lastKey } of modificationDetails) { const modificationDetails: {
if ( parent: PipeExpression['body']
!( faceDetails: Models['FaceIsPlanar_type']
faceDetails.origin && lastKey: number
faceDetails.x_axis && }[] = []
faceDetails.y_axis && for (const { path, sketchName } of pathsDependingOnExtrude) {
faceDetails.z_axis const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
) )
) { if (err(parent)) {
continue return
}
const sketchToPreserve = sketchGroupFromKclValue(
programMemory.get(sketchName),
sketchName
)
if (err(sketchToPreserve)) return sketchToPreserve
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
// is three engine calls in one and they conflict
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
if (
!(
faceDetails.origin &&
faceDetails.x_axis &&
faceDetails.y_axis &&
faceDetails.z_axis
)
) {
return
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
faceDetails,
lastKey,
})
} }
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [ for (const { parent, faceDetails, lastKey } of modificationDetails) {
createObjectExpression({ if (
plane: createObjectExpression({ !(
origin: createObjectExpression({ faceDetails.origin &&
x: roundLiteral(faceDetails.origin.x), faceDetails.x_axis &&
y: roundLiteral(faceDetails.origin.y), faceDetails.y_axis &&
z: roundLiteral(faceDetails.origin.z), faceDetails.z_axis
}), )
x_axis: createObjectExpression({ ) {
x: roundLiteral(faceDetails.x_axis.x), continue
y: roundLiteral(faceDetails.x_axis.y), }
z: roundLiteral(faceDetails.x_axis.z), parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
}), createObjectExpression({
y_axis: createObjectExpression({ plane: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x), origin: createObjectExpression({
y: roundLiteral(faceDetails.y_axis.y), x: roundLiteral(faceDetails.origin.x),
z: roundLiteral(faceDetails.y_axis.z), y: roundLiteral(faceDetails.origin.y),
}), z: roundLiteral(faceDetails.origin.z),
z_axis: createObjectExpression({ }),
x: roundLiteral(faceDetails.z_axis.x), x_axis: createObjectExpression({
y: roundLiteral(faceDetails.z_axis.y), x: roundLiteral(faceDetails.x_axis.x),
z: roundLiteral(faceDetails.z_axis.z), y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z),
}),
y_axis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z),
}),
z_axis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z),
}),
}), }),
}), }),
}), ])
]) }
} resolve(true)
resolve(true) })().catch(reportRejection)
}) })
} }
// await prom // await prom

View File

@ -36,7 +36,7 @@ beforeAll(async () => {
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {}, modifyGrid: async () => {},
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: () => {
resolve(true) resolve(true)
}, },
}) })

View File

@ -49,6 +49,22 @@ export function applyFilletToSelection(
): void | Error { ): void | Error {
// 1. get AST // 1. get AST
let ast = kclManager.ast let ast = kclManager.ast
// 2. modify ast clone with fillet and tag
const result = modifyAstWithFilletAndTag(ast, selection, radius)
if (err(result)) return result
const { modifiedAst, pathToFilletNode } = result
// 3. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToFilletNode)
}
function modifyAstWithFilletAndTag(
ast: Program,
selection: Selections,
radius: KclCommandValue
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
const astResult = insertRadiusIntoAst(ast, radius) const astResult = insertRadiusIntoAst(ast, radius)
if (err(astResult)) return astResult if (err(astResult)) return astResult
@ -77,8 +93,7 @@ export function applyFilletToSelection(
if (trap(addFilletResult)) return addFilletResult if (trap(addFilletResult)) return addFilletResult
const { modifiedAst, pathToFilletNode } = addFilletResult const { modifiedAst, pathToFilletNode } = addFilletResult
// 4. update ast return { modifiedAst, pathToFilletNode }
updateAstAndFocus(modifiedAst, pathToFilletNode)
} }
function insertRadiusIntoAst( function insertRadiusIntoAst(

View File

@ -124,6 +124,7 @@ beforeAll(async () => {
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {}, modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => { callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [
CodeKey, CodeKey,

View File

@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
export type ArtifactId = string
interface CommonCommandProperties { interface CommonCommandProperties {
range: SourceRange range: SourceRange
pathToNode: PathToNode pathToNode: PathToNode
@ -10,7 +12,7 @@ interface CommonCommandProperties {
export interface PlaneArtifact { export interface PlaneArtifact {
type: 'plane' type: 'plane'
pathIds: Array<string> pathIds: Array<ArtifactId>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
export interface PlaneArtifactRich { export interface PlaneArtifactRich {
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
export interface PathArtifact { export interface PathArtifact {
type: 'path' type: 'path'
planeId: string planeId: ArtifactId
segIds: Array<string> segIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
solid2dId?: string solid2dId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface solid2D { interface solid2D {
type: 'solid2D' type: 'solid2D'
pathId: string pathId: ArtifactId
} }
export interface PathArtifactRich { export interface PathArtifactRich {
type: 'path' type: 'path'
@ -42,10 +44,10 @@ export interface PathArtifactRich {
interface SegmentArtifact { interface SegmentArtifact {
type: 'segment' type: 'segment'
pathId: string pathId: ArtifactId
surfaceId: string surfaceId: ArtifactId
edgeIds: Array<string> edgeIds: Array<ArtifactId>
edgeCutId?: string edgeCutId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface SegmentArtifactRich { interface SegmentArtifactRich {
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
interface ExtrusionArtifact { interface ExtrusionArtifact {
type: 'extrusion' type: 'extrusion'
pathId: string pathId: ArtifactId
surfaceIds: Array<string> surfaceIds: Array<ArtifactId>
edgeIds: Array<string> edgeIds: Array<ArtifactId>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface ExtrusionArtifactRich { interface ExtrusionArtifactRich {
@ -74,23 +76,23 @@ interface ExtrusionArtifactRich {
interface WallArtifact { interface WallArtifact {
type: 'wall' type: 'wall'
segId: string segId: ArtifactId
edgeCutEdgeIds: Array<string> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
pathIds: Array<string> pathIds: Array<ArtifactId>
} }
interface CapArtifact { interface CapArtifact {
type: 'cap' type: 'cap'
subType: 'start' | 'end' subType: 'start' | 'end'
edgeCutEdgeIds: Array<string> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: string extrusionId: ArtifactId
pathIds: Array<string> pathIds: Array<ArtifactId>
} }
interface ExtrudeEdge { interface ExtrudeEdge {
type: 'extrudeEdge' type: 'extrudeEdge'
segId: string segId: ArtifactId
extrusionId: string extrusionId: ArtifactId
subType: 'opposite' | 'adjacent' subType: 'opposite' | 'adjacent'
} }
@ -98,16 +100,16 @@ interface ExtrudeEdge {
interface EdgeCut { interface EdgeCut {
type: 'edgeCut' type: 'edgeCut'
subType: 'fillet' | 'chamfer' subType: 'fillet' | 'chamfer'
consumedEdgeId: string consumedEdgeId: ArtifactId
edgeIds: Array<string> edgeIds: Array<ArtifactId>
surfaceId: string surfaceId: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface EdgeCutEdge { interface EdgeCutEdge {
type: 'edgeCutEdge' type: 'edgeCutEdge'
edgeCutId: string edgeCutId: ArtifactId
surfaceId: string surfaceId: ArtifactId
} }
export type Artifact = export type Artifact =
@ -122,7 +124,7 @@ export type Artifact =
| EdgeCutEdge | EdgeCutEdge
| solid2D | solid2D
export type ArtifactGraph = Map<string, Artifact> export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type'] export type EngineCommand = Models['WebSocketRequest_type']
@ -149,7 +151,7 @@ export function createArtifactGraph({
responseMap: ResponseMap responseMap: ResponseMap
ast: Program ast: Program
}) { }) {
const myMap = new Map<string, Artifact>() const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */ /** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = '' let currentPlaneId = ''
@ -166,7 +168,7 @@ export function createArtifactGraph({
const artifactsToUpdate = getArtifactsToUpdate({ const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand, orderedCommand,
responseMap, responseMap,
getArtifact: (id: string) => myMap.get(id), getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId, currentPlaneId,
ast, ast,
}) })
@ -224,11 +226,11 @@ export function getArtifactsToUpdate({
orderedCommand: OrderedCommand orderedCommand: OrderedCommand
responseMap: ResponseMap responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */ /** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: string) => Artifact | undefined getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: string currentPlaneId: ArtifactId
ast: Program ast: Program
}): Array<{ }): Array<{
id: string id: ArtifactId
artifact: Artifact artifact: Artifact
}> { }> {
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
@ -514,7 +516,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
(!predicate || (!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>)) predicate(value as Extract<Artifact, { type: T[number] }>))
) )
) as Map<string, Extract<Artifact, { type: T[number] }>> ) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
} }
export function getArtifactsOfTypes<T extends Artifact['type'][]>( export function getArtifactsOfTypes<T extends Artifact['type'][]>(
@ -528,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
}, },
map: ArtifactGraph map: ArtifactGraph
): Map<string, Extract<Artifact, { type: T[number] }>> { ): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
return new Map( return new Map(
[...map].filter( [...map].filter(
([key, value]) => ([key, value]) =>
@ -537,7 +539,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
(!predicate || (!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>)) predicate(value as Extract<Artifact, { type: T[number] }>))
) )
) as Map<string, Extract<Artifact, { type: T[number] }>> ) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
} }
export function getArtifactOfTypes<T extends Artifact['type'][]>( export function getArtifactOfTypes<T extends Artifact['type'][]>(
@ -545,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
key, key,
types, types,
}: { }: {
key: string key: ArtifactId
types: T types: T
}, },
map: ArtifactGraph map: ArtifactGraph
@ -718,7 +720,7 @@ export function getExtrudeEdgeCodeRef(
} }
export function getExtrusionFromSuspectedExtrudeSurface( export function getExtrusionFromSuspectedExtrudeSurface(
id: string, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): ExtrusionArtifact | Error {
const artifact = getArtifactOfTypes( const artifact = getArtifactOfTypes(
@ -733,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
} }
export function getExtrusionFromSuspectedPath( export function getExtrusionFromSuspectedPath(
id: string, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): ExtrusionArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph) const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)

View File

@ -18,6 +18,7 @@ import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes' import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants' import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000 const pingIntervalMs = 5_000
@ -388,11 +389,12 @@ class EngineConnection extends EventTarget {
default: default:
if (this.isConnecting()) break if (this.isConnecting()) break
// Means we never could do an initial connection. Reconnect everything. // Means we never could do an initial connection. Reconnect everything.
if (!this.pingPongSpan.ping) this.connect() if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.connect() this.connect()
} }
@ -1252,6 +1254,10 @@ export type CommandLog =
type: 'execution-done' type: 'execution-done'
data: null data: null
} }
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents { export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run // engineConnection is available but scene setup may not have run
@ -1460,6 +1466,7 @@ export class EngineCommandManager extends EventTarget {
}) })
) )
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.onEngineConnectionOpened = async () => { this.onEngineConnectionOpened = async () => {
// Set the stream background color // Set the stream background color
// This takes RGBA values from 0-1 // This takes RGBA values from 0-1
@ -1476,6 +1483,7 @@ export class EngineCommandManager extends EventTarget {
// Sets the default line colors // Sets the default line colors
const opposingTheme = getOppositeTheme(this.settings.theme) const opposingTheme = getOppositeTheme(this.settings.theme)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1486,6 +1494,7 @@ export class EngineCommandManager extends EventTarget {
}) })
// Set the edge lines visibility // Set the edge lines visibility
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -1496,6 +1505,7 @@ export class EngineCommandManager extends EventTarget {
}) })
this._camControlsCameraChange() this._camControlsCameraChange()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendSceneCommand({ this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events // CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially // firing this at connection ensure the camera's are synced initially
@ -1508,6 +1518,7 @@ export class EngineCommandManager extends EventTarget {
// We want modify the grid first because we don't want it to flash. // We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do // Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282 // that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => { this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes() await this.initPlanes()
setIsStreamReady(true) setIsStreamReady(true)
@ -1711,6 +1722,7 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionNewTrack as EventListener this.onEngineConnectionNewTrack as EventListener
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineConnection?.connect() this.engineConnection?.connect()
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
@ -1918,7 +1930,13 @@ export class EngineCommandManager extends EventTarget {
} else if (cmd.type === 'export') { } else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => { const promise = new Promise<null>((resolve, reject) => {
this.pendingExport = { this.pendingExport = {
resolve, resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
data: null,
})
resolve(passThrough)
},
reject: (reason: string) => { reject: (reason: string) => {
this.exportIntent = null this.exportIntent = null
reject(reason) reject(reason)
@ -2115,6 +2133,7 @@ export class EngineCommandManager extends EventTarget {
* @param visible - whether to show or hide the scale grid * @param visible - whether to show or hide the scale grid
*/ */
setScaleGridVisibility(visible: boolean) { setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible) this.modifyGrid(!visible)
} }

View File

@ -360,6 +360,7 @@ export const executor = async (
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
@ -569,6 +570,7 @@ export async function coreDump(
a new GitHub issue for the user. a new GitHub issue for the user.
*/ */
if (openGithubIssue && dump.github_issue_url) { if (openGithubIssue && dump.github_issue_url) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
openWindow(dump.github_issue_url) openWindow(dump.github_issue_url)
} else { } else {
console.error( console.error(

View File

@ -6,7 +6,7 @@ const META =
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super' PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt' const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
const noModifiersPressed = (e: React.MouseEvent) => const noModifiersPressed = (e: React.MouseEvent | MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem = export type CameraSystem =
@ -53,14 +53,14 @@ export function mouseControlsToCameraSystem(
interface MouseGuardHandler { interface MouseGuardHandler {
description: string description: string
callback: (e: React.MouseEvent) => boolean callback: (e: React.MouseEvent | MouseEvent) => boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
interface MouseGuardZoomHandler { interface MouseGuardZoomHandler {
description: string description: string
dragCallback: (e: React.MouseEvent) => boolean dragCallback: (e: React.MouseEvent | MouseEvent) => boolean
scrollCallback: (e: React.MouseEvent) => boolean scrollCallback: (e: React.MouseEvent | MouseEvent) => boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
@ -70,7 +70,7 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
export const btnName = (e: React.MouseEvent) => ({ export const btnName = (e: React.MouseEvent | MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1, middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2, right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0, left: !!(e.buttons & 1) || e.button === 0,

View File

@ -10,7 +10,7 @@ import {
} from 'lib/settings/settingsTypes' } from 'lib/settings/settingsTypes'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { PathValue } from 'lib/types' import { PathValue } from 'lib/types'
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
import { getPropertyByPath } from 'lib/objectPropertyByPath' import { getPropertyByPath } from 'lib/objectPropertyByPath'
import { buildCommandArgument } from 'lib/createMachineCommand' import { buildCommandArgument } from 'lib/createMachineCommand'
import decamelize from 'decamelize' import decamelize from 'decamelize'
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
) as SettingsPaths[] ) as SettingsPaths[]
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>( const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
actor: InterpreterFrom<T>, actor: Actor<T>,
isProjectAvailable: boolean, isProjectAvailable: boolean,
hideOnLevel?: SettingsLevel hideOnLevel?: SettingsLevel
): CommandArgument<SettingsLevel, T> => ({ ): CommandArgument<SettingsLevel, T> => ({
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
type: SettingsPaths type: SettingsPaths
send: Function send: Function
context: ContextFrom<typeof settingsMachine> context: ContextFrom<typeof settingsMachine>
actor: InterpreterFrom<typeof settingsMachine> actor: Actor<typeof settingsMachine>
isProjectAvailable: boolean isProjectAvailable: boolean
} }
@ -132,7 +132,7 @@ export function createSettingsCommand({
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
send({ type: `set.${type}`, data }) send({ type: `set.${type}`, data })
} else { } else {
send(type) send({ type })
} }
}, },
args: { args: {

View File

@ -1,11 +1,6 @@
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { AllMachines } from 'hooks/useStateMachineCommands' import { AllMachines } from 'hooks/useStateMachineCommands'
import { import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
} from 'xstate'
import { Selection } from './selections' import { Selection } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm' import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
@ -186,7 +181,7 @@ export type CommandArgument<
machineContext?: ContextFrom<T> machineContext?: ContextFrom<T>
) => boolean) ) => boolean)
skip?: boolean skip?: boolean
machineActor: InterpreterFrom<T> machineActor: Actor<T>
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
*/ */

View File

@ -2,7 +2,7 @@ import {
AnyStateMachine, AnyStateMachine,
ContextFrom, ContextFrom,
EventFrom, EventFrom,
InterpreterFrom, Actor,
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
groupId: T['id'] groupId: T['id']
state: StateFrom<T> state: StateFrom<T>
send: Function send: Function
actor: InterpreterFrom<T> actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S> commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void onCancel?: () => void
} }
@ -90,9 +90,9 @@ export function createMachineCommand<
needsReview: commandConfig.needsReview || false, needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => { onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) { if (data !== undefined && data !== null) {
send(type, { data }) send({ type, data })
} else { } else {
send(type) send({ type })
} }
}, },
} }
@ -124,7 +124,7 @@ function buildCommandArguments<
>( >(
state: StateFrom<T>, state: StateFrom<T>,
args: CommandConfig<T, CommandName, S>['args'], args: CommandConfig<T, CommandName, S>['args'],
machineActor: InterpreterFrom<T> machineActor: Actor<T>
): NonNullable<Command<T, CommandName, S>['args']> { ): NonNullable<Command<T, CommandName, S>['args']> {
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']> const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
@ -143,7 +143,7 @@ export function buildCommandArgument<
>( >(
arg: CommandArgumentConfig<O, T>, arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>, context: ContextFrom<T>,
machineActor: InterpreterFrom<T> machineActor: Actor<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,

View File

@ -8,7 +8,6 @@ import {
parseProjectSettings, parseProjectSettings,
} from 'lang/wasm' } from 'lang/wasm'
import { import {
DEFAULT_HOST,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
PROJECT_FOLDER, PROJECT_FOLDER,
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
@ -462,29 +461,60 @@ export const readProjectSettingsFile = async (
*/ */
export const readAppSettingsFile = async () => { export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath() let settingsPath = await getAppSettingsFilePath()
const initialProjectDirConfig: DeepPartial<
Configuration['settings']['project']
> = { directory: await getInitialDefaultDir() }
// The file exists, read it and parse it. // The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) { if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath) const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml) const parsedAppConfig = parseAppSettings(configToml)
if (err(configObj)) { if (err(parsedAppConfig)) {
return Promise.reject(configObj) return Promise.reject(parsedAppConfig)
} }
return configObj const hasProjectDirectorySetting =
parsedAppConfig.settings?.project?.directory ||
parsedAppConfig.settings?.app?.project_directory
if (hasProjectDirectorySetting) {
return parsedAppConfig
} else {
// inject the default project directory setting
const mergedConfig: DeepPartial<Configuration> = {
...parsedAppConfig,
settings: {
...parsedAppConfig.settings,
project: Object.assign(
{},
parsedAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
return mergedConfig
}
} }
// The file doesn't exist, create a new one. // The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings() const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) { if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig) return Promise.reject(defaultAppConfig)
} }
const initialDirConfig: DeepPartial<Configuration> = {
settings: { project: { directory: await getInitialDefaultDir() } }, // inject the default project directory setting
const mergedDefaultConfig: DeepPartial<Configuration> = {
...defaultAppConfig,
settings: {
...defaultAppConfig.settings,
project: Object.assign(
{},
defaultAppConfig.settings?.project,
initialProjectDirConfig
),
},
} }
const config = Object.assign(defaultAppConfig, initialDirConfig) return mergedDefaultConfig
return config
} }
export const writeAppSettingsFile = async (tomlStr: string) => { export const writeAppSettingsFile = async (tomlStr: string) => {
@ -525,28 +555,6 @@ export const getUser = async (
token: string, token: string,
hostname: string hostname: string
): Promise<Models['User_type']> => { ): Promise<Models['User_type']> => {
// Use the host passed in if it's set.
// Otherwise, use the default host.
const host = !hostname ? DEFAULT_HOST : hostname
// Change the baseURL to the one we want.
let baseurl = host
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
baseurl = `https://${host}`
if (host.indexOf('localhost') === 0) {
baseurl = `http://${host}`
}
}
// Use kittycad library to fetch the user info from /user/me
if (baseurl !== DEFAULT_HOST) {
// The TypeScript generated library uses environment variables for this
// because it was intended for NodeJS.
// Needs to stay like this because window.electron.kittycad needs it
// internally.
window.electron.setBaseUrl(baseurl)
}
try { try {
const user = await window.electron.kittycad('users.get_user_self', { const user = await window.electron.kittycad('users.get_user_self', {
client: { token }, client: { token },

View File

@ -14,7 +14,7 @@ const save_ = async (file: ModelingAppFile) => {
extensions.push(extension) extensions.push(extension)
} }
if (!(window as any).playwrightSkipFilePicker) { if (window.electron.process.env.IS_PLAYWRIGHT) {
// skip file picker, save to default location // skip file picker, save to default location
await window.electron.writeFile( await window.electron.writeFile(
file.name, file.name,

View File

@ -1,5 +1,7 @@
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { components } from './machine-api' import { components } from './machine-api'
import { reportRejection } from './trap'
import { toSync } from './utils'
export type MachinesListing = Array< export type MachinesListing = Array<
components['schemas']['MachineInfoResponse'] components['schemas']['MachineInfoResponse']
@ -17,7 +19,7 @@ export class MachineManager {
return return
} }
this.updateMachines() this.updateMachines().catch(reportRejection)
} }
start() { start() {
@ -31,11 +33,14 @@ export class MachineManager {
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => { const timeoutLoop = () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(async () => { timeoutId = setTimeout(
await this.updateMachineApiIp() toSync(async () => {
await this.updateMachines() await this.updateMachineApiIp()
timeoutLoop() await this.updateMachines()
}, 10000) timeoutLoop()
}, reportRejection),
10000
)
} }
timeoutLoop() timeoutLoop()
} }

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