Compare commits

...

62 Commits

Author SHA1 Message Date
49c8fc4c97 Update bracket example code and some test colors that broke 2024-09-03 18:35:09 -04:00
3cbda10eab Update next/prev adjacency + tests to match engine (#3706)
* update next/prev adjacency + tests to match engine

* quick tests fix post engine merge
2024-09-03 13:34:31 -07:00
0f3432b5a0 fix frame on non macs (#3751)
* fix frame on non macs

* fixes

* fmt

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jessie Frazelle <jess@zoo.dev>
2024-09-03 12:11:29 -07:00
f11dc07f0b internal: Remove unused wait-on dependency (#3749)
Remove unused wait-on dependency
2024-09-03 14:30:57 -04:00
e49beb6609 Package with electron-builder and enable auto-updates (#3717)
* WIP: enable build releases
Will eventually fix #3528

* Build on all branches

* WIP: electron-forge publish to gcs

* WIP env var

* WIP windows

* WIP checkout in publish

* Back to matrix for build-apps and upstream wasm build

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP clean up out upload with all dry runs

* WIP macos

* Clean up

* Add update-electron-app

* Bump version down to 0.24.11

* Explicit NODE_ENV=production

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

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

* Push dummy version 0.99.99

* Undo

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

* Trigger CI

* Lint

* Experiment with DMG and MSI

* Split up artifacts

* Executable name to Zoo Modeling App

* Linux kebab-case exe, autoUpdate on wix

* Experiment with electron-builder

* WIP

* fail-fast false

* tronb:vite

* DMG and NSIS

* Typo

* Disable updater for electron-builder tests, quick fix

* WIP macOS sign and notarize

* WIP Win signing

* CSC_FOR_PULL_REQUEST

* Comment out signingHashAlgorithms

* APPLE_APP_SPECIFIC_PASSWORD and move scripts

* notarize: true and change script link

* mac.notarize.teamid

* Clean up and first steps on auto updater

* Lint

* Add logs

* Work on nsis config

* More extensive configs

* Clean up

* Test push updater

* Push again; Fix lint

* Bump down to 0.24.11 to test, disable publish

* WIP mac updater

* Back to .12 to push zips

* Back to .11 to test

* Back to .12 to push to same dir

* Fix windows and names

* Back to .11 to test, no publish

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

* Push again .12

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

* Add publisherName as in certificate

* Back to 11 build

* Add msi target

* MSI params

* perMarchine: false

* .12 msi push

* WIP tauri bundle generation (macOS)

* Typo

* Universal build mac

* Test last_update tauri gen for macOS

* VERSION fix

* Add v to VERSION

* Add v to VERSION part 2

* Fix tar

* WIP windows updater

* WIP windows

* Change Compress-Archive to 7z on Windows

* 7z change

* Fix flag

* -mm Deflate

* -mm Copy and version .99

* perMachine true

* perMachine true

* Manual autoUpdater.quitAndInstall

* Test NSIS for tauri transition

* WIP

* No more universal for mac, last_download.json endpoint

* Typo in json

* Tweaks

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

* Fix typo in download.json

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

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

This reverts commit 0d6d67ec2c.

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

This reverts commit b01bc589ab.

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

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

This reverts commit 5deff7614f.

* Fix tauri update json for universal to arch specified

* Fix tauri update json for universal to arch specified part 2

* Fix tauri update json for universal to arch specified part 3

* Back to checkUpdateAndNotify, frames on window

* Clean up

* Default prod env values

* CI clean up

* More clean up

* Override if forge env not set

* Make basic-sketch test more robust

* Fix env vars set

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49lf <ircsurfer33@gmail.com>
2024-09-03 12:30:14 -04:00
b8f27b77a8 Bump async-trait from 0.1.81 to 0.1.82 in /src/wasm-lib (#3747)
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.81 to 0.1.82.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.81...0.1.82)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 09:15:03 -07:00
fa7e31223d Bump postcss from 8.4.41 to 8.4.43 (#3740)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.41 to 8.4.43.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.41...8.4.43)

---
updated-dependencies:
- dependency-name: postcss
  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-09-02 14:32:32 -07:00
f04c4588df Update .gitignore 2024-09-02 14:16:08 -07:00
c95812efa6 Bump kittycad from 0.3.17 to 0.3.18 in /src/wasm-lib (#3674)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.3.17 to 0.3.18.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.3.17...v0.3.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 12:38:50 -07:00
96385cd5ee Bump syn from 2.0.76 to 2.0.77 in /src/wasm-lib (#3743)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.76 to 2.0.77.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.76...2.0.77)

---
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-09-02 11:46:07 -07:00
64707edaad Bump tokio from 1.39.3 to 1.40.0 in /src/wasm-lib (#3742)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.39.3 to 1.40.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.39.3...tokio-1.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 11:45:58 -07:00
27baf135e7 Parse multi-line comments in |> expressions (#3731)
Fixes #3708, ty @mlfarrell for reporting!
2024-08-30 21:12:00 -05:00
a4cf68c661 replace tanArc(to) with tanArcToRelative (#3729)
* replace tanArc(to) with tanArcToRelative

Closes #3319

* Handle invalid tanArcs
2024-08-30 13:44:20 -05:00
403e074249 Nadro/3686/file swapping while executing (#3703)
* chore: Implemented a executeAst interrupt to stop processing a KCL program

* fix: added a catch since this promise was not being caught

* fix: fmt formatting, need to fix some tsc errors next.

* fix: fixing tsc errors

* fix: cleaning up comment

* fix: only rejecting pending modeling commands

* fix: adding constant for rejection message, adding rejection in WASM send command

* fix: tsc, lint, fmt checks

* fix circ dependency

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-08-30 10:14:24 +00:00
50259aa052 add edges to Artifact Graph (Fillet UI related) (#3675)
* add edges to artifact graph

* update graph snapshot sizes (too cluttered

* fix adjencent reverse issue

* add comments

* remove log

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

* remove log

* remove silly debug

* make wasm-prep windows friendly

* don't swallow error

* more rust tweaks

* Increase test timeout

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-08-30 19:46:48 +10:00
1739f3dafe new snapshots (#3725)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-29 15:58:38 -07:00
7ceb518446 Fix tests for upcoming PR (#3723)
fix tests for upcoming PR
2024-08-29 15:27:00 -07:00
36a6b8c0ea KCL None needs more specific serialization (#3720)
Previously, many KCL values could be deserialized as KCL None values.
This isn't good -- the only thing you should be able to possibly
deserialize as KCL None is KCL None.

Solution was to add a private field in KCL None, serialize it with a
special magic number value, and then check for that magic number when
deserializing
2024-08-29 13:41:32 -05:00
bbdca7421e New sign-in page and signed-in on-load animation (#3684)
* Basic sign-in page layout

* Better responsive styling

* Add wipe animation

* Fix mobile button styling

* Add juicy on-load animation to logo in app header

* Make video card a link to the sample's code

* Fix video URL on bundled desktop app, add "open default browser" behavior to links

* Revert "Add juicy on-load animation to logo in app header"

This reverts commit c167569d7e.

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-08-29 14:21:42 -04:00
03c6f6d60e Fix extrude button (#3718)
* Fix: regression on command bar buttons (remove drag)

* Revert "Fix: regression on command bar buttons (remove drag)"

This reverts commit 2404bcdf31.

* Make all elements opt-out of drag behavior by default, add comments around drag attribute

* Add vendor-specific user-select

* It won't do to make all elements opt-out, it ends up swallowing the drag events themselves!

* Sneaking in this email truncation nit that's bothered me

* Gotta remove that one more attempt at a generic "we made this clickable" element

* Make orbit continue to work when dragging over the AppHeader

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-08-29 10:38:13 -04:00
18c7e7934a remove electron test console noise (#3713) 2024-08-29 07:55:27 +00:00
bf650fd129 Fix Stream shows the inside of sketch after extrude (#3711)
Fix Stream shows the inside of sketch after extrude 2998
2024-08-29 16:45:46 +10:00
81ccb65f15 remove double zoom to fit (#3710)
remove double zoom to fit 3217
2024-08-29 16:28:57 +10:00
335b5100ae Fix playwright artifact paths to be unique (#3704) 2024-08-28 17:30:20 -04:00
1162ff3b03 Update machine-api for modified api schema (#3572)
update to new machine-api format
2024-08-28 15:15:37 -04:00
5e8227ead8 File tree stuff (#3679)
* Fix and test file tree operations

* fmt

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

* Make tsc happy

* I've been lied to

* Fix navigating to deleted file

* tsc

* Remove debugger statement

* Fix test

* All tests fixed

* Remove old config and remove slowmo

* fmt

* Remove unintentional changelog in readme (#3678)

* lint

* fmt

* Increase test timeout

* Fix the damn test

* fix web app

* fmt

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-08-28 06:38:14 -04:00
max
ed339a6b9a Bug: Fillet Button - selecting edge without tag caused an Error rev2 (#3690)
* typos

* typos2

* trigger ci
2024-08-28 01:53:33 +02:00
1d19fc6b7e Fix platform detection in Vite (#3689) 2024-08-27 19:02:49 -04:00
max
5b5355376f Revert "Bug: Fillet Button - selecting edge without tag caused an Error" (#3687)
Revert "Bug: Fillet Button - selecting edge without tag caused an Error (#3685)"

This reverts commit 5c90f72c91.
2024-08-28 00:20:50 +02:00
max
5c90f72c91 Bug: Fillet Button - selecting edge without tag caused an Error (#3685)
fixed
2024-08-27 21:49:46 +00:00
026a8d19cb Remove unintentional changelog in readme (#3678) 2024-08-27 17:27:41 +00:00
6dd0981709 make wasm-build windows safe (#3676)
make wasm-prep windows safe
2024-08-27 11:26:52 +00:00
b231a26115 more conclusive text to cad playwright tests (#3672) 2024-08-27 06:36:05 +00:00
3f47486fb5 fix electron playwright reports/traces (#3670)
add matricx to playwright reports
2024-08-27 02:04:34 +00:00
57e97d16d0 integrate wasm-prep (#3671)
* integrate wasm-prep

* update readme
2024-08-27 01:35:11 +00: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
176 changed files with 18866 additions and 5862 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

@ -1,4 +1,4 @@
name: build-test-publish-apps name: build-publish-apps
on: on:
pull_request: pull_request:
@ -21,7 +21,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
prepare-json-files: prepare-files:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows) runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
@ -33,6 +33,19 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
run: | run: |
@ -42,36 +55,50 @@ jobs:
# TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json # TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files
path: | path: |
package.json package.json
src/wasm-lib/pkg/wasm_lib*
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-app-macos: build-apps:
needs: [prepare-json-files] needs: [prepare-files]
runs-on: macos-14 strategy:
fail-fast: false
matrix:
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
if: github.event_name == 'schedule' name: prepared-files
- name: Copy updated .json files - name: Copy prepared files
if: github.event_name == 'schedule'
run: | run: |
ls -l artifact ls -R prepared-files
cp artifact/package.json package.json cp prepared-files/package.json package.json
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -81,79 +108,10 @@ jobs:
- run: yarn install - run: yarn install
- name: Setup Rust - run: yarn tronb:vite
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
# TODO: sign the app (and updater bundle potentially)
- name: Add signing certificate
if: ${{ env.BUILD_RELEASE == 'true' }}
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
- name: Build the app for arm64
run: "yarn electron-forge make"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*/*"
build-test-app-windows:
needs: [prepare-json-files]
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Prepare certificate and variables (Windows only) - name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
run: | run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12 cat /d/Certificate_pkcs12.p12
@ -168,7 +126,7 @@ jobs:
shell: bash shell: bash
- name: Setup certicate with SSM KSP (Windows only) - name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
run: | run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn msiexec /i smtools-windows-x64.msi /quiet /qn
@ -178,83 +136,47 @@ jobs:
smksp_cert_sync.exe smksp_cert_sync.exe
shell: cmd shell: cmd
- name: Build the app for x64 - name: Build the app
run: "yarn electron-forge make --arch x64" run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
- name: Build the app for arm64 - name: List artifacts in out/
run: "yarn electron-forge make --arch arm64" run: ls -R out
- name: List artifacts - name: Prepare the tauri update bundles (macOS)
run: "ls -R out/make" if: ${{ env.BUILD_RELEASE && matrix.os == 'macos-14' }}
- name: Sign using Signtool
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
run: | run: |
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}" for ARCH in arm64 x64; do
signtool.exe verify /v /pa "${{ env.X64_FILE }}" TAURI_DIR=out/tauri/$VERSION/macos
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}" TEMP_DIR=temp/$ARCH
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}" mkdir -p $TAURI_DIR
mkdir -p $TEMP_DIR
unzip out/*-$ARCH-mac.zip -d $TEMP_DIR
tar -czvf "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" -C $TEMP_DIR "Zoo Modeling App.app"
yarn tauri signer sign "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz"
done
ls -R out
- name: Prepare the tauri update bundles (Windows)
if: ${{ env.BUILD_RELEASE && matrix.os == 'windows-2022' }}
run: |
$env:TAURI_DIR="out/tauri/${env:VERSION}/nsis"
mkdir -p ${env:TAURI_DIR}
$env:OUT_FILE="${env:TAURI_DIR}/Zoo Modeling App_${env:VERSION_NO_V}_x64-setup.nsis.zip"
7z a -mm=Copy "${env:OUT_FILE}" ./out/*-x64-win.exe
yarn tauri signer sign "${env:OUT_FILE}"
ls -R out
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
path: "out/make/*/*/*" name: out-${{ matrix.os }}
path: |
# TODO: Run e2e tests out/Zoo*.*
out/latest*.yml
out/tauri
build-test-app-ubuntu:
needs: [prepare-json-files]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Build the app for arm64
run: "yarn electron-forge make --arch arm64"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
# TODO: sign the app (and updater bundle potentially) # TODO: add the updater tests back
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
publish-apps-release: publish-apps-release:
@ -262,87 +184,107 @@ jobs:
permissions: permissions:
contents: write contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu] needs: [prepare-files, build-apps]
env: env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }} NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }} WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
BUCKET_DIR_TAURI: 'dl.kittycad.io/releases/modeling-app/tauri-compat'
WEBSITE_DIR_TAURI: 'dl.zoo.dev/releases/modeling-app/tauri-compat'
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/checkout@v4
- name: Generate the update static endpoint - uses: actions/download-artifact@v3
with:
name: out-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-ubuntu-22.04
path: out
- name: Generate the download static endpoint
run: | run: |
ls -l artifact/*/*oo* RELEASE_DIR=https://${WEBSITE_DIR}
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \ jq --null-input \
--arg version "${VERSION}" \ --arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \ --arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \ --arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \ --arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \ --arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.msi" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \ --arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.msi" \
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \ '{
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \ "version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-arm64": {
"url": $mac_arm64_url
},
"dmg-x64": {
"url": $mac_x64_url
},
"msi-arm64": {
"url": $windows_arm64_url
},
"msi-x64": {
"url": $windows_x64_url
}
}
}' > last_download.json
cat last_download.json
- name: Generate the update static endpoint for tauri
run: |
TAURI_DIR=out/tauri/$VERSION
MAC_ARM64_SIG=`cat $TAURI_DIR/macos/*-arm64.app.tar.gz.sig`
MAC_X64_SIG=`cat $TAURI_DIR/macos/*-x64.app.tar.gz.sig`
WINDOWS_SIG=`cat $TAURI_DIR/nsis/*.nsis.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR_TAURI}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg mac_arm64_sig "$MAC_ARM64_SIG" \
--arg mac_arm64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-arm64.app.tar.gz" \
--arg mac_x64_sig "$MAC_X64_SIG" \
--arg mac_x64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-x64.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/nsis/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64-setup.nsis.zip" \
'{ '{
"version": $version, "version": $version,
"pub_date": $pub_date, "pub_date": $pub_date,
"notes": $notes, "notes": $notes,
"platforms": { "platforms": {
"darwin-x86_64": { "darwin-x86_64": {
"signature": $darwin_sig, "signature": $mac_x64_sig,
"url": $darwin_url "url": $mac_x64_url
}, },
"darwin-aarch64": { "darwin-aarch64": {
"signature": $darwin_sig, "signature": $mac_arm64_sig,
"url": $darwin_url "url": $mac_arm64_url
}, },
"windows-x86_64": { "windows-x86_64": {
"signature": $windows_x86_64_sig, "signature": $windows_sig,
"url": $windows_x86_64_url "url": $windows_url
},
"windows-aarch64": {
"signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
} }
} }
}' > last_update.json }' > last_update.json
cat last_update.json cat last_update.json
- name: Generate the download static endpoint - name: List artifacts
run: | run: "ls -R out"
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_x86_64_url
},
"msi-aarch64": {
"url": $windows_aarch64_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.5' uses: 'google-github-actions/auth@v2.1.5'
@ -352,21 +294,23 @@ jobs:
- name: Set up Google Cloud SDK - name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0 uses: google-github-actions/setup-gcloud@v2.1.0
with: with:
project_id: kittycadapi project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload release files to public bucket - name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: artifact path: out
glob: '*/Zoo*' glob: 'Zoo*'
parent: false parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }} destination: ${{ env.BUCKET_DIR }}
- name: Upload update endpoint to public bucket - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: last_update.json path: out
destination: ${{ env.BUCKET_DIR }} glob: 'latest*'
parent: false
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.3 uses: google-github-actions/upload-cloud-storage@v2.1.3
@ -374,11 +318,27 @@ jobs:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: "out/tauri/${{ env.VERSION }}"
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket for tauri
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github - name: Upload release files to Github
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: 'artifact/*/Zoo*' files: 'out/Zoo*'
# TODO: Add GitHub publisher
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

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

View File

@ -139,7 +139,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
@ -174,14 +174,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true' if: steps.git-check.outputs.modified == 'true'
with: with:
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run playwright/chrome flow (with retries) - name: Run playwright/chrome flow (with retries)
id: retry id: retry
@ -244,14 +244,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
@ -351,7 +351,7 @@ jobs:
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-ubuntu-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run electron tests (with retries) - name: Run electron tests (with retries)
id: retry id: retry
@ -423,14 +423,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: test-results-electron-${{ github.sha }} name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/ path: test-results/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-electron-${{ github.sha }} name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true

6
.gitignore vendored
View File

@ -54,19 +54,15 @@ 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
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg
venv venv
.vite/ .vite/
# 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

@ -110,7 +110,6 @@ Which commands from setup are one off vs need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale: The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash ```bash
yarn install yarn install
yarn wasm-prep
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
yarn start # or yarn build:local && yarn serve for slower but more production-like build yarn start # or yarn build:local && yarn serve for slower but more production-like build
``` ```
@ -189,12 +188,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 +212,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
``` ```

View File

@ -1,24 +0,0 @@
#!/usr/bin/env sh
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
#create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12

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)
@ -87,6 +88,7 @@ layout: manual
* [`tan`](kcl/tan) * [`tan`](kcl/tan)
* [`tangentialArc`](kcl/tangentialArc) * [`tangentialArc`](kcl/tangentialArc)
* [`tangentialArcTo`](kcl/tangentialArcTo) * [`tangentialArcTo`](kcl/tangentialArcTo)
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
* [`tau`](kcl/tau) * [`tau`](kcl/tau)
* [`toDegrees`](kcl/toDegrees) * [`toDegrees`](kcl/toDegrees)
* [`toRadians`](kcl/toRadians) * [`toRadians`](kcl/toRadians)

File diff suppressed because it is too large Load Diff

View File

@ -37,8 +37,7 @@ const example = extrude(10, exampleSketch)
offset: number, offset: number,
// Radius of the arc. Not to be confused with Raiders of the Lost Ark. // Radius of the arc. Not to be confused with Raiders of the Lost Ark.
radius: number, radius: number,
} | }
[number, number]
``` ```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED) * `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
```js ```js

File diff suppressed because one or more lines are too long

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

@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
} }
// deselect line tool // deselect line tool
await page.getByTestId('line').click() const btnLine = page.getByTestId('line')
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
if (btnLineAriaPressed === 'true') {
await btnLine.click()
}
await page.waitForTimeout(100)
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0) const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect await expect
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE)) .poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
await expect await expect
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249])) .poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
} }
// click between first two clicks to get center of the line // click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3) await expect(
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
).toBeLessThan(3)
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3) await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
} }
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100)
// click between the latest two clicks to get center of the line // click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
await page.waitForTimeout(100)
// selected two lines therefore there should be two cursors // selected two lines therefore there should be two cursors
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(100)
} }
await page.getByRole('button', { name: 'Length: open menu' }).click() await page.getByRole('button', { name: 'Length: open menu' }).click()

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 })
@ -399,7 +517,7 @@ test.describe('Editor tests', () => {
const width = 0.500 const width = 0.500
const height = 0.500 const height = 0.500
const dia = 4 const dia = 4
fn squareHole = (l, w) => { fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY') const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %) |> startProfileAt([-width / 2, -length / 2], %)

View File

@ -0,0 +1,204 @@
import { test, expect } from '@playwright/test'
import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('when using the file tree to', () => {
const fromFile = 'main.kcl'
const toFile = 'hello.kcl'
test(
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
renameFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await renameFile(fromFile, toFile)
await page.reload()
await test.step('Postcondition: editor has same content as before the rename', async () => {
await editorTextMatches(kclCube)
})
await test.step('Postcondition: opening and closing settings works', async () => {
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsOpenButton.click()
await settingsCloseButton.click()
})
await electronApp.close()
}
)
test(
`create many new untitled files they increment their names`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const { panesOpen, createAndSelectProject, createNewFile } =
await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files'])
await createAndSelectProject('project-000')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /Untitled[-]?[0-5]?/ })
).toHaveCount(5)
})
await electronApp.close()
}
)
test(
'create a new file with the same name as an existing file cancels the operation',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
renameFile,
selectFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
const kcl1 = 'main.kcl'
const kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
await renameFile(kcl2, kcl1)
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
await selectFile(kcl1)
await editorTextMatches(kclCube)
})
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
await selectFile(kcl2)
await editorTextMatches(kclCylinder)
})
await electronApp.close()
}
)
test(
'deleting all files recreates a default main.kcl with no code',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
deleteFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
const kcl1 = 'main.kcl'
await deleteFile(kcl1)
await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => {
await editorTextMatches('')
})
await electronApp.close()
}
)
})

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: 33 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

@ -511,10 +511,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
editorTextMatches: async (code: string) => { editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector) const editor = page.locator(editorSelector)
const editorText = await editor.textContent() return expect(editor).toHaveText(code, { useInnerText: true })
return expect(util.toNormalizedCode(editorText || '')).toBe(
util.toNormalizedCode(code)
)
}, },
pasteCodeInEditor: async (code: string) => { pasteCodeInEditor: async (code: string) => {
@ -532,18 +529,62 @@ export async function getUtils(page: Page, test_?: typeof test) {
}) })
}, },
createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => {
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
})
},
selectFile: async (name: string) => {
return test?.step(`Select ${name}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
})
},
createNewFileAndSelect: async (name: string) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page await page
.getByTestId('file-pane-scroll-container') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name }) .filter({ hasText: name })
.click() .click()
}) })
}, },
renameFile: async (fromName: string, toName: string) => {
return test?.step(`Rename ${fromName} to ${toName}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: fromName })
.click({ button: 'right' })
await page.getByTestId('context-menu-rename').click()
await page.getByTestId('file-rename-field').fill(toName)
await page.keyboard.press('Enter')
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: toName })
.click()
})
},
deleteFile: async (name: string) => {
return test?.step(`Delete ${name}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click({ button: 'right' })
await page.getByTestId('context-menu-delete').click()
await page.getByTestId('delete-confirmation').click()
})
},
panesOpen: async (paneIds: PaneId[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
@ -772,7 +813,6 @@ export async function setup(context: BrowserContext, page: Page) {
localStorage.setItem('persistCode', ``) localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true') localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
console.log('TEST_SETTINGS.projects', settings)
}, },
{ {
token: secrets.token, token: secrets.token,
@ -912,3 +952,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

@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
let noHoverColor: [number, number, number] = [82, 82, 82] let noHoverColor: [number, number, number] = [92, 92, 92]
let hoverColor: [number, number, number] = [116, 116, 116] let hoverColor: [number, number, number] = [127, 127, 127]
let selectColor: [number, number, number] = [144, 148, 97] let selectColor: [number, number, number] = [155, 155, 105]
const extrudeWall = { x: 670, y: 275 } const extrudeWall = { x: 670, y: 275 }
const extrudeText = `line([170.36, -121.61], %, $seg01)` const extrudeText = `line([170.36, -121.61], %, $seg01)`
@ -816,9 +816,9 @@ const extrude001 = extrude(50, sketch001)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face // because of shading, color is not exact everywhere on the face
noHoverColor = [104, 104, 104] noHoverColor = [115, 115, 115]
hoverColor = [134, 134, 134] hoverColor = [145, 145, 145]
selectColor = [158, 162, 110] selectColor = [168, 168, 120]
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6) await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
await page.mouse.move(cap.x, cap.y) await page.mouse.move(cap.x, cap.y)

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,7 @@
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 } from './test-utils'
import { join } from 'path'
import fs from 'fs'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page) await setup(context, page)
@ -683,3 +685,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, dir } = await setupElectron({ testInfo })
const fileExists = () =>
fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl'))
const { createAndSelectProject, panesOpen } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
await panesOpen(['code', 'files'])
// Create and navigate to the project
await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await 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 })
expect(fileExists()).toBeTruthy()
})
await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// 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()
expect(fileExists()).toBeFalsy()
})
await electronApp.close()
}
)

65
electron-builder.yml Normal file
View File

@ -0,0 +1,65 @@
appId: dev.zoo.modeling-app
directories:
output: out
buildResources: assets
files:
- .vite/**
mac:
category: public.app-category.developer-tools
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
notarize:
teamId: 92H8YB3B95
win:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:
- target: nsis
arch:
- x64
- arm64
- target: msi
arch:
- x64
- arm64
signingHashAlgorithms:
- sha256
sign: "./sign-win.js"
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico"
msi:
oneClick: false
perMachine: true
nsis:
oneClick: false
perMachine: true
allowElevation: true
license: "LICENSE"
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
linux:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:
- target: appImage
arch:
- x64
- arm64
publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder
channel: latest

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: {

8
installer.nsh Normal file
View File

@ -0,0 +1,8 @@
!macro preInit
SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
!macroend

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
} }

File diff suppressed because it is too large Load Diff

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",
@ -39,11 +39,13 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.2.1",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"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",
@ -82,14 +84,13 @@
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"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": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e", "lint": "eslint --fix src e2e",
"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",
@ -97,7 +98,9 @@
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make", "tron:make": "electron-forge make",
"tron:publish": "electron-forge publish", "tron:publish": "electron-forge publish",
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron" "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package": "electron-builder --config electron-builder.yml"
}, },
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -119,24 +122,28 @@
}, },
"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",
"@tauri-apps/cli": "^2.0.0-rc.9",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10", "@types/d3-force": "^3.0.10",
"@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",
@ -147,7 +154,6 @@
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
@ -158,6 +164,8 @@
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "^32.0.1", "electron": "^32.0.1",
"electron-builder": "^24.13.3",
"electron-notarize": "^1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
@ -169,7 +177,7 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.43",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
@ -182,7 +190,6 @@
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"wasm-pack": "^0.13.0", "wasm-pack": "^0.13.0",
"ws": "^8.17.0", "ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"

View File

@ -1,31 +0,0 @@
import { defineConfig, devices } from '@playwright/test'
import dotenv from 'dotenv'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 120_000, // override the default 30s timeout
testDir: './e2e/playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Do not retry */
retries: process.env.CI ? 0 : 0,
/* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 1 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[process.env.CI ? 'dot' : 'list'],
['json', { outputFile: './test-results/report.json' }],
['html'],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure',
actionTimeout: 15000,
screenshot: 'only-on-failure',
},
})

BIN
public/wheel-loop-dark.mp4 Normal file

Binary file not shown.

BIN
public/wheel-loop.mp4 Normal file

Binary file not shown.

38
sign-win.js Normal file
View File

@ -0,0 +1,38 @@
// From https://github.com/OpenBuilds/OpenBuilds-CONTROL/blob/4800540ffaa517925fc2cff26670809efa341ffe/signWin.js
const { execSync } = require('node:child_process')
exports.default = async (configuration) => {
if (!process.env.SM_API_KEY) {
console.error(
'Signing using signWin.js script: failed: SM_API_KEY ENV VAR NOT FOUND'
)
return
}
if (!process.env.WINDOWS_CERTIFICATE_THUMBPRINT) {
console.error(
'Signing using signWin.js script: failed: FINGERPRINT ENV VAR NOT FOUND'
)
return
}
if (!configuration.path) {
throw new Error(
`Signing using signWin.js script: failed: TARGET PATH NOT FOUND`
)
}
try {
execSync(
`smctl sign --fingerprint="${
process.env.WINDOWS_CERTIFICATE_THUMBPRINT
}" --input "${String(configuration.path)}"`,
{
stdio: 'inherit',
}
)
console.log('Signing using signWin.js script: successful')
} catch (error) {
console.error('Signing using signWin.js script: failed:', error)
}
}

View File

@ -119,6 +119,15 @@ export function App() {
paneOpacity + paneOpacity +
(context.store?.buttonDownInStream ? ' pointer-events-none' : '') (context.store?.buttonDownInStream ? ' pointer-events-none' : '')
} }
// Override the electron window draggable region behavior as well
// when the button is down in the stream
style={
{
'-webkit-app-region': context.store?.buttonDownInStream
? 'no-drag'
: '',
} as React.CSSProperties
}
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}
/> />

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

@ -42,7 +42,13 @@ export type ActionButtonProps =
export const ActionButton = forwardRef((props: ActionButtonProps, ref) => { export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${ const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
props.iconStart ? (props.iconEnd ? 'px-0' : 'pr-2') : 'px-2' props.iconStart
? props.iconEnd
? 'px-0'
: 'pr-2'
: props.iconEnd
? 'px-2'
: 'pl-2'
} ${props.className ? props.className : ''}` } ${props.className ? props.className : ''}`
switch (props.Element) { switch (props.Element) {

View File

@ -35,7 +35,7 @@ export const ActionIcon = ({
return ( return (
<div <div
className={ className={
`w-fit inline-grid place-content-center ${className} ` + `w-fit self-stretch inline-grid place-content-center ${className} ` +
computedBgClassName computedBgClassName
} }
> >

View File

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

View File

@ -12,6 +12,7 @@ interface AppHeaderProps extends React.PropsWithChildren {
project?: Omit<IndexLoaderData, 'code'> project?: Omit<IndexLoaderData, 'code'>
className?: string className?: string
enableMenu?: boolean enableMenu?: boolean
style?: React.CSSProperties
} }
export const AppHeader = ({ export const AppHeader = ({
@ -19,6 +20,7 @@ export const AppHeader = ({
project, project,
children, children,
className = '', className = '',
style,
enableMenu = false, enableMenu = false,
}: AppHeaderProps) => { }: AppHeaderProps) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
@ -33,6 +35,7 @@ export const AppHeader = ({
' overlaid-panes sticky top-0 z-20 px-2 items-start ' + ' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
className className
} }
style={style}
> >
<ProjectSidebarMenu <ProjectSidebarMenu
enableMenu={enableMenu} enableMenu={enableMenu}

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

@ -135,16 +135,15 @@ interface ContextMenuItemProps {
icon?: ActionIconProps['icon'] icon?: ActionIconProps['icon']
onClick?: () => void onClick?: () => void
hotkey?: string hotkey?: string
'data-testid'?: string
} }
export function ContextMenuItem({ export function ContextMenuItem(props: ContextMenuItemProps) {
children, const { children, icon, onClick, hotkey } = props
icon,
onClick,
hotkey,
}: ContextMenuItemProps) {
return ( return (
<button <button
data-testid={props['data-testid']}
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={onClick} onClick={onClick}
> >

View File

@ -332,7 +332,7 @@ const CustomIconMap = {
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
d="M5.5 4C4.11929 4 3 5.11929 3 6.5V7C3 10.0376 5.46243 12.5 8.5 12.5H8.96482C9.46635 12.5 9.93469 12.2493 10.2129 11.8321L10.5173 11.3755C11.1396 12.0849 12.0423 12.5 13 12.5H13.75H15V14C15 14.2626 14.9483 14.5227 14.8478 14.7654C14.7472 15.008 14.5999 15.2285 14.4142 15.4142C14.2285 15.5999 14.008 15.7472 13.7654 15.8478C13.5227 15.9483 13.2626 16 13 16C12.7374 16 12.4773 15.9483 12.2346 15.8478C11.992 15.7472 11.7715 15.5999 11.5858 15.4142C11.4001 15.2285 11.2528 15.008 11.1522 14.7654C11.1164 14.6789 11.0868 14.5902 11.0635 14.5H11.8544C11.9168 14.6431 12.0056 14.7734 12.1161 14.8839C12.2322 15 12.37 15.092 12.5216 15.1548C12.6733 15.2177 12.8358 15.25 13 15.25C13.1642 15.25 13.3267 15.2177 13.4784 15.1548C13.63 15.092 13.7678 15 13.8839 14.8839C14 14.7678 14.092 14.63 14.1548 14.4784C14.2177 14.3267 14.25 14.1642 14.25 14V13H13.25V14C13.25 14.0328 13.2435 14.0653 13.231 14.0957C13.2184 14.126 13.2 14.1536 13.1768 14.1768C13.1536 14.2 13.126 14.2184 13.0957 14.231C13.0653 14.2435 13.0328 14.25 13 14.25C12.9672 14.25 12.9347 14.2435 12.9043 14.231C12.874 14.2184 12.8464 14.2 12.8232 14.1768C12.8 14.1536 12.7816 14.126 12.769 14.0957C12.7565 14.0653 12.75 14.0328 12.75 14V13.5H12.25H10.5H10V14C10 14.394 10.0776 14.7841 10.2284 15.1481C10.3791 15.512 10.6001 15.8427 10.8787 16.1213C11.1573 16.3999 11.488 16.6209 11.8519 16.7716C12.2159 16.9224 12.606 17 13 17C13.394 17 13.7841 16.9224 14.1481 16.7716C14.512 16.6209 14.8427 16.3999 15.1213 16.1213C15.3999 15.8427 15.6209 15.512 15.7716 15.1481C15.9224 14.7841 16 14.394 16 14V12.5H17V11.5H16V8.5C16 6.01472 13.9853 4 11.5 4H5.5ZM11.084 10.4746L10.9226 10.2326L9.42875 7.74275L8.57125 8.25725L9.90846 10.4859L9.38084 11.2773C9.28811 11.4164 9.13199 11.5 8.96482 11.5H8.5C6.01472 11.5 4 9.48528 4 7V6.5C4 5.67157 4.67157 5 5.5 5H11.5C13.433 5 15 6.567 15 8.5V11.5H13.75H13C12.2301 11.5 11.5111 11.1152 11.084 10.4746ZM13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5Z" d="M5.5 4C4.11929 4 3 5.11929 3 6.5V7C3 10.0376 5.46243 12.5 8.5 12.5H8.96482C9.46635 12.5 9.93469 12.2493 10.2129 11.8321L10.5173 11.3755C11.1396 12.0849 12.0423 12.5 13 12.5H13.75H15V14C15 14.2626 14.9483 14.5227 14.8478 14.7654C14.7472 15.008 14.5999 15.2285 14.4142 15.4142C14.2285 15.5999 14.008 15.7472 13.7654 15.8478C13.5227 15.9483 13.2626 16 13 16C12.7374 16 12.4773 15.9483 12.2346 15.8478C11.992 15.7472 11.7715 15.5999 11.5858 15.4142C11.4001 15.2285 11.2528 15.008 11.1522 14.7654C11.1164 14.6789 11.0868 14.5902 11.0635 14.5H11.8544C11.9168 14.6431 12.0056 14.7734 12.1161 14.8839C12.2322 15 12.37 15.092 12.5216 15.1548C12.6733 15.2177 12.8358 15.25 13 15.25C13.1642 15.25 13.3267 15.2177 13.4784 15.1548C13.63 15.092 13.7678 15 13.8839 14.8839C14 14.7678 14.092 14.63 14.1548 14.4784C14.2177 14.3267 14.25 14.1642 14.25 14V13H13.25V14C13.25 14.0328 13.2435 14.0653 13.231 14.0957C13.2184 14.126 13.2 14.1536 13.1768 14.1768C13.1536 14.2 13.126 14.2184 13.0957 14.231C13.0653 14.2435 13.0328 14.25 13 14.25C12.9672 14.25 12.9347 14.2435 12.9043 14.231C12.874 14.2184 12.8464 14.2 12.8232 14.1768C12.8 14.1536 12.7816 14.126 12.769 14.0957C12.7565 14.0653 12.75 14.0328 12.75 14V13.5H12.25H10.5H10V14C10 14.394 10.0776 14.7841 10.2284 15.1481C10.3791 15.512 10.6001 15.8427 10.8787 16.1213C11.1573 16.3999 11.488 16.6209 11.8519 16.7716C12.2159 16.9224 12.606 17 13 17C13.394 17 13.7841 16.9224 14.1481 16.7716C14.512 16.6209 14.8427 16.3999 15.1213 16.1213C15.3999 15.8427 15.6209 15.512 15.7716 15.1481C15.9224 14.7841 16 14.394 16 14V12.5H17V11.5H16V8.5C16 6.01472 13.9853 4 11.5 4H5.5ZM11.084 10.4746L10.9226 10.2326L9.42875 7.74275L8.57125 8.25725L9.90846 10.4859L9.38084 11.2773C9.28811 11.4164 9.13199 11.5 8.96482 11.5H8.5C6.01472 11.5 4 9.48528 4 7V6.5C4 5.67157 4.67157 5 5.5 5H11.5C13.433 5 15 6.567 15 8.5V11.5H13.75H13C12.2301 11.5 11.5111 11.1152 11.084 10.4746ZM13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5Z"
fill="black" fill="currentColor"
/> />
</svg> </svg>
), ),

View File

@ -16,7 +16,11 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' import {
DEFAULT_FILE_NAME,
DEFAULT_PROJECT_KCL_FILE,
FILE_EXT,
} from 'lib/constants'
import { getProjectInfo } from 'lib/desktop' import { getProjectInfo } from 'lib/desktop'
import { getNextDirName, getNextFileName } from 'lib/desktopFS' import { getNextDirName, getNextFileName } from 'lib/desktopFS'
@ -167,6 +171,25 @@ export const FileMachineProvider = ({
name name
) )
// no-op
if (oldPath === newPath) {
return {
message: `Old is the same as new.`,
newPath,
oldPath,
}
}
// if there are any siblings with the same name, report error.
const entries = await window.electron.readdir(
window.electron.path.dirname(newPath)
)
for (let entry of entries) {
if (entry === newName) {
return Promise.reject(new Error('Filename already exists.'))
}
}
window.electron.rename(oldPath, newPath) window.electron.rename(oldPath, newPath)
if (!file) { if (!file) {
@ -209,6 +232,27 @@ export const FileMachineProvider = ({
.catch((e) => console.error('Error deleting file', e)) .catch((e) => console.error('Error deleting file', e))
} }
// If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
const entries = await window.electron.readdir(project.path)
const hasKclEntries =
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return
}
// If we just deleted the current file or one of its parent directories, // If we just deleted the current file or one of its parent directories,
// navigate to the project root // navigate to the project root
if ( if (

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})`
@ -357,10 +358,18 @@ function FileTreeContextMenu({
<ContextMenu <ContextMenu
menuTargetElement={itemRef} menuTargetElement={itemRef}
items={[ items={[
<ContextMenuItem onClick={onRename} hotkey="Enter"> <ContextMenuItem
data-testid="context-menu-rename"
onClick={onRename}
hotkey="Enter"
>
Rename Rename
</ContextMenuItem>, </ContextMenuItem>,
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}> <ContextMenuItem
data-testid="context-menu-delete"
onClick={onDelete}
hotkey={metaKey + ' + Del'}
>
Delete Delete
</ContextMenuItem>, </ContextMenuItem>,
]} ]}

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

@ -49,7 +49,11 @@ export const NetworkMachineIndicator = ({
{Object.entries(machineManager.machines).map( {Object.entries(machineManager.machines).map(
([hostname, machine]) => ( ([hostname, machine]) => (
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}> <li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.model || machine.manufacturer}</p> <p className="">
{machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine'}
</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs"> <p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Hostname {hostname} Hostname {hostname}
</p> </p>

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

@ -217,7 +217,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</p> </p>
{displayedName !== user.email && ( {displayedName !== user.email && (
<p <p
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs" className="m-0 overflow-ellipsis overflow-hidden text-chalkboard-70 dark:text-chalkboard-40 text-xs"
data-testid="email" data-testid="email"
> >
{user.email} {user.email}

View File

@ -9,6 +9,7 @@ import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
import { import {
getCapCodeRef, getCapCodeRef,
getExtrudeEdgeCodeRef,
getExtrusionFromSuspectedExtrudeSurface, getExtrusionFromSuspectedExtrudeSurface,
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
@ -60,6 +61,13 @@ export function useEngineConnectionSubscriptions() {
? [codeRef.range] ? [codeRef.range]
: [codeRef.range, extrusion.codeRef.range] : [codeRef.range, extrusion.codeRef.range]
) )
} else if (artifact?.type === 'extrudeEdge') {
const codeRef = getExtrudeEdgeCodeRef(
artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'segment') { } else if (artifact?.type === 'segment') {
editorManager.setHighlightRange([ editorManager.setHighlightRange([
artifact?.codeRef?.range || [0, 0], artifact?.codeRef?.range || [0, 0],

View File

@ -4,6 +4,18 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
button,
input,
select,
textarea,
a {
/* Make all interactive elements not act as handles
* to drag the electron app window by default,
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
*/
-webkit-app-region: no-drag;
}
body { body {
margin: 0; margin: 0;
@apply font-sans; @apply font-sans;
@ -97,7 +109,7 @@ button:disabled {
} }
a { a {
@apply text-primary underline hover:hue-rotate-15; @apply text-primary hover:hue-rotate-15;
} }
.dark a { .dark a {
@ -274,6 +286,35 @@ code {
} }
} }
@layer utilities {
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */
@keyframes circle-in-hesitate {
0% {
clip-path: circle(
var(--circle-size-start, 0%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
40% {
clip-path: circle(
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
);
}
100% {
clip-path: circle(
var(--circle-size-end, 125%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
}
.in-circle-hesitate {
animation: var(--circle-duration, 2.5s)
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
both;
}
}
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
height: 100% !important; height: 100% !important;

View File

@ -4,6 +4,7 @@ import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
@ -122,6 +123,7 @@ export class KclManager {
get isExecuting() { get isExecuting() {
return this._isExecuting return this._isExecuting
} }
set isExecuting(isExecuting) { set isExecuting(isExecuting) {
this._isExecuting = isExecuting this._isExecuting = isExecuting
// If we have finished executing, but the execute is stale, we should // If we have finished executing, but the execute is stale, we should
@ -232,6 +234,12 @@ export class KclManager {
async executeAst(args: ExecuteArgs = {}): Promise<void> { async executeAst(args: ExecuteArgs = {}): Promise<void> {
if (this.isExecuting) { if (this.isExecuting) {
this.executeIsStale = args this.executeIsStale = args
// The previous execteAst will be rejected and cleaned up. The execution will be marked as stale.
// A new executeAst will start.
this.engineCommandManager.rejectAllModelingCommands(
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE
)
// Exit early if we are already executing. // Exit early if we are already executing.
return return
} }
@ -245,44 +253,38 @@ export class KclManager {
// Make sure we clear before starting again. End session will do this. // Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession() this.engineCommandManager?.endSession()
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory, isInterrupted } = await executeAst({
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
this.lints = await lintAst({ ast: ast }) // Program was not interrupted, setup the scene
// Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) {
this.lints = await lintAst({ ast: ast })
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(programMemory, this.engineCommandManager) defaultSelectionFilter(programMemory, this.engineCommandManager)
await this.engineCommandManager.waitForAllCommands()
if (args.zoomToFit) { if (args.zoomToFit) {
let zoomObjectId: string | undefined = '' let zoomObjectId: string | undefined = ''
if (args.zoomOnRangeAndType) { if (args.zoomOnRangeAndType) {
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId( zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
args.zoomOnRangeAndType.range, args.zoomOnRangeAndType.range,
args.zoomOnRangeAndType.type args.zoomOnRangeAndType.type
) )
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
},
})
} }
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects
},
})
} }
this.isExecuting = false this.isExecuting = false
@ -293,7 +295,8 @@ export class KclManager {
return return
} }
this.logs = logs this.logs = logs
this.addKclErrors(errors) // Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addKclErrors(isInterrupted ? [] : errors)
this.programMemory = programMemory this.programMemory = programMemory
this.ast = { ...ast } this.ast = { ...ast }
this._executeCallback() this._executeCallback()
@ -301,6 +304,7 @@ export class KclManager {
type: 'execution-done', type: 'execution-done',
data: null, data: null,
}) })
this._cancelTokens.delete(currentExecutionId) this._cancelTokens.delete(currentExecutionId)
} }
// NOTE: this always updates the code state and editor. // NOTE: this always updates the code state and editor.
@ -399,6 +403,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

@ -54,10 +54,12 @@ export async function executeAst({
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
isInterrupted?: boolean
}): Promise<{ }): Promise<{
logs: string[] logs: string[]
errors: KCLError[] errors: KCLError[]
programMemory: ProgramMemory programMemory: ProgramMemory
isInterrupted: boolean
}> { }> {
try { try {
if (!useFakeExecutor) { if (!useFakeExecutor) {
@ -73,13 +75,23 @@ export async function executeAst({
logs: [], logs: [],
errors: [], errors: [],
programMemory, programMemory,
isInterrupted: false,
} }
} catch (e: any) { } catch (e: any) {
let isInterrupted = false
if (e instanceof KCLError) { if (e instanceof KCLError) {
// Detect if it is a force interrupt error which is not a KCL processing error.
if (
e.msg ===
'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")'
) {
isInterrupted = true
}
return { return {
errors: [e], errors: [e],
logs: [], logs: [],
programMemory: ProgramMemory.empty(), programMemory: ProgramMemory.empty(),
isInterrupted,
} }
} else { } else {
console.log(e) console.log(e)
@ -87,6 +99,7 @@ export async function executeAst({
logs: [e], logs: [e],
errors: [], errors: [],
programMemory: ProgramMemory.empty(), programMemory: ProgramMemory.empty(),
isInterrupted,
} }
} }
} }

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
)
}, 5_000)
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,
@ -284,6 +469,9 @@ export const hasValidFilletSelection = ({
if (segmentNode.node.type === 'CallExpression') { if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) { if (segmentName in sketchLineHelperMap) {
// Add check whether the tag exists at all:
if (!(segmentNode.node.arguments.length === 3)) return true
// If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({ const edges = isTagUsedInFillet({
ast, ast,
callExp: segmentNode.node, callExp: segmentNode.node,

View File

@ -58,7 +58,10 @@ Map {
92, 92,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
@ -77,7 +80,10 @@ Map {
], ],
}, },
"edgeCutId": "UUID", "edgeCutId": "UUID",
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
@ -95,7 +101,10 @@ Map {
156, 156,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
@ -113,7 +122,10 @@ Map {
209, 209,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
@ -152,7 +164,16 @@ Map {
266, 266,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceIds": [ "surfaceIds": [
"UUID", "UUID",
@ -209,6 +230,54 @@ Map {
"type": "cap", "type": "cap",
}, },
"UUID-15" => { "UUID-15" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-16" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-17" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-18" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-19" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-20" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-21" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-22" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-23" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -226,7 +295,7 @@ Map {
"subType": "fillet", "subType": "fillet",
"type": "edgeCut", "type": "edgeCut",
}, },
"UUID-16" => { "UUID-24" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -250,7 +319,7 @@ Map {
"solid2dId": "UUID", "solid2dId": "UUID",
"type": "path", "type": "path",
}, },
"UUID-17" => { "UUID-25" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -263,12 +332,15 @@ Map {
416, 416,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
}, },
"UUID-18" => { "UUID-26" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -281,12 +353,15 @@ Map {
438, 438,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
}, },
"UUID-19" => { "UUID-27" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -299,12 +374,15 @@ Map {
491, 491,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceId": "UUID", "surfaceId": "UUID",
"type": "segment", "type": "segment",
}, },
"UUID-20" => { "UUID-28" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -321,11 +399,11 @@ Map {
"pathId": "UUID", "pathId": "UUID",
"type": "segment", "type": "segment",
}, },
"UUID-21" => { "UUID-29" => {
"pathId": "UUID", "pathId": "UUID",
"type": "solid2D", "type": "solid2D",
}, },
"UUID-22" => { "UUID-30" => {
"codeRef": { "codeRef": {
"pathToNode": [ "pathToNode": [
[ [
@ -338,7 +416,14 @@ Map {
546, 546,
], ],
}, },
"edgeIds": [], "edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"pathId": "UUID", "pathId": "UUID",
"surfaceIds": [ "surfaceIds": [
"UUID", "UUID",
@ -349,40 +434,76 @@ Map {
], ],
"type": "extrusion", "type": "extrusion",
}, },
"UUID-23" => { "UUID-31" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID", "extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-24" => { "UUID-32" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID", "extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-25" => { "UUID-33" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID", "extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-26" => { "UUID-34" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID", "extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "start", "subType": "start",
"type": "cap", "type": "cap",
}, },
"UUID-27" => { "UUID-35" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID", "extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "end", "subType": "end",
"type": "cap", "type": "cap",
}, },
"UUID-36" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-37" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-38" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-39" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
"UUID-40" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "opposite",
"type": "extrudeEdge",
},
"UUID-41" => {
"extrusionId": "UUID",
"segId": "UUID",
"subType": "adjacent",
"type": "extrudeEdge",
},
} }
`; `;

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