Compare commits

...

28 Commits

Author SHA1 Message Date
d2a3bfdb5a Revert "Ensure auth device token is saved to a file so it persists upgrades and reinstalls (#3640)"
This reverts commit f6bb10170d.
2024-08-27 10:12:25 +10:00
dbdc7e5c8b add maker wix (#3668)
* add maker wix

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-08-26 13:54:18 -07:00
f6bb10170d Ensure auth device token is saved to a file so it persists upgrades and reinstalls (#3640)
* ensure auth device token is saved to a file so it persists upgrades and reinstalls
#3639

* write file on check log in

* write file on check log in

* clean up
2024-08-27 05:59:25 +10:00
972dca8743 Change mouse controls display to be easier to understand (#3648)
* Change mouse controls display to be easier to understand

* Fix to not duplicate default camera controls

* Change "Scroll wheel" to "Scroll" on all platforms
2024-08-26 15:05:33 -04:00
e9e933eecd remove priviledged schemes (#3666)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-26 18:27:20 +00:00
2b1315423f Bump serde_json from 1.0.125 to 1.0.127 in /src/wasm-lib (#3662)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.125 to 1.0.127.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/1.0.125...1.0.127)

---
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-08-26 10:03:11 -07:00
bd4c24bc04 Remove references to src-tauri (#3663)
* Remove references to src-tauri

* Remove more ignores from the electron migration
2024-08-26 16:31:28 +00:00
50cc88977c Bump micromatch from 4.0.7 to 4.0.8 (#3649)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 16:16:23 +00:00
bea9a1c3ec Bump syn from 2.0.75 to 2.0.76 in /src/wasm-lib (#3659)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.75 to 2.0.76.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.75...2.0.76)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 08:56:37 -07:00
f43411fdb4 Bump serde from 1.0.208 to 1.0.209 in /src/wasm-lib (#3661)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.208 to 1.0.209.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.208...v1.0.209)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 08:55:52 -07:00
max
c2e9d18f92 3036 tests add fillet (#3530)
* addFillet.ts - refactor existing code

* move logic from modelingMachine to addFillet

* rename getPathForSelection into getPathToExtrudeForSegmentSelection

* stuck with kclManager

* stuck 2

* remove engineless exe from fillet test

* pathToExtrudeNode properly tested

* resolve conflicts

* engine initialization update

* cleanup comments

* passed ExecuteArgs instead of Program to executeAst

* afterAll engineCommandManager.tearDown

* resolve conflicts

* mutateAstForRadiusInsertion

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

* save banner from hulk mutations

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

* sweet errors

* purging the as

* make type of getNodeFromPath safe again

* as cleaning part 2

* cleared mutation logic

* last bits

* make the linter happy

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-26 08:07:20 +02:00
199722c505 fix and tests (#3656)
* fix and tests

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-08-25 22:14:38 +00:00
f9699d174c header changes and open new window for double click in finder macos (#3652)
* header changes and open new window for double click in finder macos

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

* cleanup

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

* add protocols

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

* updates

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

* extend with info.plist

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-08-25 19:56:11 +00:00
590a6479e0 double-click to open / open from cli (#3643)
* fixes

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

* add tests

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

* updates

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

* Look at this (photo)Graph *in the voice of Nickelback*

* remove unneeded rust

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

* remove dep on clap

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

* fixups for imports

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

* updates

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

* fix types

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

* bump

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-25 00:47:09 +00:00
fbf0d3d953 Nadro/3581/change base unit test (#3621)
* fix: Updating the playwright tests section

* fix: cleaning up formatting for playwright test markdown

* fix: autocomplete put a 3rd *

* chore: e2e playwright for change of base unit in multiple scenarios

* fix: fmt auto format

* fix: fmt and clean up for PR

* fix: removing typo

* fix: added the ) formatting back

* fix: linting errors + formatting

* fix: removing unused testing code from previous logic

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-24 23:51:50 +00:00
3dd66bc8d2 Add test for not creating main.kcl when there are nested directories (#3647)
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-24 23:51:36 +00:00
a928b8fbd0 Update README.md to add updating upstreams for kcl-lib (#3650)
Update README.md
2024-08-24 16:33:01 -07:00
d2349bec2b chore: implemented e2e tests for text-to-cad file workflows (#3645) 2024-08-24 15:31:27 -07:00
6e10f75ff6 KCL executor: accept and send 'replay' flag, track session data (#3646)
Part of https://github.com/KittyCAD/engine/issues/2504:

The engine accepts this 'replay' flag now, so, accept it too and send it up to the engine.

Part of https://github.com/KittyCAD/cli/issues/847

The engine sends 'session data' now (like the API Call ID). The CLI executes KCL using this executor, and would like to get the session data after execution.
2024-08-23 17:40:30 -05:00
03e289af20 Fix Commands button to show correct shortcut on Windows and Linux (#3625)
* Fix Commands button to show correct shortcut

* Fix onboarding to use the same shortcut reference

* Rename test file to be more general

* Add test for commands button text

* Remove outdated reference to Ctrl+/

* Change shortcut separator to be + and no spaces

* Add JSDocs and improve comments

* Add unit tests

* Change control modifier to regular ASCII caret

* Add browser test and fix platform detection

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

* Add useful debug info to the error message

* Fix to display metaKey as Super on Linux

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

This reverts commit f8da90d5d2.

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

* Approve snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-23 16:20:22 -04:00
efc140abbf Test: Can load a file with CRLF line endings (#3636)
* Test: Can load a file with CRLF line endings #3616

* first arg stuff??

* Fix paths in playwright for windows

* Fix line ending replace on windows

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

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

---------

Co-authored-by: Adam Sunderland <iterion@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: Adam Sunderland <adam@kittycad.io>
2024-08-23 13:51:30 -04:00
4dfad19b7e add hollow (#3642)
* add hollow

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

* 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-08-23 16:57:02 +00:00
e56c634b35 Fix existing: Zoom should be consistent when exiting or entering sketches (#3638)
#3637
2024-08-23 16:10:46 +00:00
00292abc98 Bump @babel/preset-env from 7.25.3 to 7.25.4 (#3630)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.25.3 to 7.25.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.4/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-23 09:00:56 -07:00
483d6903d6 Bump @kittycad/lib from 2.0.0 to 2.0.1 (#3631)
Bumps [@kittycad/lib](https://github.com/KittyCAD/kittycad.ts) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/KittyCAD/kittycad.ts/releases)
- [Commits](https://github.com/KittyCAD/kittycad.ts/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: "@kittycad/lib"
  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-08-23 09:00:48 -07:00
3780996374 Fix opening bottom right version link in external browser (#3633) 2024-08-23 09:00:18 -07:00
2fde71228a Bump quote from 1.0.36 to 1.0.37 in /src/wasm-lib (#3628)
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.36 to 1.0.37.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.36...1.0.37)

---
updated-dependencies:
- dependency-name: quote
  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-08-23 08:48:53 -07:00
5cd8ab3812 Enable all disabled win32 tests (#3618)
* Enable all disabled win32 tests

* Skip one test
2024-08-23 10:50:40 -04:00
112 changed files with 8732 additions and 2314 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock

View File

@ -37,4 +37,4 @@ jobs:
# We specifically want to test the disable-println feature # We specifically want to test the disable-println feature
# Since it is not enabled by default, we need to specify it # Since it is not enabled by default, we need to specify it
# This is used in kcl-lsp # This is used in kcl-lsp
cargo check --all --features disable-println --features pyo3 cargo check --all --features disable-println --features pyo3 --features cli

View File

@ -38,11 +38,6 @@ jobs:
with: with:
toolchain: stable toolchain: stable
override: true override: true
- name: install dependencies
if: matrix.dir == 'src-tauri'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install vector - name: Install vector
run: | run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh

5
.gitignore vendored
View File

@ -54,7 +54,6 @@ e2e/playwright/export-snapshots/*
## generated files ## generated files
src/**/*.typegen.ts src/**/*.typegen.ts
src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile Mac_App_Distribution.provisionprofile
@ -66,7 +65,3 @@ venv
# electron # electron
out/ out/
src-tauri/target
electron-test-projects-dir
electron-test-projects-dir-2

344
Info.plist Normal file
View File

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.kcl</string>
</array>
<key>CFBundleTypeName</key>
<string>KCL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.toml</string>
</array>
<key>CFBundleTypeName</key>
<string>TOML</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.gltf</string>
</array>
<key>CFBundleTypeName</key>
<string>glTF</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.glb</string>
</array>
<key>CFBundleTypeName</key>
<string>glb</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.step</string>
</array>
<key>CFBundleTypeName</key>
<string>STEP</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.fbx</string>
</array>
<key>CFBundleTypeName</key>
<string>FBX</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.sldprt</string>
</array>
<key>CFBundleTypeName</key>
<string>Solidworks Part</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.geometry-definition-format</string>
</array>
<key>CFBundleTypeName</key>
<string>OBJ</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.polygon-file-format</string>
</array>
<key>CFBundleTypeName</key>
<string>PLY</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.standard-tesselated-geometry-format</string>
</array>
<key>CFBundleTypeName</key>
<string>STL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.folder</string>
</array>
<key>CFBundleTypeName</key>
<string>Folders</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.kcl</string>
<key>UTTypeReferenceURL</key>
<string>https://zoo.dev/docs/kcl</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.source-code</string>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.script</string>
</array>
<key>UTTypeDescription</key>
<string>KCL (KittyCAD Language) document</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.zoo.kcl</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.gltf</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF)</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gltf</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf+json</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.glb</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF) binary</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>glb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf-binary</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.step</string>
<key>UTTypeReferenceURL</key>
<string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>STEP-file, ISO 10303-21</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>step</string>
<string>stp</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/step</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.sldprt</string>
<key>UTTypeReferenceURL</key>
<string>https://docs.fileformat.com/cad/sldprt/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Solidworks Part</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>sldprt</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.solidworks.sldprt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.fbx</string>
<key>UTTypeReferenceURL</key>
<string>https://en.wikipedia.org/wiki/FBX</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Autodesk Filmbox (FBX) format</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fbx</string>
<string>fbxb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.autodesk.fbx</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.toml</string>
<key>UTTypeReferenceURL</key>
<string>https://toml.io/en/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Tom's Obvious Minimal Language</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/toml</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -189,12 +189,22 @@ For more information on fuzzing you can check out
### Playwright tests ### Playwright tests
You will need a `./e2e/playwright/playwright-secrets.env` file:
```bash
$ touch ./e2e/playwright/playwright-secrets.env
$ cat ./e2e/playwright/playwright-secrets.env
token=<dev.zoo.dev/account/api-tokens>
snapshottoken=<your-snapshot-token>
```
For a portable way to run Playwright you'll need Docker. For a portable way to run Playwright you'll need Docker.
#### Generic example
After that, open a terminal and run: After that, open a terminal and run:
```bash ```bash
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1 docker run --network host --rm --init -it playwright/chrome:playwright-x.xx.x
``` ```
and in another terminal, run: and in another terminal, run:
@ -203,21 +213,27 @@ and in another terminal, run:
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite> PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
``` ```
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE: #### Specific example
open a terminal and run:
```bash ```bash
# ./e2e/playwright/playwright-secrets.env docker run --network host --rm --init -it playwright/chrome:playwright-1.46.0
token=<your-token> ```
snapshottoken=<your-snapshot-token>
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts
``` ```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
run a specific test change the test from `test('...` to `test.only('...` run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests) (note if you commit this, the tests will instantly fail without running any of the tests)
**Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date.
run headed run headed
``` ```

835
docs/kcl/hollow.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -44,6 +44,7 @@ layout: manual
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix) * [`helix`](kcl/helix)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow)
* [`import`](kcl/import) * [`import`](kcl/import)
* [`inch`](kcl/inch) * [`inch`](kcl/inch)
* [`int`](kcl/int) * [`int`](kcl/int)

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,39 @@ test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
}) })
test.describe('Electron user sidebar menu tests', () => { test.describe('Electron app header tests', () => {
test(
'Open Command Palette button has correct shortcut',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// No space before the shortcut since it checks textContent.
let text
switch (process.platform) {
case 'darwin':
text = 'Commands⌘K'
break
case 'win32':
text = 'CommandsCtrl+K'
break
default: // 'linux' etc.
text = 'CommandsCtrl+K'
break
}
const commandsButton = page.getByRole('button', { name: 'Commands' })
await expect(commandsButton).toBeVisible()
await expect(commandsButton).toHaveText(text)
await electronApp.close()
}
)
test( test(
'User settings has correct shortcut', 'User settings has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -1,6 +1,13 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { getUtils, setup, setupElectron, tearDown } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { join } from 'path'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises' import fsp from 'fs/promises'
@ -223,26 +230,24 @@ test(
'Opening multiple panes persists when switching projects', 'Opening multiple panes persists when switching projects',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
// Setup multiple projects. // Setup multiple projects.
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
const bracketDir = join(dir, 'bracket')
await Promise.all([ await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), fsp.mkdir(routerTemplateDir, { recursive: true }),
fsp.mkdir(`${dir}/bracket`, { recursive: true }), fsp.mkdir(bracketDir, { recursive: true }),
]) ])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', executorInputPath('router-template-slate.kcl'),
`${dir}/router-template-slate/main.kcl` join(routerTemplateDir, 'main.kcl')
), ),
fsp.copyFile( fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
), ),
]) ])
}, },

View File

@ -1,5 +1,11 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { getUtils, setupElectron, tearDown } from './test-utils' import { join } from 'path'
import {
getUtils,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -10,22 +16,19 @@ test(
'export works on the first try', 'export works on the first try',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await Promise.all([fsp.mkdir(`${dir}/bracket`, { recursive: true })]) const bracketDir = join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', executorInputPath('router-template-slate.kcl'),
`${dir}/bracket/other.kcl` join(bracketDir, 'other.kcl')
), ),
fsp.copyFile( fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
), ),
]) ])
}, },

View File

@ -84,6 +84,63 @@ test.describe('Editor tests', () => {
|> close(%)`) |> close(%)`)
}) })
test('if you click the format button it formats your code and executes so lints are still there', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
await page.keyboard.type(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
})
test('fold gutters work', async ({ page }) => { test('fold gutters work', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -241,6 +298,67 @@ test.describe('Editor tests', () => {
|> close(%)`) |> close(%)`)
}) })
test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
localStorage.setItem('disableAxis', 'true')
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
// focus the editor
await u.codeLocator.click()
// Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF')
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
})
test('if you write kcl with lint errors you get lints', async ({ page }) => { test('if you write kcl with lint errors you get lints', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })

View File

@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { setupElectron, tearDown } from './test-utils' import { setupElectron, tearDown, executorInputPath } from './test-utils'
import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -10,17 +11,14 @@ test(
'When machine-api server not found butt is disabled and shows the reason', 'When machine-api server not found butt is disabled and shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
) )
}, },
}) })
@ -58,17 +56,14 @@ test(
'When machine-api server not found home screen & project status shows the reason', 'When machine-api server not found home screen & project status shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
) )
}, },
}) })

View File

@ -1,6 +1,13 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { import {
@ -347,17 +354,14 @@ test(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', executorInputPath('router-template-slate.kcl'),
`${dir}/router-template-slate/main.kcl` join(routerTemplateDir, 'main.kcl')
) )
}, },
}) })

View File

@ -1,6 +1,7 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { import {
doExport, doExport,
executorInputPath,
getUtils, getUtils,
isOutOfViewInScrollContainer, isOutOfViewInScrollContainer,
Paths, Paths,
@ -49,17 +50,11 @@ test(
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'bracket'), { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
join( executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
'src', join(bracketDir, 'main.kcl')
'wasm-lib',
'tests',
'executor',
'inputs',
'focusrite_scarlett_mounting_braket.kcl'
),
join(dir, 'bracket', 'main.kcl')
) )
}, },
}) })
@ -99,14 +94,7 @@ test(
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'broken-code'), { recursive: true }) await fsp.mkdir(join(dir, 'broken-code'), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
join( executorInputPath('broken-code-test.kcl'),
'src',
'wasm-lib',
'tests',
'executor',
'inputs',
'broken-code-test.kcl'
),
join(dir, 'broken-code', 'main.kcl') join(dir, 'broken-code', 'main.kcl')
) )
}, },
@ -143,17 +131,14 @@ test.describe('Can export from electron app', () => {
`Can export using ${method}`, `Can export using ${method}`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
) )
}, },
}) })
@ -469,6 +454,7 @@ test(
await electronApp.close() await electronApp.close()
} }
) )
test( test(
'File in the file pane should open with a single click', 'File in the file pane should open with a single click',
{ tag: '@electron' }, { tag: '@electron' },
@ -521,6 +507,69 @@ test(
} }
) )
test(
'Nested directories in project without main.kcl do not create main.kcl',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
let testDir: string | undefined
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'router-template-slate', 'nested'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(dir, 'router-template-slate', 'nested', 'slate.kcl')
)
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(dir, 'router-template-slate', 'nested', 'bracket.kcl')
)
testDir = dir
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await test.step('Open the project', async () => {
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// It actually loads.
await expect(u.codeLocator).toContainText('mounting bracket')
await expect(u.codeLocator).toContainText('const radius =')
})
await u.openFilePanel()
// Find the current file.
const filesPane = page.locator('#files-pane')
await expect(filesPane.getByText('bracket.kcl')).toBeVisible()
// But there's no main.kcl in the file tree browser.
await expect(filesPane.getByText('main.kcl')).not.toBeVisible()
// No main.kcl file is created on the filesystem.
expect(testDir).toBeDefined()
if (testDir !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
await expect(
fsp.access(join(testDir, 'router-template-slate', 'main.kcl'))
).rejects.toThrow()
// eslint-disable-next-line jest/no-conditional-expect
await expect(
fsp.access(join(testDir, 'router-template-slate', 'nested', 'main.kcl'))
).rejects.toThrow()
}
await electronApp.close()
}
)
test( test(
'Deleting projects, can delete individual project, can still create projects after deleting all', 'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' }, { tag: '@electron' },
@ -605,6 +654,48 @@ test(
} }
) )
test(
'Can load a file with CRLF line endings',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
const file = await fsp.readFile(
executorInputPath('router-template-slate.kcl'),
'utf-8'
)
// Replace both \r optionally so we don't end up with \r\r\n
const fileWithCRLF = file.replace(/\r?\n/g, '\r\n')
await fsp.writeFile(
join(routerTemplateDir, 'main.kcl'),
fileWithCRLF,
'utf-8'
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await page.getByText('router-template-slate').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText('routerDiameter')
await expect(u.codeLocator).toContainText('templateGap')
await expect(u.codeLocator).toContainText('minClampingDistance')
await electronApp.close()
}
)
test( test(
'Can sort projects on home page', 'Can sort projects on home page',
{ tag: '@electron' }, { tag: '@electron' },
@ -1032,10 +1123,6 @@ test(
'Search projects on desktop home', 'Search projects on desktop home',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ browserName: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const projectData = [ const projectData = [
['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'], ['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['basic-cube', 'basic_fillet_cube_end.kcl'], ['basic-cube', 'basic_fillet_cube_end.kcl'],
@ -1050,7 +1137,7 @@ test(
for (const [name, file] of projectData) { for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true }) await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', file), executorInputPath(file),
join(dir, name, `main.kcl`) join(dir, name, `main.kcl`)
) )
} }
@ -1097,14 +1184,11 @@ test(
'file pane is scrollable when there are many files', 'file pane is scrollable when there are many files',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/testProject`, { recursive: true }) const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
const fileNames = [ const fileNames = [
'angled_line.kcl', 'angled_line.kcl',
'basic_fillet_cube_close_opposite.kcl', 'basic_fillet_cube_close_opposite.kcl',
@ -1168,8 +1252,8 @@ test(
] ]
for (const fileName of fileNames) { for (const fileName of fileNames) {
await fsp.copyFile( await fsp.copyFile(
`src/wasm-lib/tests/executor/inputs/${fileName}`, executorInputPath(fileName),
`${dir}/testProject/${fileName}` join(testDir, fileName)
) )
} }
}, },
@ -1210,19 +1294,16 @@ test(
'select all in code editor does not actually select all, just what is visible (regression)', 'select all in code editor does not actually select all, just what is visible (regression)',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
// src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl // src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl
const name = 'mike_stress_test' const name = 'mike_stress_test'
await fsp.mkdir(`${dir}/${name}`, { recursive: true }) const testDir = join(dir, name)
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
`src/wasm-lib/tests/executor/inputs/${name}.kcl`, executorInputPath(`${name}.kcl`),
`${dir}/${name}/main.kcl` join(testDir, 'main.kcl')
) )
}, },
}) })
@ -1320,27 +1401,16 @@ test.describe('Renaming in the file tree', () => {
'A file you have open', 'A file you have open',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page, dir } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'), executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl') join(dir, 'Test Project', 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'cylinder.kcl'), executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl') join(dir, 'Test Project', 'fileToRename.kcl')
) )
}, },
@ -1425,27 +1495,16 @@ test.describe('Renaming in the file tree', () => {
'A file you do not have open', 'A file you do not have open',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page, dir } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'), executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl') join(dir, 'Test Project', 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'cylinder.kcl'), executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl') join(dir, 'Test Project', 'fileToRename.kcl')
) )
}, },
@ -1527,10 +1586,6 @@ test.describe('Renaming in the file tree', () => {
`A folder you're not inside`, `A folder you're not inside`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page, dir } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
@ -1538,19 +1593,12 @@ test.describe('Renaming in the file tree', () => {
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), { await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true, recursive: true,
}) })
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'), executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl') join(dir, 'Test Project', 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'cylinder.kcl'), executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl') join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
) )
}, },
@ -1625,11 +1673,6 @@ test.describe('Renaming in the file tree', () => {
`A folder you are inside`, `A folder you are inside`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
const { electronApp, page, dir } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
@ -1638,11 +1681,11 @@ test.describe('Renaming in the file tree', () => {
recursive: true, recursive: true,
}) })
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'), executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl') join(dir, 'Test Project', 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
join(exampleDir, 'cylinder.kcl'), executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl') join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
) )
}, },
@ -1738,21 +1781,18 @@ test.describe('Deleting files from the file pane', () => {
`when main.kcl exists, navigate to main.kcl`, `when main.kcl exists, navigate to main.kcl`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/testProject`, { recursive: true }) const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl', executorInputPath('cylinder.kcl'),
`${dir}/testProject/main.kcl` join(testDir, 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/basic_fillet_cube_end.kcl', executorInputPath('basic_fillet_cube_end.kcl'),
`${dir}/testProject/fileToDelete.kcl` join(testDir, 'fileToDelete.kcl')
) )
}, },
}) })

View File

@ -1,6 +1,13 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { join } from 'path'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
@ -425,17 +432,14 @@ const sketch001 = startSketchAt([-0, -0])
`Network health indicator only appears in modeling view`, `Network health indicator only appears in modeling view`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ browserName: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
) )
}, },
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

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

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 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: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 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: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 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: 60 KiB

After

Width:  |  Height:  |  Size: 60 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: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

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

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -912,3 +912,7 @@ export async function createProjectAndRenameIt({
await page.getByLabel('checkmark').last().click() await page.getByLabel('checkmark').last().click()
} }
export function executorInputPath(fileName: string): string {
return join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName)
}

View File

@ -174,168 +174,166 @@ test.describe('Testing Camera Movement', () => {
}, [0, -85, -85]) }, [0, -85, -85])
}) })
// TODO fixme something is wrong with sketch here test('Zoom should be consistent when exiting or entering sketches', async ({
test.fixme( page,
'Zoom should be consistent when exiting or entering sketches', }) => {
async ({ page }) => { // start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place // than zoom and pan outside of sketch mode and enter again and it should not change from where it is
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is // than again for sketching
// than again for sketching
test.skip(process.platform !== 'darwin', 'Zoom should be consistent') test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible() ).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 325)
let code = `const sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift')
await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' })
await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
let y = 350,
x = 948
await u.canvasLocator.click({ position: { x: 783, y } })
code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line([11.18, 0], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line([0, 6.99], %)`
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'line Line', exact: true }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.mouse.move(700, 325)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
// select a plane
await page.mouse.click(700, 325)
let code = `const sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift')
await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' })
await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
let y = 350,
x = 948
await u.canvasLocator.click({ position: { x: 783, y } })
code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line([11.18, 0], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line([0, 6.99], %)`
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'line Line', exact: true }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.mouse.move(700, 325)
await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
timeout: 10_000,
})
}
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200)
// hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over vertical line
await u.canvasLocator.hover({ position: { x, y: 325 } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// click exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000, timeout: 10_000,
}) })
} }
)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200)
// hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over vertical line
await u.canvasLocator.hover({ position: { x, y: 325 } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// click exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
})
}) })

View File

@ -1,6 +1,13 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils' import { join } from 'path'
import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } 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'
@ -115,6 +122,36 @@ test.describe('Testing settings', () => {
).not.toBeChecked() ).not.toBeChecked()
}) })
test('Keybindings display the correct hotkey for Command Palette', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await test.step('Open keybindings settings', async () => {
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('ControlOrMeta+Shift+,')
// Go to Keybindings tab.
const keybindingsTab = page.getByRole('radio', { name: 'Keybindings' })
await keybindingsTab.click()
})
// Go to the hotkey for Command Palette.
const commandPalette = page.getByText('Toggle Command Palette')
await commandPalette.scrollIntoViewIfNeeded()
// The heading is above it and should be in view now.
const commandPaletteHeading = page.getByRole('heading', {
name: 'Command Palette',
})
// The hotkey is in a kbd element next to the heading.
const hotkey = commandPaletteHeading.locator('+ div kbd')
const text = process.platform === 'darwin' ? 'Command+K' : 'Control+K'
await expect(hotkey).toHaveText(text)
})
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 page.setViewportSize({ width: 1200, height: 500 })
@ -203,10 +240,11 @@ test.describe('Testing settings', () => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true }) const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
`${dir}/bracket/main.kcl` join(bracketDir, 'main.kcl')
) )
}, },
}) })
@ -329,4 +367,130 @@ test.describe('Testing settings', () => {
await electronApp.close() await electronApp.close()
} }
) )
test('Changing modeling default unit', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
// Open the settings modal with lower-right button
await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
const resetButton = page.getByRole('button', {
name: 'Restore default settings',
})
// Default unit should be mm
await resetButton.click()
await test.step('Change modeling default unit within project tab', async () => {
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
await page
.getByTestId('modeling-defaultUnit')
.selectOption(`${unitOfMeasure}`)
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
})
}
await changeUnitOfMeasureInProjectTab('in')
await changeUnitOfMeasureInProjectTab('ft')
await changeUnitOfMeasureInProjectTab('yd')
await changeUnitOfMeasureInProjectTab('mm')
await changeUnitOfMeasureInProjectTab('cm')
await changeUnitOfMeasureInProjectTab('m')
})
// Go to the user tab
await userSettingsTab.click()
await test.step('Change modeling default unit within user tab', async () => {
const changeUnitOfMeasureInUserTab = async (unitOfMeasure: string) => {
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
await page
.getByTestId('modeling-defaultUnit')
.selectOption(`${unitOfMeasure}`)
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" as a user default`
)
await expect(toastMessage).toBeVisible()
})
}
await changeUnitOfMeasureInUserTab('in')
await changeUnitOfMeasureInUserTab('ft')
await changeUnitOfMeasureInUserTab('yd')
await changeUnitOfMeasureInUserTab('mm')
await changeUnitOfMeasureInUserTab('cm')
await changeUnitOfMeasureInUserTab('m')
})
// Close settings
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsCloseButton.click()
await test.step('Change modeling default unit within command bar', async () => {
const commands = page.getByRole('button', { name: 'Commands' })
const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => {
// Open command bar
await commands.click()
const settingsModelingDefaultUnitCommand = page.getByText(
'Settings · modeling · default unit'
)
await settingsModelingDefaultUnitCommand.click()
const commandOption = page.getByRole('option', {
name: unitOfMeasure,
exact: true,
})
await commandOption.click()
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
}
await changeUnitOfMeasureInCommandBar('in')
await changeUnitOfMeasureInCommandBar('ft')
await changeUnitOfMeasureInCommandBar('yd')
await changeUnitOfMeasureInCommandBar('mm')
await changeUnitOfMeasureInCommandBar('cm')
await changeUnitOfMeasureInCommandBar('m')
})
await test.step('Change modeling default unit within gizmo', async () => {
const changeUnitOfMeasureInGizmo = async (
unitOfMeasure: string,
copy: string
) => {
const gizmo = page.getByRole('button', {
name: 'Current units are: ',
})
await gizmo.click()
const button = page.getByRole('button', {
name: copy,
exact: true,
})
await button.click()
const toastMessage = page.getByText(
`Set default unit to "${unitOfMeasure}" for this project`
)
await expect(toastMessage).toBeVisible()
}
await changeUnitOfMeasureInGizmo('in', 'Inches')
await changeUnitOfMeasureInGizmo('ft', 'Feet')
await changeUnitOfMeasureInGizmo('yd', 'Yards')
await changeUnitOfMeasureInGizmo('mm', 'Millimeters')
await changeUnitOfMeasureInGizmo('cm', 'Centimeters')
await changeUnitOfMeasureInGizmo('m', 'Meters')
})
})
}) })

View File

@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils' import {
getUtils,
setup,
tearDown,
setupElectron,
createProjectAndRenameIt,
} from './test-utils'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page) await setup(context, page)
@ -683,3 +689,60 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
}) })
} }
test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Create and navigate to the project
await createProjectAndRenameIt({ name: 'test-000', page })
await page.getByTestId('project-link').click()
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// Open the files pane
const filesPaneButton = page.getByTestId('files-pane-button')
await filesPaneButton.click()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4')
// File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
})
await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// 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 test.step(`Test file deletion on rejection`, async () => {
const rejectButton = page.getByRole('button', { name: 'Reject' })
// A file is created and can be navigated to while this prompt is still opened
// Click the "Reject" button within the prompt and it will delete the file.
await rejectButton.click()
const submittingToastMessage = page.getByText(
`Successfully deleted file "lego-2x4.kcl"`
)
await expect(submittingToastMessage).toBeVisible()
})
await electronApp.close()
}
)

View File

@ -4,10 +4,17 @@ import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb' import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm' import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite' import { VitePlugin } from '@electron-forge/plugin-vite'
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
import { FusesPlugin } from '@electron-forge/plugin-fuses' import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses' import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path' import path from 'path'
interface ExtendedMakerWixConfig extends MakerWixConfig {
// see https://github.com/electron/forge/issues/3673
// this is an undocumented property of electron-wix-msi
associateExtensions?: string
}
const rootDir = process.cwd() const rootDir = process.cwd()
const config: ForgeConfig = { const config: ForgeConfig = {
@ -23,12 +30,23 @@ const config: ForgeConfig = {
undefined, undefined,
executableName: 'zoo-modeling-app', executableName: 'zoo-modeling-app',
icon: path.resolve(rootDir, 'assets', 'icon'), icon: path.resolve(rootDir, 'assets', 'icon'),
protocols: [
{
name: 'Zoo Studio',
schemes: ['zoo-studio'],
},
],
extendInfo: 'Info.plist', // Information for file associations.
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [ makers: [
new MakerSquirrel({ new MakerSquirrel({
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'), setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
}), }),
new MakerWix({
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
associateExtensions: 'kcl',
} as ExtendedMakerWixConfig),
new MakerZIP({}, ['darwin']), new MakerZIP({}, ['darwin']),
new MakerRpm({ new MakerRpm({
options: { options: {

1
interface.d.ts vendored
View File

@ -31,6 +31,7 @@ export interface IElectronAPI {
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 setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: { packageJson: {
name: string name: string
} }

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^2.0.0", "@kittycad/lib": "^2.0.1",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1", "@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -44,6 +44,7 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"minimist": "^1.2.8",
"openid-client": "^5.6.5", "openid-client": "^5.6.5",
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",
"react": "^18.3.1", "react": "^18.3.1",
@ -88,8 +89,8 @@
"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": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e", "lint": "eslint --fix src e2e",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"postinstall": "yarn xstate:typegen", "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)\"",
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
@ -119,16 +120,18 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.24.3", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "^7.4.0", "@electron-forge/cli": "^7.4.0",
"@electron-forge/maker-deb": "^7.4.0", "@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0", "@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0", "@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-wix": "^7.4.0",
"@electron-forge/maker-zip": "^7.4.0", "@electron-forge/maker-zip": "^7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0", "@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0", "@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0", "@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0", "@electron/fuses": "^1.8.0",
"@electron/rebuild": "^3.6.0",
"@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",
@ -137,6 +140,7 @@
"@types/d3-force": "^3.0.10", "@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^22.5.0", "@types/node": "^22.5.0",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",

View File

@ -33,7 +33,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider' import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/desktop'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -71,17 +70,13 @@ const router = createRouter([
loader: async () => { loader: async () => {
const onDesktop = isDesktop() const onDesktop = isDesktop()
if (onDesktop) { if (onDesktop) {
const appState = await getState() const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (appState) { if (projectStartupFile !== null) {
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
// Redirect to the file if we have a file path. // Redirect to the file if we have a file path.
if (appState.current_file) { if (projectStartupFile.length > 0) {
return redirect( return redirect(
PATHS.FILE + '/' + encodeURIComponent(appState.current_file) PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
) )
} }
} }

View File

@ -1,4 +1,4 @@
import { MouseGuard } from 'lib/cameraControls' import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
import { import {
Euler, Euler,
MathUtils, MathUtils,
@ -81,24 +81,7 @@ export class CameraControls {
pendingZoom: number | null = null pendingZoom: number | null = null
pendingRotation: Vector2 | null = null pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = { interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => {
console.log('event', e)
return !!(e.buttons & 2)
},
},
}
isFovAnimationInProgress = false isFovAnimationInProgress = false
fovBeforeOrtho = 45 fovBeforeOrtho = 45
get isPerspective() { get isPerspective() {

View File

@ -4,4 +4,21 @@
*/ */
.header { .header {
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
-webkit-app-region: drag; /* Make the header of the app draggable */
}
.header button {
-webkit-app-region: no-drag; /* Make the button not draggable */
}
.header a {
-webkit-app-region: no-drag; /* Make the link not draggable */
}
.header textarea {
-webkit-app-region: no-drag; /* Make the textarea not draggable */
}
.header input {
-webkit-app-region: no-drag; /* Make the input not draggable */
} }

View File

@ -9,6 +9,8 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
export const CommandBar = () => { export const CommandBar = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
@ -24,7 +26,7 @@ export const CommandBar = () => {
}, [pathname]) }, [pathname])
// Hook up keyboard shortcuts // Hook up keyboard shortcuts
useHotkeyWrapper(['mod+k'], () => { useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
if (commandBarState.context.commands.length === 0) return if (commandBarState.context.commands.length === 0) return
if (commandBarState.matches('Closed')) { if (commandBarState.matches('Closed')) {
commandBarSend({ type: 'Open' }) commandBarSend({ type: 'Open' })

View File

@ -1,5 +1,7 @@
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
export function CommandBarOpenButton() { export function CommandBarOpenButton() {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
@ -12,7 +14,7 @@ export function CommandBarOpenButton() {
> >
<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">
{platform === 'macos' ? '⌘K' : '^/'} {hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)}
</kbd> </kbd>
</button> </button>
) )

View File

@ -1,4 +1,4 @@
import type { FileEntry, IndexLoaderData } from 'lib/types' import type { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
@ -20,6 +20,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog' import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu' import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`

View File

@ -9,7 +9,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator' import { NetworkMachineIndicator } from './NetworkMachineIndicator'
export function LowerRightControls({ export function LowerRightControls({
@ -66,6 +66,9 @@ export function LowerRightControls({
{children} {children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
<a <a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`} href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -15,7 +15,7 @@ import { Extension } from '@codemirror/state'
import { LanguageSupport } from '@codemirror/language' import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { FileEntry } from 'lib/types' import { FileEntry } from 'lib/project'
import Worker from 'editor/plugins/lsp/worker.ts?worker' import Worker from 'editor/plugins/lsp/worker.ts?worker'
import { import {
KclWorkerOptions, KclWorkerOptions,

View File

@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip' import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog' import { DeleteConfirmationDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm' import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
function ProjectCard({ function ProjectCard({
project, project,

View File

@ -1,7 +1,7 @@
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { HTMLProps, forwardRef } from 'react' import { HTMLProps, forwardRef } from 'react'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> { interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
project: Project project: Project

View File

@ -1,4 +1,4 @@
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'

View File

@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider' import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { CommandBarProvider } from './CommandBar/CommandBarProvider' import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
const now = new Date() const now = new Date()
const projectWellFormed = { const projectWellFormed = {

View File

@ -25,8 +25,17 @@ const ProjectSidebarMenu = ({
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
file?: IndexLoaderData['file'] file?: IndexLoaderData['file']
}) => { }) => {
// Make room for traffic lights on desktop left side.
// TODO: make sure this doesn't look like shit on Linux or Windows
const trafficLightsOffset =
isDesktop() && window.electron.os.isMac ? 'ml-20' : ''
return ( return (
<div className="!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2"> <div
className={
'!no-underline h-full mr-auto max-h-min min-h-12 min-w-max flex items-center gap-2 ' +
trafficLightsOffset
}
>
<AppLogoLink project={project} file={file} /> <AppLogoLink project={project} file={file} />
{enableMenu ? ( {enableMenu ? (
<ProjectMenuPopover project={project} file={file} /> <ProjectMenuPopover project={project} file={file} />

View File

@ -399,6 +399,9 @@ export class KclManager {
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
// Write back to the file system. // Write back to the file system.
codeManager.writeToFile() codeManager.writeToFile()
// execute the code.
this.executeCode()
} }
// There's overlapping responsibility between updateAst and executeAst. // There's overlapping responsibility between updateAst and executeAst.
// updateAst was added as it was used a lot before xState migration so makes the port easier. // updateAst was added as it was used a lot before xState migration so makes the port easier.

View File

@ -6,9 +6,13 @@ import {
Expr, Expr,
Program, Program,
CallExpression, CallExpression,
makeDefaultPlanes,
PipeExpression,
VariableDeclaration,
} from '../wasm' } from '../wasm'
import { import {
addFillet, addFillet,
getPathToExtrudeForSegmentSelection,
hasValidFilletSelection, hasValidFilletSelection,
isTagUsedInFillet, isTagUsedInFillet,
} from './addFillet' } from './addFillet'
@ -16,9 +20,204 @@ import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
beforeAll(async () => { beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
token: VITE_KC_DEV_TOKEN,
width: 256,
height: 256,
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: async () => {
resolve(true)
},
})
})
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
})
const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string,
selectedSegmentSnippet: string,
expectedExtrudeSnippet: string
) => {
// helpers
function getExtrudeExpression(
ast: Program,
pathToExtrudeNode: PathToNode
): CallExpression | PipeExpression | undefined | Error {
if (pathToExtrudeNode.length === 0) return undefined // no extrude node
const extrudeNodeResult = getNodeFromPath(ast, pathToExtrudeNode)
if (err(extrudeNodeResult)) {
return extrudeNodeResult
}
return extrudeNodeResult.node as CallExpression | PipeExpression
}
function getExpectedExtrudeExpression(
ast: Program,
code: string,
expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number] = [
code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
]
const expedtedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expedtedExtrudeNodeResult = getNodeFromPath(ast, expedtedExtrudePath)
if (err(expedtedExtrudeNodeResult)) {
return expedtedExtrudeNodeResult
}
const expectedExtrudeNode =
expedtedExtrudeNodeResult.node as VariableDeclaration
return expectedExtrudeNode.declarations[0].init as
| CallExpression
| PipeExpression
}
// ast
const astOrError = parse(code)
if (err(astOrError)) return new Error('AST not found')
const ast = astOrError as Program
// selection
const segmentRange: [number, number] = [
code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
]
const selection: Selections = {
codeBasedSelections: [
{
range: segmentRange,
type: 'default',
},
],
otherSelections: [],
}
// programMemory and artifactGraph
await kclManager.executeAst({ ast })
const programMemory = kclManager.programMemory
const artifactGraph = engineCommandManager.artifactGraph
// get extrude expression
const pathResult = getPathToExtrudeForSegmentSelection(
ast,
selection,
programMemory,
artifactGraph
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
const extrudeExpression = getExtrudeExpression(ast, pathToExtrudeNode)
// test
if (expectedExtrudeSnippet) {
const expectedExtrudeExpression = getExpectedExtrudeExpression(
ast,
code,
expectedExtrudeSnippet
)
if (err(expectedExtrudeExpression)) return expectedExtrudeExpression
expect(extrudeExpression).toEqual(expectedExtrudeExpression)
} else {
expect(extrudeExpression).toBeUndefined()
}
}
describe('Testing getPathToExtrudeForSegmentSelection', () => {
it('should return the correct paths for a valid selection and extrusion', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = `const extrude001 = extrude(-15, sketch001)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)
|> line([15, 0], %)
|> line([0, -15], %)
|> line([-15, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn('XY')
|> startProfileAt([30, 30], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn('XY')
|> startProfileAt([30, -30], %)
|> line([25, 0], %)
|> line([0, -25], %)
|> line([-25, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)
const extrude002 = extrude(-15, sketch002)
const extrude003 = extrude(-15, sketch003)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = `const extrude002 = extrude(-15, sketch002)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should not return any path for missing extrusion', async () => {
const code = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)
|> line([15, 0], %)
|> line([0, -15], %)
|> line([-15, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn('XY')
|> startProfileAt([30, 30], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn('XY')
|> startProfileAt([30, -30], %)
|> line([25, 0], %)
|> line([0, -25], %)
|> line([-25, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-15, sketch001)
const extrude003 = extrude(-15, sketch003)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = ``
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
}) })
const runFilletTest = async ( const runFilletTest = async (
@ -57,8 +256,6 @@ const runFilletTest = async (
return new Error('Path to extrude node not found') return new Error('Path to extrude node not found')
} }
// const radius = createLiteral(5) as Expr
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius) const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (err(result)) { if (err(result)) {
return result return result
@ -68,7 +265,6 @@ const runFilletTest = async (
expect(newCode).toContain(expectedCode) expect(newCode).toContain(expectedCode)
} }
describe('Testing addFillet', () => { describe('Testing addFillet', () => {
/** /**
* 1. Ideal Case * 1. Ideal Case

View File

@ -4,9 +4,11 @@ import {
ObjectExpression, ObjectExpression,
PathToNode, PathToNode,
Program, Program,
ProgramMemory,
Expr, Expr,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
sketchGroupFromKclValue,
} from '../wasm' } from '../wasm'
import { import {
createCallExpressionStdLib, createCallExpressionStdLib,
@ -28,62 +30,210 @@ import {
getTagFromCallExpression, getTagFromCallExpression,
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections' import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
getExtrusionFromSuspectedPath,
} from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
/**
* Apply Fillet To Selection
*/
export function applyFilletToSelection(
selection: Selections,
radius: KclCommandValue
): void | Error {
// 1. get AST
let ast = kclManager.ast
const astResult = insertRadiusIntoAst(ast, radius)
if (err(astResult)) return astResult
// 2. get path
const programMemory = kclManager.programMemory
const artifactGraph = engineCommandManager.artifactGraph
const getPathToExtrudeForSegmentSelectionResult =
getPathToExtrudeForSegmentSelection(
ast,
selection,
programMemory,
artifactGraph
)
if (err(getPathToExtrudeForSegmentSelectionResult))
return getPathToExtrudeForSegmentSelectionResult
const { pathToSegmentNode, pathToExtrudeNode } =
getPathToExtrudeForSegmentSelectionResult
// 3. add fillet
const addFilletResult = addFillet(
ast,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
)
if (trap(addFilletResult)) return addFilletResult
const { modifiedAst, pathToFilletNode } = addFilletResult
// 4. update ast
updateAstAndFocus(modifiedAst, pathToFilletNode)
}
function insertRadiusIntoAst(
ast: Program,
radius: KclCommandValue
): { ast: Program } | Error {
try {
// Validate and update AST
if (
'variableName' in radius &&
radius.variableName &&
radius.insertIndex !== undefined
) {
const newAst = structuredClone(ast)
newAst.body.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
return { ast: newAst }
}
return { ast }
} catch (error) {
return new Error(`Failed to handle AST: ${(error as Error).message}`)
}
}
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selections,
programMemory: ProgramMemory,
artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeBasedSelections[0].range
)
const varDecNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = sketchGroupFromKclValue(
kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketchGroup)) return sketchGroup
const extrusion = getExtrusionFromSuspectedPath(sketchGroup.id, artifactGraph)
if (err(extrusion)) return extrusion
const pathToExtrudeNode = getNodePathFromSourceRange(
ast,
extrusion.codeRef.range
)
if (err(pathToExtrudeNode)) return pathToExtrudeNode
return { pathToSegmentNode, pathToExtrudeNode }
}
async function updateAstAndFocus(
modifiedAst: Program,
pathToFilletNode: PathToNode
) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}
/**
* Add Fillet
*/
export function addFillet( export function addFillet(
node: Program, ast: Program,
pathToSegmentNode: PathToNode, pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode, pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Expr radius: Expr = createLiteral(5)
// shouldPipe = false, // TODO: Implement this feature
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error { ): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// clone ast to make mutations safe // Clone AST to ensure safe mutations
let _node = structuredClone(node) const astClone = structuredClone(ast)
/** // Modify AST clone : TAG the sketch segment and retrieve tag
* Add Tag to the Segment Expression const segmentResult = mutateAstWithTagForSketchSegment(
*/ astClone,
pathToSegmentNode
)
if (err(segmentResult)) return segmentResult
const { tag } = segmentResult
// Find the specific sketch segment to tag with the new tag // Modify AST clone : Insert FILLET node and retrieve path to fillet
const sketchSegmentChunk = getNodeFromPath( const filletResult = mutateAstWithFilletNode(
_node, astClone,
pathToExtrudeNode,
radius,
tag
)
if (err(filletResult)) return filletResult
const { pathToFilletNode } = filletResult
return { modifiedAst: astClone, pathToFilletNode }
}
function mutateAstWithTagForSketchSegment(
astClone: Program,
pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error {
const segmentNode = getNodeFromPath<CallExpression>(
astClone,
pathToSegmentNode, pathToSegmentNode,
'CallExpression' 'CallExpression'
) )
if (err(sketchSegmentChunk)) return sketchSegmentChunk if (err(segmentNode)) return segmentNode
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
// Check whether selection is a valid segment from sketchLineHelpersMap // Check whether selection is a valid segment
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) { if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment') return new Error('Selection is not a sketch segment')
} }
// Add tag to the sketch segment or use existing tag // Add tag to the sketch segment or use existing tag
// a helper function that creates the updated node and applies the changes to the AST
const taggedSegment = addTagForSketchOnFace( const taggedSegment = addTagForSketchOnFace(
{ {
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode, pathToNode: pathToSegmentNode,
node: _node, node: astClone,
}, },
sketchSegmentNode.callee.name segmentNode.node.callee.name
) )
if (err(taggedSegment)) return taggedSegment if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment const { tag } = taggedSegment
/** return { modifiedAst: astClone, tag }
* Find Extrude Expression automatically }
*/
// 1. Get the sketch name function mutateAstWithFilletNode(
astClone: Program,
pathToExtrudeNode: PathToNode,
radius: Expr,
tag: string
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// Locate the extrude call
const locatedExtrudeDeclarator = locateExtrudeDeclarator(
astClone,
pathToExtrudeNode
)
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator
/** /**
* Add Fillet to the Extrude expression * Prepare changes to the AST
*/ */
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [ const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({ createObjectExpression({
radius: radius, radius: radius,
@ -92,104 +242,36 @@ export function addFillet(
createPipeSubstitution(), createPipeSubstitution(),
]) ])
// Locate the extrude call /**
const extrudeChunk = getNodeFromPath<VariableDeclaration>( * Mutate the AST
_node, */
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
// CallExpression - no fillet // CallExpression - no fillet
// PipeExpression - fillet exists // PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = ( let pathToFilletNode: PathToNode = []
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator, if (extrudeDeclarator.init.type === 'CallExpression') {
tag: string // 1. case when no fillet exists
): PathToNode => {
let pathToFilletObj: any // modify ast with new fillet call by mutating the extrude node
let inFillet = false extrudeDeclarator.init = createPipeExpression([
traverse(extrudeDeclarator.init, { extrudeDeclarator.init,
enter(node, path) { filletCall,
if (node.type === 'CallExpression' && node.callee.name === 'fillet') { ])
inFillet = true
} // get path to the fillet node
if (inFillet && node.type === 'ObjectExpression') { pathToFilletNode = getPathToNodeOfFilletLiteral(
const hasTag = node.properties.some((prop) => { pathToExtrudeNode,
const isTagProp = prop.key.name === 'tags' extrudeDeclarator,
if (isTagProp && prop.value.type === 'ArrayExpression') { tag
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
) )
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [ return { modifiedAst: astClone, pathToFilletNode }
...pathToExtrudeNode.slice(0, indexOfPipeExpression), } else if (extrudeDeclarator.init.type === 'PipeExpression') {
...pathToFilletObj, // 2. case when fillet exists
]
}
if (extrudeInit.type === 'CallExpression') { const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet' return node.type === 'CallExpression' && node.callee.name === 'fillet'
}) })
@ -198,25 +280,13 @@ export function addFillet(
} }
// check if the existing fillet has the same tag as the new fillet // check if the existing fillet has the same tag as the new fillet
let filletTag = null const filletTag = getFilletTag(existingFilletCall)
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
if (filletTag !== tag) { if (filletTag !== tag) {
extrudeInit.body.push(filletCall) // mutate the extrude node with the new fillet call
extrudeDeclarator.init.body.push(filletCall)
return { return {
modifiedAst: _node, modifiedAst: astClone,
pathToFilletNode: getPathToNodeOfFilletLiteral( pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode, pathToExtrudeNode,
extrudeDeclarator, extrudeDeclarator,
@ -228,9 +298,124 @@ export function addFillet(
return new Error('Unsupported extrude type.') return new Error('Unsupported extrude type.')
} }
return new Error('Unsupported extrude type.') return { modifiedAst: astClone, pathToFilletNode }
} }
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
): { extrudeDeclarator: VariableDeclarator } | Error {
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.')
}
const extrudeInit = extrudeDeclarator?.init
if (!extrudeInit) {
return new Error('Extrude Init not found.')
}
if (
extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression'
) {
return new Error('Extrude must be a PipeExpression or CallExpression')
}
return { extrudeDeclarator }
}
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (!hasTag(node, tag)) return false
pathToFilletObj = getPathToRadiusLiteral(node, path)
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
function hasTag(node: ObjectExpression, tag: string): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
}
return false
})
}
function getPathToRadiusLiteral(node: ObjectExpression, path: any): PathToNode {
let pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
return pathToFilletObj
}
function getFilletTag(existingFilletCall: CallExpression): string | null {
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
return elements[0].name
}
}
}
return null
}
/**
* Button states
*/
export const hasValidFilletSelection = ({ export const hasValidFilletSelection = ({
selectionRanges, selectionRanges,
ast, ast,

View File

@ -1,4 +1,10 @@
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType' import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
import { platform } from './utils'
const PLATFORM = platform()
const META =
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
const noModifiersPressed = (e: React.MouseEvent) => const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
@ -73,99 +79,99 @@ export const btnName = (e: React.MouseEvent) => ({
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = { export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: { KittyCAD: {
pan: { pan: {
description: 'Right click + Shift + drag or middle click + drag', description: 'Shift + Right click drag or middle click drag',
callback: (e) => callback: (e) =>
(btnName(e).middle && noModifiersPressed(e)) || (btnName(e).middle && noModifiersPressed(e)) ||
(btnName(e).right && e.shiftKey), (btnName(e).right && e.shiftKey),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll or Ctrl + Right click drag',
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey, dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
OnShape: { OnShape: {
pan: { pan: {
description: 'Right click + Ctrl + drag or middle click + drag', description: 'Ctrl + Right click drag or middle click drag',
callback: (e) => callback: (e) =>
(btnName(e).right && e.ctrlKey) || (btnName(e).right && e.ctrlKey) ||
(btnName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll',
dragCallback: () => false, dragCallback: () => false,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Right click + drag', description: 'Right click drag',
callback: (e) => btnName(e).right && noModifiersPressed(e), callback: (e) => btnName(e).right && noModifiersPressed(e),
}, },
}, },
'Trackpad Friendly': { 'Trackpad Friendly': {
pan: { pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag', description: `${ALT} + Shift + Left click drag or middle click drag`,
callback: (e) => callback: (e) =>
(btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) || (btnName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(btnName(e).middle && noModifiersPressed(e)), (btnName(e).middle && noModifiersPressed(e)),
}, },
zoom: { zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag', description: `Scroll or ${ALT} + ${META} + Left click drag`,
dragCallback: (e) => btnName(e).left && e.altKey && e.metaKey, dragCallback: (e) => btnName(e).left && e.altKey && e.metaKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Left click + Alt + drag', description: `${ALT} + Left click drag`,
callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey, callback: (e) => btnName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0, lenientDragStartButton: 0,
}, },
}, },
Solidworks: { Solidworks: {
pan: { pan: {
description: 'Right click + Ctrl + drag', description: 'Ctrl + Right click drag',
callback: (e) => btnName(e).right && e.ctrlKey, callback: (e) => btnName(e).right && e.ctrlKey,
lenientDragStartButton: 2, lenientDragStartButton: 2,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Shift + drag', description: 'Scroll or Shift + Middle click drag',
dragCallback: (e) => btnName(e).middle && e.shiftKey, dragCallback: (e) => btnName(e).middle && e.shiftKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
NX: { NX: {
pan: { pan: {
description: 'Middle click + Shift + drag', description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag', description: 'Scroll or Ctrl + Middle click drag',
dragCallback: (e) => btnName(e).middle && e.ctrlKey, dragCallback: (e) => btnName(e).middle && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
}, },
Creo: { Creo: {
pan: { pan: {
description: 'Left click + Ctrl + drag', description: 'Ctrl + Left click drag',
callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey, callback: (e) => btnName(e).left && !btnName(e).right && e.ctrlKey,
}, },
zoom: { zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag', description: 'Scroll or Ctrl + Right click drag',
dragCallback: (e) => btnName(e).right && !btnName(e).left && e.ctrlKey, dragCallback: (e) => btnName(e).right && !btnName(e).left && e.ctrlKey,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle (or Left + Right) click + Ctrl + drag', description: 'Ctrl + Middle (or Left + Right) click drag',
callback: (e) => { callback: (e) => {
const b = btnName(e) const b = btnName(e)
return (b.middle || (b.left && b.right)) && e.ctrlKey return (b.middle || (b.left && b.right)) && e.ctrlKey
@ -174,16 +180,16 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
}, },
AutoCAD: { AutoCAD: {
pan: { pan: {
description: 'Middle click + drag', description: 'Middle click drag',
callback: (e) => btnName(e).middle && noModifiersPressed(e), callback: (e) => btnName(e).middle && noModifiersPressed(e),
}, },
zoom: { zoom: {
description: 'Scroll wheel', description: 'Scroll',
dragCallback: () => false, dragCallback: () => false,
scrollCallback: () => true, scrollCallback: () => true,
}, },
rotate: { rotate: {
description: 'Middle click + Shift + drag', description: 'Shift + Middle click drag',
callback: (e) => btnName(e).middle && e.shiftKey, callback: (e) => btnName(e).middle && e.shiftKey,
}, },
}, },

View File

@ -1,8 +1,6 @@
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project, FileEntry } from 'lib/project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { import {
defaultAppSettings, defaultAppSettings,
@ -477,18 +475,6 @@ export const writeAppSettingsFile = async (tomlStr: string) => {
return window.electron.writeFile(appSettingsFilePath, tomlStr) return window.electron.writeFile(appSettingsFilePath, tomlStr)
} }
let appStateStore: ProjectState | undefined = undefined
export const getState = async (): Promise<ProjectState | undefined> => {
return Promise.resolve(appStateStore)
}
export const setState = async (
state: ProjectState | undefined
): Promise<void> => {
appStateStore = state
}
export const getUser = async ( export const getUser = async (
token: string, token: string,
hostname: string hostname: string

View File

@ -1,5 +1,5 @@
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import type { FileEntry } from 'lib/types' import type { FileEntry } from 'lib/project'
import { import {
FILE_EXT, FILE_EXT,
INDEX_IDENTIFIER, INDEX_IDENTIFIER,

View File

@ -0,0 +1,98 @@
import { promises as fs } from 'fs'
import path from 'path'
import os from 'os'
import { v4 as uuidv4 } from 'uuid'
import getCurrentProjectFile from './getCurrentProjectFile'
describe('getCurrentProjectFile', () => {
test('with explicit open file with space (URL encoded)', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')
const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i%20have%20a%20space.kcl')
)
expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))
await fs.rm(tmpProjectDir, { recursive: true, force: true })
})
test('with explicit open file with space', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')
const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i have a space.kcl')
)
expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))
await fs.rm(tmpProjectDir, { recursive: true, force: true })
})
test('with source path dot', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
// Set the current directory to the temp project directory.
const originalCwd = process.cwd()
process.chdir(tmpProjectDir)
try {
const state = await getCurrentProjectFile('.')
if (state instanceof Error) {
throw state
}
expect(state.replace('/private', '')).toBe(
path.join(tmpProjectDir, 'main.kcl')
)
} finally {
process.chdir(originalCwd)
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
test('with main.kcl not existing', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
try {
const state = await getCurrentProjectFile(tmpProjectDir)
expect(state).toBe(path.join(tmpProjectDir, 'main.kcl'))
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
test('with directory, main.kcl not existing, other.kcl does', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'other.kcl'), '')
try {
const state = await getCurrentProjectFile(tmpProjectDir)
expect(state).toBe(path.join(tmpProjectDir, 'other.kcl'))
// make sure we didn't create a main.kcl file
await expect(
fs.access(path.join(tmpProjectDir, 'main.kcl'))
).rejects.toThrow()
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
})

View File

@ -0,0 +1,116 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import { Models } from '@kittycad/lib/dist/types/src'
import { PROJECT_ENTRYPOINT } from './constants'
// Create a const object with the values
const FILE_IMPORT_FORMATS = {
fbx: 'fbx',
gltf: 'gltf',
obj: 'obj',
ply: 'ply',
sldprt: 'sldprt',
step: 'step',
stl: 'stl',
} as const
// Extract the values into an array
const fileImportFormats: Models['FileImportFormat_type'][] =
Object.values(FILE_IMPORT_FORMATS)
export const allFileImportFormats: string[] = [
...fileImportFormats,
'stp',
'fbxb',
'glb',
]
export const relevantExtensions = ['kcl', ...allFileImportFormats]
/// Get the current project file from the path.
/// This is used for double-clicking on a file in the file explorer,
/// or the command line args, or deep linking.
export default async function getCurrentProjectFile(
pathString: string
): Promise<string | Error> {
// Fix for "." path, which is the current directory.
let sourcePath = pathString === '.' ? process.cwd() : pathString
// URL decode the path.
sourcePath = decodeURIComponent(sourcePath)
// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
sourcePath = path.isAbsolute(sourcePath)
? sourcePath
: path.join(process.cwd(), sourcePath)
// If the path is a directory, let's assume it is a project directory.
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) {
// Walk the directory and look for a kcl file.
const files = await fs.readdir(sourcePath)
const kclFiles = files.filter((file) => path.extname(file) === '.kcl')
if (kclFiles.length === 0) {
let projectFile = path.join(sourcePath, PROJECT_ENTRYPOINT)
// Check if we have a main.kcl file in the project.
try {
await fs.access(projectFile)
} catch {
// Create the default file in the project.
await fs.writeFile(projectFile, '')
}
return projectFile
}
// If a project entrypoint file exists, use it.
// Otherwise, use the first kcl file in the project.
const gotMain = files.filter((file) => file === PROJECT_ENTRYPOINT)
if (gotMain.length === 0) {
return path.join(sourcePath, kclFiles[0])
}
return path.join(sourcePath, PROJECT_ENTRYPOINT)
}
// Check if the extension on what we are trying to open is a relevant file type.
const extension = path.extname(sourcePath).slice(1)
if (!relevantExtensions.includes(extension) && extension !== 'toml') {
return new Error(
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join(
', '
)}`
)
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
const parent = path.dirname(sourcePath)
// If we got an import model file, we need to check if we have a file in the project for
// this import model.
if (allFileImportFormats.includes(extension)) {
const importFileName = path.basename(sourcePath)
// Check if we have a file in the project for this import model.
const kclWrapperFilename = `${importFileName}.kcl`
const kclWrapperFilePath = path.join(parent, kclWrapperFilename)
try {
await fs.access(kclWrapperFilePath)
} catch {
// Create the file in the project with the default import content.
const content = `// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("${importFileName}")`
await fs.writeFile(kclWrapperFilePath, content)
}
return kclWrapperFilePath
}
return sourcePath
}

View File

@ -0,0 +1,35 @@
import { hotkeyDisplay } from './hotkeyWrapper'
describe('hotkeyDisplay', () => {
it('displays mod', async () => {
expect(hotkeyDisplay('mod+c', 'macos')).toEqual('⌘C')
expect(hotkeyDisplay('mod+c', 'windows')).toEqual('Ctrl+C')
expect(hotkeyDisplay('mod+c', 'linux')).toEqual('Ctrl+C')
})
it('displays shift', async () => {
expect(hotkeyDisplay('shift+c', 'macos')).toEqual('⬆C')
expect(hotkeyDisplay('shift+c', 'windows')).toEqual('Shift+C')
expect(hotkeyDisplay('shift+c', 'linux')).toEqual('Shift+C')
})
it('displays meta', async () => {
expect(hotkeyDisplay('meta+c', 'macos')).toEqual('⌘C')
expect(hotkeyDisplay('meta+c', 'windows')).toEqual('Win+C')
// That's correct. What browsers call meta is actually super.
expect(hotkeyDisplay('meta+c', 'linux')).toEqual('Super+C')
})
it('displays alt', async () => {
expect(hotkeyDisplay('alt+c', 'macos')).toEqual('⌥C')
expect(hotkeyDisplay('alt+c', 'windows')).toEqual('Alt+C')
expect(hotkeyDisplay('alt+c', 'linux')).toEqual('Alt+C')
})
it('displays ctrl', async () => {
expect(hotkeyDisplay('ctrl+c', 'macos')).toEqual('^C')
expect(hotkeyDisplay('ctrl+c', 'windows')).toEqual('Ctrl+C')
expect(hotkeyDisplay('ctrl+c', 'linux')).toEqual('Ctrl+C')
})
it('displays multiple modifiers', async () => {
expect(hotkeyDisplay('shift+alt+ctrl+c', 'windows')).toEqual(
'Shift+Alt+Ctrl+C'
)
})
})

View File

@ -1,9 +1,10 @@
import { Options, useHotkeys } from 'react-hotkeys-hook' import { Options, useHotkeys } from 'react-hotkeys-hook'
import { useEffect } from 'react' import { useEffect } from 'react'
import { codeManager } from './singletons' import { codeManager } from './singletons'
import { Platform } from './utils'
// Hotkey wrapper wraps hotkeys for the app (outside of the editor) // Hotkey wrapper wraps hotkeys for the app (outside of the editor)
// With hotkeys inside the editor. // with hotkeys inside the editor.
// This way we can have hotkeys defined in one place and not have to worry about // This way we can have hotkeys defined in one place and not have to worry about
// conflicting hotkeys, or them only being implemented for the app but not // conflicting hotkeys, or them only being implemented for the app but not
// inside the editor. // inside the editor.
@ -37,3 +38,48 @@ function mapHotkeyToCodeMirrorHotkey(hotkey: string): string {
.replaceAll('shift', 'Shift') .replaceAll('shift', 'Shift')
.replaceAll('alt', 'Alt') .replaceAll('alt', 'Alt')
} }
const LOWER_CASE_LETTER = /[a-z]/
const WHITESPACE = /\s+/g
/**
* Convert hotkey to display text.
*
* TODO: We should handle capitalized single letter hotkeys like K as Shift+K,
* but we don't.
*/
export function hotkeyDisplay(hotkey: string, platform: Platform): string {
const isMac = platform === 'macos'
const isWindows = platform === 'windows'
// Browsers call it metaKey, but that's a misnomer.
const meta = isWindows ? 'Win' : 'Super'
const outputSeparator = isMac ? '' : '+'
const display = hotkey
// Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be
// shown as a separate modifier.
.split('+')
.map((word) => {
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
return word.toUpperCase()
}
return word
})
.join(outputSeparator)
// Collapse multiple spaces into one.
.replaceAll(WHITESPACE, ' ')
.replaceAll('mod', isMac ? '⌘' : 'Ctrl')
.replaceAll('meta', isMac ? '⌘' : meta)
// This is technically the wrong arrow for control, but it's more visible
// and recognizable. May want to change this in the future.
//
// The correct arrow is ⌃ "UP ARROWHEAD" Unicode: U+2303
.replaceAll('ctrl', isMac ? '^' : 'Ctrl')
// This is technically the wrong arrow for shift, but it's more visible and
// recognizable. May want to change this in the future.
//
// The correct arrow is ⇧ "UPWARDS WHITE ARROW" Unicode: U+21E7
.replaceAll('shift', isMac ? '⬆' : 'Shift')
.replaceAll('alt', isMac ? '⌥' : 'Alt')
return display
}

46
src/lib/project.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
/**
* The permissions of a file.
*/
export type FilePermission = 'read' | 'write' | 'execute'
/**
* The type of a file.
*/
export type FileType = 'file' | 'directory' | 'symlink'
/**
* Metadata about a file or directory.
*/
export type FileMetadata = {
accessed: string | null
created: string | null
type: FileType | null
size: number
modified: string | null
permission: FilePermission | null
}
/**
* Information about a file or directory.
*/
export type FileEntry = {
path: string
name: string
children: Array<FileEntry> | null
}
/**
* Information about project.
*/
export type Project = {
metadata: FileMetadata | null
kcl_file_count: number
directory_count: number
/**
* The default file to open on load.
*/
default_file: string
path: string
name: string
children: Array<FileEntry> | null
}

View File

@ -61,7 +61,7 @@ export const interactionMap: Record<
name: 'toggle-command-palette', name: 'toggle-command-palette',
sequence: `${PRIMARY}+K`, sequence: `${PRIMARY}+K`,
title: 'Toggle Command Palette', title: 'Toggle Command Palette',
description: 'Always available. Use Ctrl+/ on Windows/Linux.', description: 'Always available.',
}, },
], ],
Panes: [ Panes: [

View File

@ -1,5 +1,5 @@
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
const DESC = ':desc' const DESC = ':desc'

View File

@ -1,7 +1,4 @@
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { Project, FileEntry } from 'lib/project'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
export type IndexLoaderData = { export type IndexLoaderData = {
code: string | null code: string | null

View File

@ -151,6 +151,32 @@ export function platform(): Platform {
return '' return ''
} }
} }
// navigator.platform is deprecated, but many browsers still support it, and
// it's more accurate than userAgent and userAgentData in Playwright.
if (
navigator.platform?.indexOf('Mac') === 0 ||
navigator.platform === 'iPhone'
) {
return 'macos'
}
if (navigator.platform === 'Win32') {
return 'windows'
}
// Chrome only, but more accurate than userAgent.
let userAgentDataPlatform: unknown
if (
'userAgentData' in navigator &&
navigator.userAgentData &&
typeof navigator.userAgentData === 'object' &&
'platform' in navigator.userAgentData
) {
userAgentDataPlatform = navigator.userAgentData.platform
if (userAgentDataPlatform === 'macOS') return 'macos'
if (userAgentDataPlatform === 'Windows') return 'windows'
}
if (navigator.userAgent.indexOf('Mac') !== -1) { if (navigator.userAgent.indexOf('Mac') !== -1) {
return 'macos' return 'macos'
} else if (navigator.userAgent.indexOf('Win') !== -1) { } else if (navigator.userAgent.indexOf('Win') !== -1) {
@ -158,7 +184,12 @@ export function platform(): Platform {
} else if (navigator.userAgent.indexOf('Linux') !== -1) { } else if (navigator.userAgent.indexOf('Linux') !== -1) {
return 'linux' return 'linux'
} }
console.error('Unknown platform userAgent:', navigator.userAgent) console.error(
'Unknown platform userAgent:',
navigator.platform,
userAgentDataPlatform,
navigator.userAgent
)
return '' return ''
} }

View File

@ -1,6 +1,5 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import type { FileEntry } from 'lib/types' import { Project, FileEntry } from 'lib/project'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export const fileMachine = createMachine( export const fileMachine = createMachine(
{ {

View File

@ -1,6 +1,6 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig' import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'lib/project'
export const homeMachine = createMachine( export const homeMachine = createMachine(
{ {

View File

@ -4,7 +4,6 @@ import {
VariableDeclarator, VariableDeclarator,
parse, parse,
recast, recast,
sketchGroupFromKclValue,
} from 'lang/wasm' } from 'lang/wasm'
import { Axis, Selection, Selections, updateSelections } from 'lib/selections' import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
@ -35,7 +34,7 @@ import {
setEqualLengthInfo, setEqualLengthInfo,
} from 'components/Toolbar/EqualLength' } from 'components/Toolbar/EqualLength'
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst' import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
import { addFillet } from 'lang/modifyAst/addFillet' import { applyFilletToSelection } from 'lang/modifyAst/addFillet'
import { getNodeFromPath } from '../lang/queryAst' import { getNodeFromPath } from '../lang/queryAst'
import { import {
applyConstraintEqualAngle, applyConstraintEqualAngle,
@ -59,7 +58,6 @@ import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { getExtrusionFromSuspectedPath } from 'lang/std/artifactGraph'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -1163,65 +1161,16 @@ export const modelingMachine = createMachine(
'AST fillet': async (_, event) => { 'AST fillet': async (_, event) => {
if (!event.data) return if (!event.data) return
// Extract inputs
const { selection, radius } = event.data const { selection, radius } = event.data
let ast = kclManager.ast
if ( // Apply fillet to selection
'variableName' in radius && const applyFilletToSelectionResult = applyFilletToSelection(
radius.variableName && selection,
radius.insertIndex !== undefined radius
) {
const newBody = [...ast.body]
newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
ast.body = newBody
}
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeBasedSelections[0].range
) )
if (err(applyFilletToSelectionResult))
const varDecNode = getNodeFromPath<VariableDeclaration>( return applyFilletToSelectionResult
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = sketchGroupFromKclValue(
kclManager.programMemory.get(sketchVar),
sketchVar
)
if (trap(sketchGroup)) return
const extrusion = getExtrusionFromSuspectedPath(
sketchGroup.id,
engineCommandManager.artifactGraph
)
const pathToExtrudeNode = err(extrusion)
? []
: getNodePathFromSourceRange(ast, extrusion.codeRef.range)
// we assume that there is only one body related to the sketch
// and apply the fillet to it
const addFilletResult = addFillet(
ast,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius
? radius.variableIdentifierAst
: radius.valueAst
)
if (trap(addFilletResult)) return
const { modifiedAst, pathToFilletNode } = addFilletResult
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}, },
'conditionally equip line tool': (_, { type }) => { 'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') { if (type === 'done.invoke.animate-to-face') {

View File

@ -8,6 +8,13 @@ import { Issuer } from 'openid-client'
import { Bonjour, Service } from 'bonjour-service' import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343 // @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import' import * as kittycad from '@kittycad/lib/import'
import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
let mainWindow: BrowserWindow | null = null
// Check the command line arguments for a project path
const args = parseCLIArgs()
// If it's not set, scream. // If it's not set, scream.
const NODE_ENV = process.env.NODE_ENV || 'production' const NODE_ENV = process.env.NODE_ENV || 'production'
@ -22,8 +29,25 @@ if (require('electron-squirrel-startup')) {
app.quit() app.quit()
} }
const createWindow = () => { const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
const mainWindow = new BrowserWindow({
/// Register our application to handle all "electron-fiddle://" protocols.
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
path.resolve(process.argv[1]),
])
}
} else {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL)
}
// Global app listeners
// Must be done before ready event.
registerStartupListeners()
const createWindow = (): BrowserWindow => {
const newWindow = new BrowserWindow({
autoHideMenuBar: true, autoHideMenuBar: true,
show: false, show: false,
width: 1800, width: 1800,
@ -35,13 +59,15 @@ const createWindow = () => {
preload: path.join(__dirname, './preload.js'), preload: path.join(__dirname, './preload.js'),
}, },
icon: path.resolve(process.cwd(), 'assets', 'icon.png'), icon: path.resolve(process.cwd(), 'assets', 'icon.png'),
frame: false,
titleBarStyle: 'hiddenInset',
}) })
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else { } else {
mainWindow.loadFile( newWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`) path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
) )
} }
@ -49,7 +75,9 @@ const createWindow = () => {
// Open the DevTools. // Open the DevTools.
// mainWindow.webContents.openDevTools() // mainWindow.webContents.openDevTools()
mainWindow.show() newWindow.show()
return newWindow
} }
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
@ -64,7 +92,10 @@ app.on('window-all-closed', () => {
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on('ready', createWindow) app.on('ready', (event, data) => {
// Create the mainWindow
mainWindow = createWindow()
})
// For now there is no good reason to separate these out to another file(s) // For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything // There is just not enough code to warrant it and further abstracts everything
@ -159,3 +190,104 @@ ipcMain.handle('find_machine_api', () => {
) )
}) })
}) })
ipcMain.handle('loadProjectAtStartup', async () => {
// If we are in development mode, we don't want to load a project at
// startup.
// Since the args passed are always '.'
if (NODE_ENV !== 'production') {
return null
}
let projectPath: string | null = null
// macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles
if (macOpenFiles && macOpenFiles && macOpenFiles.length > 0) {
projectPath = macOpenFiles[0] // We only do one project at a time
}
// Reset this so we don't accidentally use it again.
const macOpenFilesEmpty: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFilesEmpty
// macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) {
projectPath = getOpenUrls[0] // We only do one project at a
}
// Reset this so we don't accidentally use it again.
// @ts-ignore
global['getOpenUrls'] = []
// Check if we have a project path in the command line arguments
// If we do, we will load the project at that path
if (args._.length > 1) {
if (args._[1].length > 0) {
projectPath = args._[1]
// Reset all this value so we don't accidentally use it again.
args._[1] = ''
}
}
if (projectPath) {
// We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`)
try {
const currentFile = await getCurrentProjectFile(projectPath)
console.log(`Project loaded: ${currentFile}`)
return currentFile
} catch (e) {
console.error(e)
}
return null
}
return null
})
function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {})
}
function registerStartupListeners() {
/**
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
*/
const macOpenFiles: string[] = []
// @ts-ignore
global['macOpenFiles'] = macOpenFiles
app.on('open-file', function (event, path) {
event.preventDefault()
macOpenFiles.push(path)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
}
})
/**
* macOS: react to open-url requests.
*/
const openUrls: string[] = []
// @ts-ignore
global['openUrls'] = openUrls
const onOpenUrl = function (
event: { preventDefault: () => void },
url: string
) {
event.preventDefault()
openUrls.push(url)
// If we have a mainWindow, lets open another window.
if (mainWindow) {
createWindow()
}
}
app.on('will-finish-launching', function () {
app.on('open-url', onOpenUrl)
})
}

View File

@ -60,6 +60,9 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> => const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api') ipcRenderer.invoke('find_machine_api')
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
login, login,
// Passing fs directly is not recommended since it gives a lot of power // Passing fs directly is not recommended since it gives a lot of power
@ -93,6 +96,7 @@ contextBridge.exposeInMainWorld('electron', {
isWindows, isWindows,
isLinux, isLinux,
}, },
loadProjectAtStartup,
// IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is // IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is
// no way to set it across the bridge boundary. We need to make it a command. // no way to set it across the bridge boundary. We need to make it a command.
setBaseUrl: (value: string) => (process.env.BASE_URL = value), setBaseUrl: (value: string) => (process.env.BASE_URL = value),

View File

@ -31,13 +31,13 @@ import { kclManager } from 'lib/singletons'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings' import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { LowerRightControls } from 'components/LowerRightControls' import { LowerRightControls } from 'components/LowerRightControls'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { import {
createNewProjectDirectory, createNewProjectDirectory,
listProjects, listProjects,
renameProjectDirectory, renameProjectDirectory,
} from 'lib/desktop' } from 'lib/desktop'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar' import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
import { Project } from 'lib/project'
// This route only opens in the desktop context for now, // This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types. // as defined in Router.tsx, so we can use the desktop APIs and types.

View File

@ -2,6 +2,8 @@ import usePlatform from 'hooks/usePlatform'
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.' import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
export default function CmdK() { export default function CmdK() {
const { context } = useModelingContext() const { context } = useModelingContext()
@ -20,15 +22,9 @@ export default function CmdK() {
<h2 className="text-2xl font-bold">Command Bar</h2> <h2 className="text-2xl font-bold">Command Bar</h2>
<p className="my-4"> <p className="my-4">
Press{' '} Press{' '}
{platformName === 'macos' ? ( <kbd className={kbdClasses}>
<> {hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platformName)}
<kbd className={kbdClasses}>K</kbd> </kbd>{' '}
</>
) : (
<>
<kbd className={kbdClasses}>Ctrl + /</kbd>
</>
)}{' '}
to open the command bar. Try changing your theme with it. to open the command bar. Try changing your theme with it.
</p> </p>
<p className="my-4"> <p className="my-4">

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

@ -70,54 +70,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.8" version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.86"
@ -169,7 +127,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -180,7 +138,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -191,7 +149,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -426,12 +384,8 @@ version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [ dependencies = [
"anstream",
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim 0.11.0",
"unicase",
"unicode-width",
] ]
[[package]] [[package]]
@ -443,7 +397,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -452,12 +406,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "colored" name = "colored"
version = "2.1.0" version = "2.1.0"
@ -642,8 +590,8 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim 0.10.0", "strsim",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -654,7 +602,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -709,7 +657,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
"synstructure", "synstructure",
] ]
@ -724,7 +672,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.24" version = "0.1.25"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -738,7 +686,7 @@ dependencies = [
"rustfmt-wrapper", "rustfmt-wrapper",
"serde", "serde",
"serde_tokenstream", "serde_tokenstream",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -749,7 +697,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -776,7 +724,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -948,7 +896,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -1038,7 +986,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -1397,7 +1345,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.7" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1464,12 +1412,12 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.8" version = "0.1.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper", "hyper",
@ -1482,9 +1430,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.3.16" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a9621df809fa105ae28b01586c52ce46561e166702babb50e5bcc5097a8ce62" checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1492,7 +1440,6 @@ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
"chrono", "chrono",
"clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures", "futures",
@ -1852,7 +1799,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.8.3", "regex-syntax 0.8.3",
"structmeta", "structmeta",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -1905,7 +1852,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2069,7 +2016,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2082,7 +2029,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-build-config", "pyo3-build-config",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2096,9 +2043,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2544,7 +2491,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2594,9 +2541,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -2612,13 +2559,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2629,14 +2576,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.125" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.2.5",
"itoa", "itoa",
@ -2653,7 +2600,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2674,7 +2621,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2796,12 +2743,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]] [[package]]
name = "structmeta" name = "structmeta"
version = "0.3.0" version = "0.3.0"
@ -2811,7 +2752,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2822,7 +2763,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2866,9 +2807,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.75" version = "2.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2889,7 +2830,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -2996,7 +2937,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -3091,7 +3032,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -3244,7 +3185,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -3272,7 +3213,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -3349,7 +3290,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
"termcolor", "termcolor",
] ]
@ -3469,12 +3410,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.10.0" version = "1.10.0"
@ -3513,7 +3448,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]
@ -3574,7 +3509,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3609,7 +3544,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3626,7 +3561,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
"clap",
"console_error_panic_hook", "console_error_panic_hook",
"data-encoding", "data-encoding",
"futures", "futures",
@ -3935,7 +3869,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.75", "syn 2.0.76",
] ]
[[package]] [[package]]

View File

@ -11,12 +11,11 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] } bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
clap = "4.5.16"
data-encoding = "2.6.0" data-encoding = "2.6.0"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad.workspace = true kittycad.workspace = true
serde_json = "1.0.125" serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["sync"] } tokio = { version = "1.39.3", features = ["sync"] }
toml = "0.8.19" toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
@ -71,7 +70,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
http = "0.2.12" http = "0.2.12"
kittycad = { version = "0.3.16", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.17", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4" kittycad-modeling-session = "0.1.4"
[[test]] [[test]]

View File

@ -1,7 +1,7 @@
[package] [package]
name = "derive-docs" name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.24" version = "0.1.25"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -18,9 +18,9 @@ once_cell = "1.19.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.75", features = ["full"] } syn = { version = "2.0.76", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-test-server" name = "kcl-test-server"
description = "A test server for KCL" description = "A test server for KCL"
version = "0.1.8" version = "0.1.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -10,6 +10,6 @@ anyhow = "1.0.86"
hyper = { version = "0.14.29", features = ["server"] } hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { version = "0.2", path = "../kcl" } kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0" pico-args = "0.5.0"
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.125" serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.7" version = "0.2.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -16,7 +16,7 @@ async-recursion = "1.1.1"
async-trait = "0.1.81" async-trait = "0.1.81"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.16", default-features = false, optional = true } clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0" convert_case = "0.6.0"
dashmap = "6.0.1" dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
@ -27,7 +27,7 @@ git_rev = "0.1.0"
gltf-json = "1.4.1" gltf-json = "1.4.1"
http = { workspace = true } http = { workspace = true }
image = { version = "0.25.1", default-features = false, features = ["png"] } image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, features = ["clap"] } kittycad = { workspace = true }
lazy_static = "1.5.0" lazy_static = "1.5.0"
measurements = "0.11.0" measurements = "0.11.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
@ -36,8 +36,8 @@ pyo3 = { version = "0.22.2", optional = true }
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] } reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.208", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.125" serde_json = "1.0.127"
sha2 = "0.10.8" sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true } tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.63" thiserror = "1.0.63"
@ -66,7 +66,7 @@ tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"]
tower-lsp = { version = "0.20.0", features = ["proposed"] } tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features] [features]
default = ["cli", "engine"] default = ["engine"]
cli = ["dep:clap"] cli = ["dep:clap"]
# For the lsp server, when run with stdout for rpc we want to disable println. # For the lsp server, when run with stdout for rpc we want to disable println.
# This is used for editor extensions that use the lsp server. # This is used for editor extensions that use the lsp server.

View File

@ -18,3 +18,11 @@ We've built a lot of tooling to make contributing to KCL easier. If you are inte
10. Run `TWENTY_TWENTY=overwrite cargo nextest run --workspace --no-fail-fast` to take snapshot tests of your example code running in the engine 10. Run `TWENTY_TWENTY=overwrite cargo nextest run --workspace --no-fail-fast` to take snapshot tests of your example code running in the engine
11. Run `EXPECTORATE=overwrite cargo test --all generate_stdlib -- --nocapture` to generate new Markdown documentation for your function that will be used [to generate docs on our website](https://zoo.dev/docs/kcl). 11. Run `EXPECTORATE=overwrite cargo test --all generate_stdlib -- --nocapture` to generate new Markdown documentation for your function that will be used [to generate docs on our website](https://zoo.dev/docs/kcl).
12. Create a PR in GitHub. 12. Create a PR in GitHub.
## Bumping the version
If you bump the version of kcl-lib and push it to crates, be sure to update the repos we own that use it as well. These are:
- [kcl.py](https://github.com/kittycad/kcl.py)
- [kcl-lsp](https://github.com/kittycad/kcl-lsp)
- [cli](https://github.com/kittycad/cli)

View File

@ -70,55 +70,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.86"
@ -377,54 +328,6 @@ dependencies = [
"windows-targets 0.52.5", "windows-targets 0.52.5",
] ]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.75",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]] [[package]]
name = "colored" name = "colored"
version = "2.1.0" version = "2.1.0"
@ -596,7 +499,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.24" version = "0.1.25"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"convert_case", "convert_case",
@ -906,12 +809,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -1090,12 +987,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -1140,7 +1031,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.6" version = "0.2.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1149,7 +1040,6 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bson", "bson",
"chrono", "chrono",
"clap",
"convert_case", "convert_case",
"dashmap 6.0.1", "dashmap 6.0.1",
"databake", "databake",
@ -1158,6 +1048,7 @@ dependencies = [
"futures", "futures",
"git_rev", "git_rev",
"gltf-json", "gltf-json",
"http 0.2.12",
"image", "image",
"js-sys", "js-sys",
"kittycad", "kittycad",
@ -1198,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5e9c51976882cdf6777557fd8c3ee68b00bb53e9307fc1721acb397f2ece9a" checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1208,7 +1099,6 @@ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
"chrono", "chrono",
"clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures", "futures",
@ -2197,7 +2087,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [ dependencies = [
"heck 0.4.1", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
@ -2649,12 +2539,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -2685,12 +2569,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.10.0" version = "1.10.0"

View File

@ -9,7 +9,7 @@ use std::{
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use dashmap::DashMap; use dashmap::DashMap;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use kittycad::types::{WebSocketRequest, WebSocketResponse}; use kittycad::types::{ModelingSessionData, WebSocketRequest, WebSocketResponse};
use tokio::sync::{mpsc, oneshot, RwLock}; use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg; use tokio_tungstenite::tungstenite::Message as WsMsg;
@ -27,10 +27,10 @@ enum SocketHealth {
type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>; type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] // for the TcpReadHandle
pub struct EngineConnection { pub struct EngineConnection {
engine_req_tx: mpsc::Sender<ToEngineReq>, engine_req_tx: mpsc::Sender<ToEngineReq>,
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>, responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>,
#[allow(dead_code)]
tcp_read_handle: Arc<TcpReadHandle>, tcp_read_handle: Arc<TcpReadHandle>,
socket_health: Arc<Mutex<SocketHealth>>, socket_health: Arc<Mutex<SocketHealth>>,
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>, batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
@ -38,6 +38,8 @@ pub struct EngineConnection {
/// The default planes for the scene. /// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
/// If the server sends session data, it'll be copied to here.
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
} }
pub struct TcpRead { pub struct TcpRead {
@ -181,6 +183,8 @@ impl EngineConnection {
let mut tcp_read = TcpRead { stream: tcp_read }; let mut tcp_read = TcpRead { stream: tcp_read };
let session_data: Arc<Mutex<Option<ModelingSessionData>>> = Arc::new(Mutex::new(None));
let session_data2 = session_data.clone();
let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new()); let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new());
let responses_clone = responses.clone(); let responses_clone = responses.clone();
let socket_health = Arc::new(Mutex::new(SocketHealth::Active)); let socket_health = Arc::new(Mutex::new(SocketHealth::Active));
@ -192,35 +196,40 @@ impl EngineConnection {
match tcp_read.read().await { match tcp_read.read().await {
Ok(ws_resp) => { Ok(ws_resp) => {
// If we got a batch response, add all the inner responses. // If we got a batch response, add all the inner responses.
if let Some(kittycad::types::OkWebSocketResponseData::ModelingBatch { responses }) = match &ws_resp.resp {
&ws_resp.resp Some(kittycad::types::OkWebSocketResponseData::ModelingBatch { responses }) => {
{ for (resp_id, batch_response) in responses {
for (resp_id, batch_response) in responses { let id: uuid::Uuid = resp_id.parse().unwrap();
let id: uuid::Uuid = resp_id.parse().unwrap(); if let Some(response) = &batch_response.response {
if let Some(response) = &batch_response.response { responses_clone.insert(
responses_clone.insert( id,
id, kittycad::types::WebSocketResponse {
kittycad::types::WebSocketResponse { request_id: Some(id),
request_id: Some(id), resp: Some(kittycad::types::OkWebSocketResponseData::Modeling {
resp: Some(kittycad::types::OkWebSocketResponseData::Modeling { modeling_response: response.clone(),
modeling_response: response.clone(), }),
}), errors: None,
errors: None, success: Some(true),
success: Some(true), },
}, );
); } else {
} else { responses_clone.insert(
responses_clone.insert( id,
id, kittycad::types::WebSocketResponse {
kittycad::types::WebSocketResponse { request_id: Some(id),
request_id: Some(id), resp: None,
resp: None, errors: batch_response.errors.clone(),
errors: batch_response.errors.clone(), success: Some(false),
success: Some(false), },
}, );
); }
} }
} }
Some(kittycad::types::OkWebSocketResponseData::ModelingSessionData { session }) => {
let mut sd = session_data2.lock().unwrap();
sd.replace(session.clone());
}
_ => {}
} }
if let Some(id) = ws_resp.request_id { if let Some(id) = ws_resp.request_id {
@ -249,6 +258,7 @@ impl EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(HashMap::new())), batch_end: Arc::new(Mutex::new(HashMap::new())),
default_planes: Default::default(), default_planes: Default::default(),
session_data,
}) })
} }
} }
@ -345,4 +355,8 @@ impl EngineManager for EngineConnection {
source_ranges: vec![source_range], source_ranges: vec![source_range],
})) }))
} }
fn get_session_data(&self) -> Option<ModelingSessionData> {
self.session_data.lock().unwrap().clone()
}
} }

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