Compare commits

...

43 Commits

Author SHA1 Message Date
31b0a8af12 Cut release v0.25.0 (#3750)
* Cut release v0.25.0

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

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

---------

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

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

* fix

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

* updates

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

* updates

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

---------

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

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

* add offsetPlane as well

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

* fix offset

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

* change to 2

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

* updates

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

* updates

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

* fixes

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

* fixes

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

* fixes

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

* add-docs

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

* docs

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

---------

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

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

* updates

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 13:12:16 -07:00
84fae12cdd Bump react-hotkeys-hook from 4.5.0 to 4.5.1 (#3759)
Bumps [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.5.0...v4.5.1)

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

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

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

* info.plist conversion

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-04 12:03:44 -07:00
114c3a2580 Bump eslint-plugin-import from 2.29.1 to 2.30.0 (#3761)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.29.1 to 2.30.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...v2.30.0)

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

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

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

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

* Use ArtifactId type in more places

---------

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

* Fix default project directory value initialization

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

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

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

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

* trigger CI

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

* Update silly little export file size expectation numbers

* Make rename timeout in test way shorter

* Fix silly little test issues

---------

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

* add small makefile util

* Remove waiting for building scene prompt

* Add a new model state indicator

* fmt lint tsc

---------

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

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

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

* Fix up focusrite example after adjacent edge switch

* Yay now the export file is yet another new size

* Update bracket example code and some test colors that broke

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

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

* bump electron playwright timeout

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

* fix double export test

* unused var

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

* Re-run CI

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

* bump another pix diff to 15

* make retries work

* update expect numbers

* remove some logs

---------

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

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

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

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

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-03 16:17:22 -07:00
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
125 changed files with 16423 additions and 3867 deletions

View File

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

View File

@ -139,7 +139,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
@ -174,14 +174,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run playwright/chrome flow (with retries)
id: retry
@ -244,14 +244,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
@ -262,7 +262,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 30
timeout-minutes: 40
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
@ -351,7 +351,7 @@ jobs:
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-${{ matrix.os }}-${{ github.sha }}
path: test-results/
- name: Run electron tests (with retries)
id: retry
@ -381,7 +381,7 @@ jobs:
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi

1
.gitignore vendored
View File

@ -59,6 +59,7 @@ src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile
*.tsbuildinfo
src/wasm-lib/pkg
venv
.vite/

View File

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

View File

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

View File

@ -56,6 +56,7 @@ layout: manual
* [`line`](kcl/line)
* [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log)
* [`log10`](kcl/log10)
* [`log2`](kcl/log2)
@ -63,6 +64,7 @@ layout: manual
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d)
@ -88,6 +90,7 @@ layout: manual
* [`tan`](kcl/tan)
* [`tangentialArc`](kcl/tangentialArc)
* [`tangentialArcTo`](kcl/tangentialArcTo)
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
* [`tau`](kcl/tau)
* [`toDegrees`](kcl/toDegrees)
* [`toRadians`](kcl/toRadians)

477
docs/kcl/loft.md Normal file

File diff suppressed because one or more lines are too long

138
docs/kcl/offsetPlane.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because one or more lines are too long

View File

@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
}
// 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)
if (openPanes.includes('code')) {
await expect
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
.toBeLessThan(3)
await page.waitForTimeout(100)
await expect
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249]))
.poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
.toBeLessThan(3)
await page.waitForTimeout(100)
}
// click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100)
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)
}
// hold down shift
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
// click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
await page.waitForTimeout(100)
// selected two lines therefore there should be two cursors
if (openPanes.includes('code')) {
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(100)
}
await page.getByRole('button', { name: 'Length: open menu' }).click()

View File

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

View File

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

View File

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

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

View File

@ -358,6 +358,7 @@ const sketch001 = startSketchAt([-0, -0])
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{
code: bracket,
@ -393,20 +394,22 @@ const sketch001 = startSketchAt([-0, -0])
await test.step('The second export is blocked', async () => {
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).toBeVisible()
await page.waitForTimeout(1000)
await Promise.all([
expect(exportingToastMessage.first()).toBeVisible(),
expect(alreadyExportingToastMessage).toBeVisible(),
])
})
await test.step('The first export still succeeds', async () => {
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(successToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000,
}),
])
})
})
@ -419,10 +422,12 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
await Promise.all([
expect(exportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible()
})

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: 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: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -511,10 +511,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector)
const editorText = await editor.textContent()
return expect(util.toNormalizedCode(editorText || '')).toBe(
util.toNormalizedCode(code)
)
return expect(editor).toHaveText(code, { useInnerText: true })
},
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) => {
return test?.step(`Create a file named ${name}, select it`, async () => {
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
await page
.getByTestId('file-pane-scroll-container')
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.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[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
@ -772,7 +813,6 @@ export async function setup(context: BrowserContext, page: Page) {
localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings)
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
console.log('TEST_SETTINGS.projects', settings)
},
{
token: secrets.token,
@ -812,10 +852,12 @@ export async function setupElectron({
testInfo,
folderSetupFn,
cleanProjectDir = true,
appSettings,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -849,7 +891,10 @@ export async function setupElectron({
if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify({
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
: {
...TEST_SETTINGS,
settings: {
app: {
@ -857,7 +902,8 @@ export async function setupElectron({
projectDirectory: projectDirName,
},
},
})
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}

View File

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

View File

@ -303,6 +303,61 @@ test.describe('Testing settings', () => {
}
)
test(
`Load desktop app with no settings file`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
// This is what makes no settings file get created
cleanProjectDir: false,
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Load desktop app with a settings file, but no project directory setting`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
themeColor: '259',
},
},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Selectors and constants
const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred',
})
const projectDirLink = page.getByText('Loaded from')
// If the app loads without exploding we're in the clear
await expect(errorHeading).not.toBeVisible()
await expect(projectDirLink).toBeVisible()
await electronApp.close()
}
)
test(
`Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' },

View File

@ -1,11 +1,5 @@
import { test, expect, Page } from '@playwright/test'
import {
getUtils,
setup,
tearDown,
setupElectron,
createProjectAndRenameIt,
} from './test-utils'
import { getUtils, setup, tearDown, setupElectron } from './test-utils'
import { join } from 'path'
import fs from 'fs'
@ -698,13 +692,16 @@ test(
async ({ browserName }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () =>
fs.existsSync(join(dir, 'test-000', 'lego-2x4.kcl'))
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 createProjectAndRenameIt({ name: 'test-000', page })
await page.getByTestId('project-link').click()
await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
@ -713,10 +710,6 @@ test(
timeout: 20_000,
})
// Open the files pane
const filesPaneButton = page.getByTestId('files-pane-button')
await filesPaneButton.click()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4')
// File is considered created if it shows up in the Project Files pane

83
electron-builder.yml Normal file
View File

@ -0,0 +1,83 @@
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
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
rank: Owner
win:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
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"
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
msi:
oneClick: false
perMachine: true
nsis:
oneClick: false
perMachine: true
allowElevation: true
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
linux:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:
- target: appImage
arch:
- x64
- arm64
fileAssociations:
- ext: kcl
name: kcl
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder
channel: latest

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.24.12",
"version": "0.25.0",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -39,6 +39,7 @@
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.0",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",
@ -50,7 +51,7 @@
"react": "^18.3.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"react-hotkeys-hook": "^4.5.1",
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
@ -97,7 +98,9 @@
"tron:package": "electron-forge package",
"tron:make": "electron-forge make",
"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": {
"trailingComma": "es5",
@ -134,6 +137,7 @@
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.46.1",
"@tauri-apps/cli": "^2.0.0-rc.9",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
@ -150,7 +154,6 @@
"@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0",
@ -161,10 +164,12 @@
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "^32.0.1",
"electron-builder": "^24.13.3",
"electron-notarize": "^1.2.2",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",
@ -172,7 +177,7 @@
"node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",
"postcss": "^8.4.43",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8",
"setimmediate": "^1.0.5",
@ -185,7 +190,6 @@
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"wasm-pack": "^0.13.0",
"ws": "^8.17.0",
"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 +
(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 }}
enableMenu={true}
/>

View File

@ -42,7 +42,13 @@ export type ActionButtonProps =
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 ${
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 : ''}`
switch (props.Element) {

View File

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

View File

@ -4,21 +4,11 @@
*/
.header {
grid-template-columns: 1fr auto 1fr;
-webkit-app-region: drag; /* Make the header of the app draggable */
}
.header button {
-webkit-app-region: no-drag; /* Make the button not draggable */
}
.header a {
-webkit-app-region: no-drag; /* Make the link not draggable */
}
.header textarea {
-webkit-app-region: no-drag; /* Make the textarea not draggable */
}
.header input {
-webkit-app-region: no-drag; /* Make the input not draggable */
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'>
className?: string
enableMenu?: boolean
style?: React.CSSProperties
}
export const AppHeader = ({
@ -19,6 +20,7 @@ export const AppHeader = ({
project,
children,
className = '',
style,
enableMenu = false,
}: AppHeaderProps) => {
const { auth } = useSettingsAuthContext()
@ -33,6 +35,7 @@ export const AppHeader = ({
' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
className
}
style={style}
>
<ProjectSidebarMenu
enableMenu={enableMenu}

View File

@ -135,16 +135,15 @@ interface ContextMenuItemProps {
icon?: ActionIconProps['icon']
onClick?: () => void
hotkey?: string
'data-testid'?: string
}
export function ContextMenuItem({
children,
icon,
onClick,
hotkey,
}: ContextMenuItemProps) {
export function ContextMenuItem(props: ContextMenuItemProps) {
const { children, icon, onClick, hotkey } = props
return (
<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"
onClick={onClick}
>

View File

@ -332,7 +332,7 @@ const CustomIconMap = {
fillRule="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"
fill="black"
fill="currentColor"
/>
</svg>
),

View File

@ -2,7 +2,7 @@ import { CommandLog } from 'lang/std/engineConnection'
import { engineCommandManager } from 'lib/singletons'
import { useState, useEffect } from 'react'
function useEngineCommands(): [CommandLog[], () => void] {
export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
engineCommandManager.commandLogs
)

View File

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

View File

@ -179,10 +179,7 @@ const FileTreeItem = ({
codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
kclManager.executeCode(true)
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)
@ -358,10 +355,18 @@ function FileTreeContextMenu({
<ContextMenu
menuTargetElement={itemRef}
items={[
<ContextMenuItem onClick={onRename} hotkey="Enter">
<ContextMenuItem
data-testid="context-menu-rename"
onClick={onRename}
hotkey="Enter"
>
Rename
</ContextMenuItem>,
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
<ContextMenuItem
data-testid="context-menu-delete"
onClick={onDelete}
hotkey={metaKey + ' + Del'}
>
Delete
</ContextMenuItem>,
]}

View File

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

View File

@ -11,6 +11,7 @@ import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator'
export function LowerRightControls({
children,
@ -65,6 +66,7 @@ export function LowerRightControls({
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`

View File

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

View File

@ -66,7 +66,6 @@ import {
hasExtrudableGeometry,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
@ -161,9 +160,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
kclManager.isFirstRender = true
kclManager.executeCode().then(() => {
kclManager.isFirstRender = false
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
@ -363,7 +360,7 @@ export const ModelingMachineProvider = ({
return {}
}),
Make: async (_, event) => {
if (event.type !== 'Make' || TEST) return
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
@ -407,7 +404,7 @@ export const ModelingMachineProvider = ({
)
},
'Engine export': async (_, event) => {
if (event.type !== 'Export' || TEST) return
if (event.type !== 'Export') return
if (engineCommandManager.exportIntent) {
toast.error('Already exporting')
return

View File

@ -49,7 +49,11 @@ export const NetworkMachineIndicator = ({
{Object.entries(machineManager.machines).map(
([hostname, machine]) => (
<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">
Hostname {hostname}
</p>

View File

@ -193,10 +193,7 @@ export const SettingsAuthProviderBase = ({
resetSettingsIncludesUnitChange
) {
// Unit changes requires a re-exec of code
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
kclManager.executeCode(true)
} else {
// For any future logging we'd like to do
// console.log(

View File

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

View File

@ -54,12 +54,10 @@ export const Stream = () => {
* central place, we can move this code there.
*/
async function executeCodeAndPlayStream() {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
kclManager.isFirstRender = false
setStreamState(StreamState.Playing)
})
}
@ -219,7 +217,7 @@ export const Stream = () => {
* Play the vid
*/
useEffect(() => {
if (!kclManager.isFirstRender) {
if (!kclManager.isExecuting) {
setTimeout(() =>
// execute in the next event loop
videoRef.current?.play().catch((e) => {
@ -227,7 +225,7 @@ export const Stream = () => {
})
)
}
}, [kclManager.isFirstRender])
}, [kclManager.isExecuting])
useEffect(() => {
if (
@ -382,15 +380,15 @@ export const Stream = () => {
</div>
</div>
)}
{(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
{(!isNetworkOkay || isLoading) && (
<div className="text-center absolute inset-0">
<Loading>
{!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
{!isNetworkOkay && !isLoading ? (
<span data-testid="loading-stream">Stream disconnected...</span>
) : !isLoading && kclManager.isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span>
) : (
!isLoading && (
<span data-testid="loading-stream">Loading stream...</span>
)
)}
</Loading>
</div>

View File

@ -217,7 +217,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
</p>
{displayedName !== user.email && (
<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"
>
{user.email}

View File

@ -9,6 +9,7 @@ import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections'
import {
getCapCodeRef,
getExtrudeEdgeCodeRef,
getExtrusionFromSuspectedExtrudeSurface,
getSolid2dCodeRef,
getWallCodeRef,
@ -60,6 +61,13 @@ export function useEngineConnectionSubscriptions() {
? [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') {
editorManager.setHighlightRange([
artifact?.codeRef?.range || [0, 0],

View File

@ -4,6 +4,18 @@
@tailwind components;
@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 {
margin: 0;
@apply font-sans;
@ -97,7 +109,7 @@ button:disabled {
}
a {
@apply text-primary underline hover:hue-rotate-15;
@apply text-primary hover:hue-rotate-15;
}
.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-editor {
height: 100% !important;

View File

@ -4,6 +4,7 @@ import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
import { err } from 'lib/trap'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import {
CallExpression,
@ -59,8 +60,6 @@ export class KclManager {
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
private _executeCallback: () => void = () => {}
isFirstRender = true
get ast() {
return this._ast
}
@ -122,6 +121,7 @@ export class KclManager {
get isExecuting() {
return this._isExecuting
}
set isExecuting(isExecuting) {
this._isExecuting = isExecuting
// If we have finished executing, but the execute is stale, we should
@ -232,6 +232,12 @@ export class KclManager {
async executeAst(args: ExecuteArgs = {}): Promise<void> {
if (this.isExecuting) {
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.
return
}
@ -245,16 +251,18 @@ export class KclManager {
// Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession()
await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({
const { logs, errors, programMemory, isInterrupted } = await executeAst({
ast,
engineCommandManager: this.engineCommandManager,
})
// 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' })
defaultSelectionFilter(programMemory, this.engineCommandManager)
await this.engineCommandManager.waitForAllCommands()
if (args.zoomToFit) {
let zoomObjectId: string | undefined = ''
@ -274,15 +282,7 @@ export class KclManager {
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
@ -293,7 +293,8 @@ export class KclManager {
return
}
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.ast = { ...ast }
this._executeCallback()
@ -301,6 +302,7 @@ export class KclManager {
type: 'execution-done',
data: null,
})
this._cancelTokens.delete(currentExecutionId)
}
// NOTE: this always updates the code state and editor.

View File

@ -54,10 +54,12 @@ export async function executeAst({
engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory
isInterrupted?: boolean
}): Promise<{
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
isInterrupted: boolean
}> {
try {
if (!useFakeExecutor) {
@ -73,13 +75,23 @@ export async function executeAst({
logs: [],
errors: [],
programMemory,
isInterrupted: false,
}
} catch (e: any) {
let isInterrupted = false
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 {
errors: [e],
logs: [],
programMemory: ProgramMemory.empty(),
isInterrupted,
}
} else {
console.log(e)
@ -87,6 +99,7 @@ export async function executeAst({
logs: [e],
errors: [],
programMemory: ProgramMemory.empty(),
isInterrupted,
}
}
}

View File

@ -152,7 +152,7 @@ const extrude001 = extrude(-15, sketch001)`
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], %)

View File

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

View File

@ -247,7 +247,7 @@ describe('testing createArtifactGraph', () => {
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 1400, 1400, 'exampleCode1.png')
await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png')
}, 20000)
})
})
@ -271,7 +271,7 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 2500, 2500, 'sketchOnFaceOnFaceEtc.png')
await GraphTheGraph(theMap, 3000, 3000, 'sketchOnFaceOnFaceEtc.png')
}, 20000)
})
})
@ -603,7 +603,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [98, 125],
pathToNode: [['body', '']],
@ -623,7 +623,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [162, 209],
pathToNode: [['body', '']],
@ -633,7 +633,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],
@ -650,7 +650,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [131, 156],
pathToNode: [['body', '']],
@ -660,7 +660,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],
@ -677,7 +677,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [98, 125],
pathToNode: [['body', '']],
@ -688,7 +688,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],
@ -705,7 +705,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'segment',
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [76, 92],
pathToNode: [['body', '']],
@ -715,7 +715,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],
@ -732,7 +732,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],
@ -749,7 +749,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'extrusion',
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: [],
edgeIds: expect.any(Array),
codeRef: {
range: [243, 266],
pathToNode: [['body', '']],

View File

@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
export type ArtifactId = string
interface CommonCommandProperties {
range: SourceRange
pathToNode: PathToNode
@ -10,7 +12,7 @@ interface CommonCommandProperties {
export interface PlaneArtifact {
type: 'plane'
pathIds: Array<string>
pathIds: Array<ArtifactId>
codeRef: CommonCommandProperties
}
export interface PlaneArtifactRich {
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
export interface PathArtifact {
type: 'path'
planeId: string
segIds: Array<string>
extrusionId: string
solid2dId?: string
planeId: ArtifactId
segIds: Array<ArtifactId>
extrusionId: ArtifactId
solid2dId?: ArtifactId
codeRef: CommonCommandProperties
}
interface solid2D {
type: 'solid2D'
pathId: string
pathId: ArtifactId
}
export interface PathArtifactRich {
type: 'path'
@ -42,10 +44,10 @@ export interface PathArtifactRich {
interface SegmentArtifact {
type: 'segment'
pathId: string
surfaceId: string
edgeIds: Array<string>
edgeCutId?: string
pathId: ArtifactId
surfaceId: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CommonCommandProperties
}
interface SegmentArtifactRich {
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
interface ExtrusionArtifact {
type: 'extrusion'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
pathId: ArtifactId
surfaceIds: Array<ArtifactId>
edgeIds: Array<ArtifactId>
codeRef: CommonCommandProperties
}
interface ExtrusionArtifactRich {
@ -74,40 +76,40 @@ interface ExtrusionArtifactRich {
interface WallArtifact {
type: 'wall'
segId: string
edgeCutEdgeIds: Array<string>
extrusionId: string
pathIds: Array<string>
segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId
pathIds: Array<ArtifactId>
}
interface CapArtifact {
type: 'cap'
subType: 'start' | 'end'
edgeCutEdgeIds: Array<string>
extrusionId: string
pathIds: Array<string>
edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId
pathIds: Array<ArtifactId>
}
interface ExtrudeEdge {
type: 'extrudeEdge'
segId: string
extrusionId: string
edgeId: string
segId: ArtifactId
extrusionId: ArtifactId
subType: 'opposite' | 'adjacent'
}
/** A edgeCut is a more generic term for both fillet or chamfer */
interface EdgeCut {
type: 'edgeCut'
subType: 'fillet' | 'chamfer'
consumedEdgeId: string
edgeIds: Array<string>
surfaceId: string
consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId>
surfaceId: ArtifactId
codeRef: CommonCommandProperties
}
interface EdgeCutEdge {
type: 'edgeCutEdge'
edgeCutId: string
surfaceId: string
edgeCutId: ArtifactId
surfaceId: ArtifactId
}
export type Artifact =
@ -122,7 +124,7 @@ export type Artifact =
| EdgeCutEdge
| solid2D
export type ArtifactGraph = Map<string, Artifact>
export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type']
@ -149,7 +151,7 @@ export function createArtifactGraph({
responseMap: ResponseMap
ast: Program
}) {
const myMap = new Map<string, Artifact>()
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
@ -166,7 +168,7 @@ export function createArtifactGraph({
const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand,
responseMap,
getArtifact: (id: string) => myMap.get(id),
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
})
@ -224,11 +226,11 @@ export function getArtifactsToUpdate({
orderedCommand: OrderedCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: string) => Artifact | undefined
currentPlaneId: string
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Program
}): Array<{
id: string
id: ArtifactId
artifact: Artifact
}> {
const pathToNode = getNodePathFromSourceRange(ast, range)
@ -422,6 +424,56 @@ export function getArtifactsToUpdate({
}
})
return returnArr
} else if (
// is opposite edge
(cmd.type === 'solid3d_get_opposite_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
response.data.modeling_response.data.edge) ||
// or is adjacent edge
(cmd.type === 'solid3d_get_prev_adjacent_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type ===
'solid3d_get_prev_adjacent_edge' &&
response.data.modeling_response.data.edge)
) {
const wall = getArtifact(cmd.face_id)
if (wall?.type !== 'wall') return returnArr
const extrusion = getArtifact(wall.extrusionId)
if (extrusion?.type !== 'extrusion') return returnArr
const path = getArtifact(extrusion.pathId)
if (path?.type !== 'path') return returnArr
const segment = getArtifact(cmd.edge_id)
if (segment?.type !== 'segment') return returnArr
return [
{
id: response.data.modeling_response.data.edge,
artifact: {
type: 'extrudeEdge',
subType:
cmd.type === 'solid3d_get_prev_adjacent_edge'
? 'adjacent'
: 'opposite',
segId: cmd.edge_id,
extrusionId: path.extrusionId,
},
},
{
id: cmd.edge_id,
artifact: {
...segment,
edgeIds: [response.data.modeling_response.data.edge],
},
},
{
id: path.extrusionId,
artifact: {
...extrusion,
edgeIds: [response.data.modeling_response.data.edge],
},
},
]
} else if (cmd.type === 'solid3d_fillet_edge') {
returnArr.push({
id,
@ -464,7 +516,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
@ -478,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
},
map: ArtifactGraph
): Map<string, Extract<Artifact, { type: T[number] }>> {
): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
return new Map(
[...map].filter(
([key, value]) =>
@ -487,7 +539,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactOfTypes<T extends Artifact['type'][]>(
@ -495,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
key,
types,
}: {
key: string
key: ArtifactId
types: T
},
map: ArtifactGraph
@ -655,8 +707,20 @@ export function getWallCodeRef(
return seg.codeRef
}
export function getExtrudeEdgeCodeRef(
edge: ExtrudeEdge,
artifactGraph: ArtifactGraph
): CommonCommandProperties | Error {
const seg = getArtifactOfTypes(
{ key: edge.segId, types: ['segment'] },
artifactGraph
)
if (err(seg)) return seg
return seg.codeRef
}
export function getExtrusionFromSuspectedExtrudeSurface(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const artifact = getArtifactOfTypes(
@ -671,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
}
export function getExtrusionFromSuspectedPath(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 617 KiB

View File

@ -16,6 +16,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -1250,6 +1252,10 @@ export type CommandLog =
type: 'execution-done'
data: null
}
| {
type: 'export-done'
data: null
}
export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run
@ -1279,6 +1285,7 @@ interface PendingMessage {
resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]>
isSceneCommand: boolean
}
export class EngineCommandManager extends EventTarget {
/**
@ -1379,6 +1386,7 @@ export class EngineCommandManager extends EventTarget {
}: CustomEvent<NewTrackArgs>) => {}
modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any
kclManager: null | KclManager = null
set exportIntent(intent: ExportIntent | null) {
this._exportIntent = intent
@ -1914,7 +1922,13 @@ export class EngineCommandManager extends EventTarget {
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
this.pendingExport = {
resolve,
resolve: (passThrough) => {
this.addCommandLog({
type: 'export-done',
data: null,
})
resolve(passThrough)
},
reject: (reason: string) => {
this.exportIntent = null
reject(reason)
@ -1932,11 +1946,21 @@ export class EngineCommandManager extends EventTarget {
;(cmd as any).sequence = this.outSequence++
}
// since it's not mouse drag or highlighting send over TCP and keep track of the command
return this.sendCommand(command.cmd_id, {
return this.sendCommand(
command.cmd_id,
{
command,
idToRangeMap: {},
range: [0, 0],
}).then(([a]) => a)
},
true // isSceneCommand
)
.then(([a]) => a)
.catch((e) => {
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
/*noop*/
return null
})
}
/**
* A wrapper around the sendCommand where all inputs are JSON strings
@ -1963,6 +1987,12 @@ export class EngineCommandManager extends EventTarget {
const idToRangeMap: { [key: string]: SourceRange } =
JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger
// Used in conjunction with rejectAllModelingCommands
if (this?.kclManager?.executeIsStale) {
return Promise.reject(EXECUTE_AST_INTERRUPT_ERROR_MESSAGE)
}
const resp = await this.sendCommand(id, {
command,
range,
@ -1980,7 +2010,8 @@ export class EngineCommandManager extends EventTarget {
command: PendingMessage['command']
range: PendingMessage['range']
idToRangeMap: PendingMessage['idToRangeMap']
}
},
isSceneCommand = false
): Promise<[Models['WebSocketResponse_type']]> {
const { promise, resolve, reject } = promiseFactory<any>()
this.pendingCommands[id] = {
@ -1990,7 +2021,9 @@ export class EngineCommandManager extends EventTarget {
command: message.command,
range: message.range,
idToRangeMap: message.idToRangeMap,
isSceneCommand,
}
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
@ -2037,6 +2070,19 @@ export class EngineCommandManager extends EventTarget {
this.deferredArtifactPopulated(null)
}
}
/**
* Reject all of the modeling pendingCommands created from sendModelingCommandFromWasm
* This interrupts the runtime of executeAst. Stops the AST processing and stops sending commands
* to the engine
*/
rejectAllModelingCommands(rejectionMessage: string) {
Object.values(this.pendingCommands).forEach(
({ reject, isSceneCommand }) =>
!isSceneCommand && reject(rejectionMessage)
)
}
async initPlanes() {
if (this.planesInitialized()) return
const planes = await this.makeDefaultPlanes()

View File

@ -95,8 +95,6 @@ export const wasmUrl = () => {
document.location.pathname.split('/').slice(0, -1).join('/') +
'/wasm_lib_bg.wasm'
console.log(`Full URL for WASM: ${fullUrl}`)
return fullUrl
}

View File

@ -25,7 +25,7 @@ export type ModelingCommandSchema = {
storage?: StorageUnion
}
Make: {
machine: components['schemas']['Machine']
machine: components['schemas']['MachineInfoResponse']
}
Extrude: {
selection: Selections // & { type: 'face' } would be cool to lock that down
@ -179,21 +179,25 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
machine: {
inputType: 'options',
required: true,
valueSummary: (machine: components['schemas']['Machine']) =>
machine.model || machine.manufacturer,
valueSummary: (machine: components['schemas']['MachineInfoResponse']) =>
machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine',
options: () => {
return Object.entries(machineManager.machines).map(
([hostname, machine]) => ({
name: `${machine.model || machine.manufacturer}, ${hostname}`,
([_, machine]) => ({
name: `${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) via ${machineManager.machineApiIp || 'the local network'}`,
isCurrent: false,
value: machine as components['schemas']['Machine'],
value: machine as components['schemas']['MachineInfoResponse'],
})
)
},
defaultValue: () => {
return Object.values(
machineManager.machines
)[0] as components['schemas']['Machine']
)[0] as components['schemas']['MachineInfoResponse']
},
},
},

View File

@ -8,6 +8,7 @@ export const MAX_PADDING = 7
* This is available for users to edit as a setting.
*/
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export const DEFAULT_PROJECT_KCL_FILE = 'main.kcl'
/** Name given the temporary "project" in the browser version of the app */
export const BROWSER_PROJECT_NAME = 'browser'
/** Name given the temporary file in the browser version of the app */
@ -66,3 +67,8 @@ export const COOKIE_NAME = '__Secure-next-auth.session-token'
/** localStorage key to determine if we're in Playwright tests */
export const PLAYWRIGHT_KEY = 'playwright'
/** Custom error message to match when rejectAllModelCommands is called
* allows us to match if the execution of executeAst was interrupted */
export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE =
'Force interrupt, executionIsStale, new AST requested'

View File

@ -462,29 +462,60 @@ export const readProjectSettingsFile = async (
*/
export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath()
const initialProjectDirConfig: DeepPartial<
Configuration['settings']['project']
> = { directory: await getInitialDefaultDir() }
// The file exists, read it and parse it.
if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
const parsedAppConfig = parseAppSettings(configToml)
if (err(parsedAppConfig)) {
return Promise.reject(parsedAppConfig)
}
return configObj
const hasProjectDirectorySetting =
parsedAppConfig.settings?.project?.directory ||
parsedAppConfig.settings?.app?.project_directory
if (hasProjectDirectorySetting) {
return parsedAppConfig
} else {
// inject the default project directory setting
const mergedConfig: DeepPartial<Configuration> = {
...parsedAppConfig,
settings: {
...parsedAppConfig.settings,
project: Object.assign(
{},
parsedAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
return mergedConfig
}
}
// The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig)
}
const initialDirConfig: DeepPartial<Configuration> = {
settings: { project: { directory: await getInitialDefaultDir() } },
// inject the default project directory setting
const mergedDefaultConfig: DeepPartial<Configuration> = {
...defaultAppConfig,
settings: {
...defaultAppConfig.settings,
project: Object.assign(
{},
defaultAppConfig.settings?.project,
initialProjectDirConfig
),
},
}
const config = Object.assign(defaultAppConfig, initialDirConfig)
return config
return mergedDefaultConfig
}
export const writeAppSettingsFile = async (tomlStr: string) => {

View File

@ -31,11 +31,11 @@ const bracket = startSketchOn('XY')
|> extrude(width, %)
|> fillet({
radius: filletR,
tags: [getPreviousAdjacentEdge(innerEdge)]
tags: [getNextAdjacentEdge(innerEdge)]
}, %)
|> fillet({
radius: filletR + thickness,
tags: [getPreviousAdjacentEdge(outerEdge)]
tags: [getNextAdjacentEdge(outerEdge)]
}, %)`
/**

View File

@ -26,15 +26,7 @@ export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
return null
}
let machineId = null
if ('id' in currentMachine) {
machineId = currentMachine.id
} else if ('hostname' in currentMachine && currentMachine.hostname) {
machineId = currentMachine.hostname
} else if ('ip' in currentMachine && currentMachine.ip) {
machineId = currentMachine.ip
}
let machineId = currentMachine?.id
if (!machineId) {
console.error('No machine id available', currentMachine)
toast.error('No machine id available')

View File

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

View File

@ -93,587 +93,56 @@ export interface paths {
export type webhooks = Record<string, never>
export interface components {
schemas: {
/** @description The type of accessory. */
AccessoryType: 'none'
/** @description Error information from a response. */
Error: {
error_code?: string
message: string
request_id: string
}
/** @description An info command. */
Info: {
/** @enum {string} */
command: 'get_version'
/** @description The info module. */
module: components['schemas']['InfoModule'][]
/** @description The reason of the info command. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the info command. */
result?: components['schemas']['Result'] | null
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
}
/** @description An info module. */
InfoModule: {
/** @description The hardware version. */
hw_ver: string
/** @description The loader version. */
loader_ver?: string | null
/** @description The module name. */
name: string
/** @description The ota version. */
ota_ver?: string | null
/** @description The project name. */
project_name?: string | null
/** @description The serial number. */
sn: string
/** @description The software version. */
sw_ver: string
}
/** @description The mode for the led. */
LedMode: 'on' | 'off' | 'flashing'
/** @description The node for the led. */
LedNode: 'chamber_light' | 'work_light'
/** @description A liveview message. */
LiveView: {
/** @enum {string} */
command: 'init'
/** @description The op protocols. */
op_protocols: components['schemas']['OperationProtocol'][]
/** @description The peer host. */
peer_host: string
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
}
/** @description Details for a 3d printer connected over USB. */
Machine:
/** @description Extra machine-specific information regarding a connected machine. */
ExtraMachineInfoResponse:
| {
Moonraker: Record<string, never>
}
| {
Usb: Record<string, never>
}
| {
Bambu: Record<string, never>
}
/** @description Information regarding a connected machine. */
MachineInfoResponse: {
/** @description Additional, per-machine information which is specific to the underlying machine type. */
extra?: components['schemas']['ExtraMachineInfoResponse'] | null
/** @description Machine Identifier (ID) for the specific Machine. */
id: string
manufacturer: string
model: string
port: string
/** @enum {string} */
type: 'UsbPrinter'
/** @description Information regarding the method of manufacture. */
machine_type: components['schemas']['MachineType']
/** @description Information regarding the make and model of the attached Machine. */
make_model: components['schemas']['MachineMakeModel']
/** @description Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.
*
* This may be `None` if the maximum size is not knowable by the Machine API.
*
* What "close" means is up to you! */
max_part_volume?: components['schemas']['Volume'] | null
}
| {
/** @description The hostname of the printer. */
hostname?: string | null
/**
* Format: ip
* @description The IP address of the printer.
*/
ip: string
/** @description The manufacturer of the printer. */
manufacturer: components['schemas']['NetworkPrinterManufacturer']
/** @description The model of the printer. */
/** @description Information regarding the make/model of a discovered endpoint. */
MachineMakeModel: {
/** @description The manufacturer that built the connected Machine. */
manufacturer?: string | null
/** @description The model of the connected Machine. */
model?: string | null
/**
* Format: uint16
* @description The port of the printer.
*/
port?: number | null
/** @description The serial number of the printer. */
/** @description The unique serial number of the connected Machine. */
serial?: string | null
/** @enum {string} */
type: 'NetworkPrinter'
}
/** @description A message from a machine. */
Message:
| {
UsbPrinter: components['schemas']['Message2']
}
| {
NetworkPrinter: components['schemas']['Message3']
}
/**
* @description A message from the printer.
* @enum {string}
*/
Message2: 'ok'
/** @description A message from the printer. */
Message3:
| {
Bambu: components['schemas']['Message4']
}
| {
Formlabs: Record<string, never>
}
/** @description A message from/to the printer. */
Message4:
| {
print: components['schemas']['Print']
}
| {
info: components['schemas']['Info']
}
| {
system: components['schemas']['System']
}
| {
security: components['schemas']['Security']
}
| {
live_view: components['schemas']['LiveView']
}
| {
json: unknown
}
| {
unknown: string | null
}
/** @description Network printer manufacturer. */
NetworkPrinterManufacturer: 'Bambu' | 'Formlabs'
/** @description A nozzle type. */
NozzleType: 'hardened_steel' | 'stainless_steel'
/** @description An operation protocol. */
OperationProtocol: {
/** @description The protocol. */
protocol: string
/** @description The version. */
version: string
} & {
[key: string]: unknown
}
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
/** @description The response from the `/ping` endpoint. */
Pong: {
/** @description The pong response. */
message: string
}
/** @description A print command. */
Print:
| ({
/** @enum {string} */
command: 'ams_control'
/** @description The param. */
param?: string | null
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'ams_change_filament'
/**
* Format: int64
* @description The error number.
*/
errorno?: number | null
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/**
* Format: int64
* @description The target temperature.
*/
tar_temp?: number | null
/**
* Format: int64
* @description The target.
*/
target: number
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'calibration'
/**
* Format: int64
* @description The option.
*/
option: number
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @description The ams. */
ams?: components['schemas']['PrintAms'] | null
/**
* Format: int64
* @description The ams rfid status.
*/
ams_rfid_status?: number | null
/**
* Format: int64
* @description The ams status.
*/
ams_status?: number | null
/** @description The aux part fan. */
aux_part_fan?: boolean | null
/**
* Format: double
* @description The target bed temperature.
*/
bed_target_temper?: number | null
/**
* Format: double
* @description The bed temperature.
*/
bed_temper?: number | null
/** @description The big fan 1 speed. */
big_fan1_speed?: string | null
/** @description The big fan 2 speed. */
big_fan2_speed?: string | null
/**
* Format: double
* @description The chamber temperature.
*/
chamber_temper?: number | null
/** @enum {string} */
command: 'push_status'
/** @description The cooling fan speed. */
cooling_fan_speed?: string | null
/**
* Format: int64
* @description The fan gear.
*/
fan_gear?: number | null
/** @description Force upgrade? */
force_upgrade?: boolean | null
/** @description The gcode file. */
gcode_file?: string | null
/** @description The gcode file prepare percent. */
gcode_file_prepare_percent?: string | null
/** @description The gcode state. */
gcode_state?: string | null
/** @description The heatbreak fan speed. */
heatbreak_fan_speed?: string | null
/** @description The hms. */
hms?: unknown[] | null
/**
* Format: int64
* @description The home flag.
*/
home_flag?: number | null
/**
* Format: int64
* @description The hw switch state.
*/
hw_switch_state?: number | null
/** @description The ipcam. */
ipcam?: components['schemas']['PrintIpcam'] | null
/**
* Format: int64
* @description The layer num.
*/
layer_num?: number | null
/** @description The lifecycle. */
lifecycle?: string | null
/** @description The lights report. */
lights_report?: components['schemas']['PrintLightsReport'][] | null
/**
* Format: int64
* @description The percentage of the print completed.
*/
mc_percent?: number | null
/** @description The mc print line number. */
mc_print_line_number?: string | null
/** @description The print stage. */
mc_print_stage?: string | null
/**
* Format: int64
* @description The mc print sub stage.
*/
mc_print_sub_stage?: number | null
/**
* Format: int64
* @description The remaining time of the print.
*/
mc_remaining_time?: number | null
/** @description The mess production state. */
mess_production_state?: string | null
/**
* Format: int64
* @description The message.
*/
msg?: number | null
/** @description The nozzle diameter. */
nozzle_diameter?: string | null
/**
* Format: double
* @description The target nozzle temperature.
*/
nozzle_target_temper?: number | null
/**
* Format: double
* @description The nozzle temperature.
*/
nozzle_temper?: number | null
/** @description The nozzle type. */
nozzle_type?: components['schemas']['NozzleType'] | null
/** @description Online status. */
online?: components['schemas']['PrintOnline'] | null
/**
* Format: int64
* @description The print error.
*/
print_error?: number | null
/** @description The print type. */
print_type?: string | null
/** @description The profile id. */
profile_id?: string | null
/** @description The project id. */
project_id?: string | null
/**
* Format: int64
* @description The queue est.
*/
queue_est?: number | null
/**
* Format: int64
* @description The queue number.
*/
queue_number?: number | null
/**
* Format: int64
* @description The queue sts.
*/
queue_sts?: number | null
/**
* Format: int64
* @description The queue total.
*/
queue_total?: number | null
/** @description The s obj. */
s_obj?: unknown[] | null
/** @description Sdcard? */
sdcard?: boolean | null
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/**
* Format: int64
* @description The spd lvl.
*/
spd_lvl?: number | null
/**
* Format: int64
* @description The spd mag.
*/
spd_mag?: number | null
/** @description The stg. */
stg?: unknown[] | null
/**
* Format: int64
* @description The stg cur.
*/
stg_cur?: number | null
/** @description The subtask id. */
subtask_id?: string | null
/** @description The subtask name. */
subtask_name?: string | null
/** @description The task id. */
task_id?: string | null
/**
* Format: int64
* @description The total layer num.
*/
total_layer_num?: number | null
/** @description The upgrade state. */
upgrade_state?: components['schemas']['PrintUpgradeState'] | null
/** @description The upload. */
upload?: components['schemas']['PrintUpload'] | null
/** @description The tray. */
vt_tray?: components['schemas']['PrintTray'] | null
/** @description The wifi signal. */
wifi_signal?: string | null
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'gcode_line'
/** @description The gcode line. */
param?: string | null
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The return code. */
return_code?: string | null
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/**
* Format: int64
* @description The source.
*/
source?: number | null
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'gcode_file'
/** @description The param. */
param?: string | null
/** @description The print type. */
print_type?: string | null
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'project_file'
/** @description The gcode file. */
gcode_file?: string | null
/** @description The profile id. */
profile_id: string
/** @description The project id. */
project_id: string
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/** @description The subtask id. */
subtask_id: string
/** @description The subtask name. */
subtask_name: string
/** @description The task id. */
task_id: string
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'pause'
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'print_speed'
/** @description The param. */
param: string
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'resume'
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'stop'
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'extrusion_cali_get'
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
/** @description The print ams. */
PrintAms: {
/** @description The ams. */
ams?: components['schemas']['PrintAmsData'][] | null
/** @description The ams exist bits. */
ams_exist_bits?: string | null
/** @description The insert flag. */
insert_flag?: boolean | null
/** @description The power on flag. */
power_on_flag?: boolean | null
/** @description The tray exist bits. */
tray_exist_bits?: string | null
/** @description The tray is bbl bits. */
tray_is_bbl_bits?: string | null
/** @description The tray now. */
tray_now?: string | null
/** @description The tray pre. */
tray_pre?: string | null
/** @description The tray read done bits. */
tray_read_done_bits?: string | null
/** @description The tray reading bits. */
tray_reading_bits?: string | null
/** @description The tray tar. */
tray_tar?: string | null
/**
* Format: int64
* @description The version.
*/
version?: number | null
} & {
[key: string]: unknown
}
/** @description The print ams data. */
PrintAmsData: {
/** @description The humidity. */
humidity: string
/** @description The id. */
id: string
/** @description The temperature. */
temp: string
/** @description The tray. */
tray: components['schemas']['PrintTray'][]
} & {
[key: string]: unknown
}
/** @description The print ipcam. */
PrintIpcam: {
/** @description The ipcam dev. */
ipcam_dev?: string | null
/** @description The ipcam record. */
ipcam_record?: string | null
/**
* Format: int64
* @description The mode bits.
*/
mode_bits?: number | null
/** @description The timelapse. */
timelapse?: string | null
} & {
[key: string]: unknown
}
/** @description The response from the `/print` endpoint. */
PrintJobResponse: {
/** @description The job id used for this print. */
@ -681,29 +150,6 @@ export interface components {
/** @description The parameters used for this print. */
parameters: components['schemas']['PrintParameters']
}
/** @description A print lights report. */
PrintLightsReport: {
/** @description The mode. */
mode: components['schemas']['LedMode']
/** @description The node. */
node: components['schemas']['LedNode']
} & {
[key: string]: unknown
}
/** @description The print online. */
PrintOnline: {
/** @description The ahb. */
ahb: boolean
/** @description The rfid. */
rfid?: boolean | null
/**
* Format: int64
* @description The version.
*/
version: number
} & {
[key: string]: unknown
}
/** @description Parameters for printing. */
PrintParameters: {
/** @description The name for the job. */
@ -711,219 +157,26 @@ export interface components {
/** @description The machine id to print to. */
machine_id: string
}
/** @description The print tray. */
PrintTray: {
/** @description The bed temperature. */
bed_temp?: string | null
/** @description The bed temperature type. */
bed_temp_type?: string | null
/** @description The id. */
id: string
/** @description Set of three values to represent the extent of a 3-D Volume. This contains the width, depth, and height values, generally used to represent some maximum or minimum.
*
* All measurements are in millimeters. */
Volume: {
/**
* Format: double
* @description The tray k.
* @description Depth of the volume ("front to back"), in millimeters.
*/
k?: number | null
/**
* Format: int64
* @description The tray n.
*/
n?: number | null
/** @description The nozzle temperature max. */
nozzle_temp_max?: string | null
/** @description The nozzle temperature min. */
nozzle_temp_min?: string | null
/**
* Format: int64
* @description The tray remain.
*/
remain?: number | null
/** @description The tag uid. */
tag_uid?: string | null
/** @description The tray color. */
tray_color?: string | null
/** @description The tray diameter. */
tray_diameter?: string | null
/** @description The tray id name. */
tray_id_name?: string | null
/** @description The tray info index. */
tray_info_idx?: string | null
/** @description The tray sub brands. */
tray_sub_brands?: string | null
/** @description The tray temperature. */
tray_temp?: string | null
/** @description The tray time. */
tray_time?: string | null
/** @description The tray type. */
tray_type?: string | null
/** @description The tray uuid. */
tray_uuid?: string | null
/** @description The tray weight. */
tray_weight?: string | null
/** @description The xcam info. */
xcam_info?: string | null
} & {
[key: string]: unknown
}
/** @description A print upgrade state. */
PrintUpgradeState: {
/** @description The consistency request. */
consistency_request?: boolean | null
/**
* Format: int64
* @description The dis state.
*/
dis_state?: number | null
/**
* Format: int64
* @description The error code.
*/
err_code?: number | null
/** @description Force upgrade? */
force_upgrade?: boolean | null
/** @description The message. */
message?: string | null
/** @description The module. */
module?: string | null
/** @description The new version list. */
new_ver_list?: unknown[] | null
/**
* Format: int64
* @description The new version state.
*/
new_version_state?: number | null
/** @description The progress. */
progress?: string | null
/**
* Format: int64
* @description The sequence id.
*/
sequence_id?: number | null
/** @description The status. */
status?: string | null
} & {
[key: string]: unknown
}
/** @description The print upload. */
PrintUpload: {
/** @description The message. */
message: string
/**
* Format: int64
* @description The progress.
*/
progress: number
/** @description The status. */
status: string
} & {
[key: string]: unknown
}
/** @description A reason for a message. */
Reason:
| 'SUCCESS'
| 'FAIL'
| {
UNKNOWN: string
}
/** @description The result of a message. */
Result: 'SUCCESS' | 'FAIL'
/** @description A security message. */
Security: {
/**
* Format: int64
* @description The address.
*/
address: number
/** @description The chip sn. */
chip_sn: string
/**
* Format: int64
* @description The chip sn length.
*/
chipsn_len: number
/** @enum {string} */
command: 'get_sn'
/**
* Format: int64
* @description The length.
*/
length: number
/** @description The module. */
module: string
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/** @description The serial number. */
sn: string
/** @description The status. */
status: string
} & {
[key: string]: unknown
}
/** @description The sequence id type. */
SequenceId: string | number
/** @description A system command. */
System:
| ({
/** @enum {string} */
command: 'ledctrl'
/**
* Format: uint32
* @description The interval time.
*/
interval_time: number
/** @description The LED mode. */
led_mode: components['schemas']['LedMode']
/** @description The LED node. */
led_node: components['schemas']['LedNode']
/**
* Format: uint32
* @description The LED off time.
*/
led_off_time: number
/**
* Format: uint32
* @description The LED on time.
*/
led_on_time: number
/**
* Format: uint32
* @description The loop times.
*/
loop_times: number
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @description The accessory type. */
accessory_type: components['schemas']['AccessoryType']
/** @description The aux part fan. */
aux_part_fan: boolean
/** @enum {string} */
command: 'get_accessories'
depth: number
/**
* Format: double
* @description The nozzle diameter.
* @description Height of the volume ("up and down"), in millimeters.
*/
nozzle_diameter: number
/** @description The nozzle type. */
nozzle_type: components['schemas']['NozzleType']
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
height: number
/**
* Format: double
* @description Width of the volume ("left and right"), in millimeters.
*/
width: number
}
}
responses: {
/** @description Error */
@ -980,9 +233,7 @@ export interface operations {
[name: string]: unknown
}
content: {
'application/json': {
[key: string]: components['schemas']['Machine']
}
'application/json': components['schemas']['MachineInfoResponse'][]
}
}
'4XX': components['responses']['Error']
@ -1007,7 +258,7 @@ export interface operations {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['Message']
'application/json': components['schemas']['MachineInfoResponse']
}
}
'4XX': components['responses']['Error']

View File

@ -1,15 +1,16 @@
import { isDesktop } from './isDesktop'
import { components } from './machine-api'
export type MachinesListing = {
[key: string]: components['schemas']['Machine']
}
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export class MachineManager {
private _isDesktop: boolean = isDesktop()
private _machines: MachinesListing = {}
private _machines: MachinesListing = []
private _machineApiIp: string | null = null
private _currentMachine: components['schemas']['Machine'] | null = null
private _currentMachine: components['schemas']['MachineInfoResponse'] | null =
null
constructor() {
if (!this._isDesktop) {
@ -44,7 +45,7 @@ export class MachineManager {
}
machineCount(): number {
return Object.keys(this._machines).length
return this._machines.length
}
get machineApiIp(): string | null {
@ -64,11 +65,13 @@ export class MachineManager {
return 'Machine API server was discovered, but no machines are available'
}
get currentMachine(): components['schemas']['Machine'] | null {
get currentMachine(): components['schemas']['MachineInfoResponse'] | null {
return this._currentMachine
}
set currentMachine(machine: components['schemas']['Machine'] | null) {
set currentMachine(
machine: components['schemas']['MachineInfoResponse'] | null
) {
this._currentMachine = machine
}
@ -78,7 +81,6 @@ export class MachineManager {
}
this._machines = await window.electron.listMachines()
console.log('Machines:', this._machines)
}
private async updateMachineApiIp(): Promise<void> {

View File

@ -90,12 +90,24 @@ export const fileLoader: LoaderFunction = async (
let code = ''
if (!urlObj.pathname.endsWith('/settings')) {
if (!currentFileName || !currentFilePath || !projectName) {
const fallbackFile = isDesktop()
? (await getProjectInfo(projectPath)).default_file
: ''
let fileExists = isDesktop()
if (currentFilePath && fileExists) {
try {
await window.electron.stat(currentFilePath)
} catch (e) {
if (e === 'ENOENT') {
fileExists = false
}
}
}
if (!fileExists || !currentFileName || !currentFilePath || !projectName) {
return redirect(
`${PATHS.FILE}/${encodeURIComponent(
isDesktop()
? (await getProjectInfo(projectPath)).default_file
: params.id + '/' + PROJECT_ENTRYPOINT
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
)}`
)
}

View File

@ -33,6 +33,7 @@ import {
getArtifactOfTypes,
getArtifactsOfTypes,
getCapCodeRef,
getExtrudeEdgeCodeRef,
getSolid2dCodeRef,
getWallCodeRef,
} from 'lang/std/artifactGraph'
@ -141,6 +142,20 @@ export async function getEventForSelectWithPoint({
},
}
}
if (_artifact.type === 'extrudeEdge') {
const codeRef = getExtrudeEdgeCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'edge' },
},
}
}
return null
}

View File

@ -14,6 +14,7 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme'
import {
getInitialDefaultDir,
readAppSettingsFile,
readProjectSettingsFile,
writeAppSettingsFile,
@ -176,6 +177,11 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings()
// Because getting the default directory is async, we need to set it after
if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir()
}
setSettingsAtLevel(
settings,
'user',

View File

@ -16,7 +16,7 @@ window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true
engineCommandManager.kclManager = kclManager
engineCommandManager.getAstCb = () => kclManager.ast

View File

@ -1118,13 +1118,11 @@ export const modelingMachine = createMachine(
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToExtrudeArg,
// commented out as a part of https://github.com/KittyCAD/modeling-app/issues/3270
// looking to add back in the future
// zoomToFit: true,
// zoomOnRangeAndType: {
// range: selection.codeBasedSelections[0].range,
// type: 'path',
// },
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.codeBasedSelections[0].range,
type: 'path',
},
})
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => {

View File

@ -8,8 +8,10 @@ import { Issuer } from 'openid-client'
import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import'
import electronUpdater, { type AppUpdater } from 'electron-updater'
import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os'
let mainWindow: BrowserWindow | null = null
@ -22,8 +24,20 @@ if (!process.env.NODE_ENV)
console.warn(
'*FOX SCREAM* process.env.NODE_ENV is not explicitly set!, defaulting to production'
)
// Default prod values
// dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
console.log(process.env)
process.env.VITE_KC_API_WS_MODELING_URL ??=
'wss://api.zoo.dev/ws/modeling/commands'
process.env.VITE_KC_API_BASE_URL ??= 'https://api.zoo.dev'
process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
process.env.VITE_KC_SKIP_AUTH ??= 'false'
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit()
@ -59,7 +73,7 @@ const createWindow = (): BrowserWindow => {
preload: path.join(__dirname, './preload.js'),
},
icon: path.resolve(process.cwd(), 'assets', 'icon.png'),
frame: false,
frame: os.platform() !== 'darwin',
titleBarStyle: 'hiddenInset',
})
@ -80,13 +94,11 @@ const createWindow = (): BrowserWindow => {
return newWindow
}
// Quit when all windows are closed, except on macOS. There, it's common
// Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// This method will be called when Electron has finished
@ -191,6 +203,36 @@ ipcMain.handle('find_machine_api', () => {
})
})
export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
return autoUpdater
}
export async function checkForUpdates(autoUpdater: AppUpdater) {
// TODO: figure out how to get the update modal back
const result = await autoUpdater.checkForUpdatesAndNotify()
console.log(result)
}
app.on('ready', async () => {
const autoUpdater = getAutoUpdater()
checkForUpdates(autoUpdater)
const fifteenMinutes = 15 * 60 * 1000
setInterval(() => {
checkForUpdates(autoUpdater)
}, fifteenMinutes)
autoUpdater.on('update-available', (info) => {
console.log('update-available', info)
})
autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info)
})
})
ipcMain.handle('loadProjectAtStartup', async () => {
// If we are in development mode, we don't want to load a project at
// startup.

View File

@ -52,7 +52,7 @@ const kittycad = (access: string, args: any) =>
// bite our butts.
const listMachines = async (): Promise<MachinesListing> => {
const machineApi = await ipcRenderer.invoke('find_machine_api')
if (!machineApi) return {}
if (!machineApi) return []
return fetch(`http://${machineApi}/machines`).then((resp) => resp.json())
}

View File

@ -107,10 +107,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}}
nextText="Overwrite code and continue"

View File

@ -13,10 +13,7 @@ export default function Sketching() {
async function clearEditor() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('')
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
}
clearEditor()

View File

@ -82,10 +82,7 @@ export function useDemoCode() {
if (!editorManager.editorView || codeManager.code === bracket) return
setTimeout(async () => {
codeManager.updateCodeStateEditor(bracket)
kclManager.isFirstRender = true
await kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
await kclManager.executeCode(true)
await codeManager.writeToFile()
})
}, [editorManager.editorView])

View File

@ -5,6 +5,16 @@ import { Themes, getSystemTheme } from '../lib/theme'
import { PATHS } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { APP_NAME } from 'lib/constants'
import { CSSProperties, useCallback } from 'react'
import { Logo } from 'components/Logo'
import { CustomIcon } from 'components/CustomIcon'
import { Link } from 'react-router-dom'
import { APP_VERSION } from './Settings'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:text-chalkboard-30`
const SignIn = () => {
const {
@ -17,12 +27,25 @@ const SignIn = () => {
},
},
} = useSettingsAuthContext()
const signInUrl = `${VITE_KC_SITE_BASE_URL}${
PATHS.SIGN_IN
}?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' && window.location.href.replace('signin', '')
)}`
const kclSampleUrl = `${VITE_KC_SITE_BASE_URL}/docs/kcl-samples/car-wheel`
const getLogoTheme = () =>
const getThemeText = useCallback(
(shouldContrast = true) =>
theme.current === Themes.Light ||
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
? shouldContrast
? '-dark'
: ''
: shouldContrast
? ''
: '-dark',
[theme.current]
)
const signInDesktop = async () => {
// We want to invoke our command to login via device auth.
@ -35,57 +58,193 @@ const SignIn = () => {
}
return (
<main className="body-bg h-full min-h-screen m-0 p-0 pt-24">
<div className="max-w-2xl mx-auto">
<div>
<img
src={`./zma-logomark${getLogoTheme()}.svg`}
alt="Zoo Modeling App"
className="w-48 inline-block"
/>
<main className="bg-primary h-screen grid place-items-stretch m-0 p-2">
<div
style={
{
height: 'calc(100vh - 16px)',
'--circle-x': '14%',
'--circle-y': '12%',
'--circle-size-mid': '15%',
'--circle-size-end': '200%',
'--circle-timing': 'cubic-bezier(0.25, 1, 0.4, 0.9)',
} as CSSProperties
}
className="in-circle-hesitate body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
>
<div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5">
<div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8">
<div className="flex items-baseline mb-8">
<Logo className="text-primary h-10 lg:h-12 xl:h-16 relative translate-y-1 mr-4 lg:mr-6 xl:mr-8" />
<h1 className="text-3xl lg:text-4xl xl:text-5xl">{APP_NAME}</h1>
<span className="px-3 py-1 text-base rounded-full bg-primary/10 text-primary self-start">
alpha v{APP_VERSION}
</span>
</div>
<h1 className="font-bold text-2xl mt-12 mb-6">
Sign in to get started with the {APP_NAME}
</h1>
<p className="py-4">
ZMA is an open-source CAD application for creating accurate 3D models
for use in manufacturing. It is built on top of KittyCAD, the design
API from Zoo. Zoo is the first software infrastructure company built
specifically for the needs of the manufacturing industry. With ZMA we
are showing how the KittyCAD API from Zoo can be used to build
entirely new kinds of software for manufacturing.
<p className="my-4 text-lg xl:text-xl">
Thank you for using our hardware design application. It is built
on a novel CAD engine and crafted to help you create parametric,
version-controlled, and accurate parts ready for manufacturing.
</p>
<p className="py-4">
ZMA is currently in development. If you would like to be notified when
ZMA is ready for production, please sign up for our mailing list at{' '}
<a href="https://zoo.dev">zoo.dev</a>.
<p className="my-4 text-lg xl:text-xl">
As alpha software, Zoo Modeling App is still in heavy development.
We encourage feedback and feature requests that align with{' '}
<a
href="https://github.com/KittyCAD/modeling-app/issues/729"
target="_blank"
rel="noreferrer"
>
our roadmap to v1.0
</a>
.
</p>
{isDesktop() ? (
<ActionButton
Element="button"
<button
onClick={signInDesktop}
iconStart={{ icon: 'arrowRight' }}
className="w-fit mt-4"
className={
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="sign-in-button"
>
Sign in
</ActionButton>
Sign in to get started
<CustomIcon name="arrowRight" className="w-6 h-6" />
</button>
) : (
<ActionButton
Element="link"
to={`${VITE_KC_SITE_BASE_URL}${
PATHS.SIGN_IN
}?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' &&
window.location.href.replace('signin', '')
)}`}
iconStart={{ icon: 'arrowRight' }}
className="w-fit mt-4"
<Link
onClick={openExternalBrowserIfDesktop(signInUrl)}
to={signInUrl}
className={
'w-fit m-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="sign-in-button"
>
Sign in
</ActionButton>
Sign in to get started
<CustomIcon name="arrowRight" className="w-6 h-6" />
</Link>
)}
</div>
<Link
className={`group relative xl:h-full xl:row-span-full col-start--1 xl:col-start-4 rounded-lg overflow-hidden grid place-items-center ${subtleBorder}`}
to={kclSampleUrl}
onClick={openExternalBrowserIfDesktop(kclSampleUrl)}
target="_blank"
rel="noreferrer noopener"
>
<video
autoPlay
loop
muted
playsInline
className="h-full object-cover object-center"
>
<source
src={`${isDesktop() ? '.' : ''}/wheel-loop${getThemeText(
false
)}.mp4`}
type="video/mp4"
/>
</video>
<div
className={
'absolute bottom-0 left-0 right-0 transition translate-y-4 opacity-0 ' +
'group-hover:translate-y-0 group-hover:opacity-100 ' +
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="sign-in-button"
>
View this sample
<CustomIcon name="arrowRight" className="w-6 h-6" />
</div>
</Link>
<div className="self-end h-min col-span-3 xl:row-span-2 grid grid-cols-2 gap-5">
<div className={cardArea}>
<h2 className="text-xl">Built in the open</h2>
<p className="text-xs my-4">
Open-source and open discussions. Check our public code base and
join our Discord.
</p>
<div className="flex gap-4 flex-wrap items-center">
<ActionButton
Element="externalLink"
to="https://github.com/KittyCAD/modeling-app"
iconStart={{ icon: 'code' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">Read our source code</span>
</ActionButton>
<ActionButton
Element="externalLink"
to="https://discord.gg/JQEpHR7Nt2"
iconStart={{ icon: 'keyboard' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">Join our community</span>
</ActionButton>
</div>
</div>
<div className={cardArea}>
<h2 className="text-xl">Ready for the future</h2>
<p className="text-xs my-4">
Modern software ideas being brought together to create a
familiar modeling experience with new superpowers.
</p>
<div className="flex gap-4 flex-wrap items-center">
<ActionButton
Element="externalLink"
to="https://zoo.dev/docs/kcl-samples/ball-bearing"
iconStart={{ icon: 'settings' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">
Parametric design with KCL
</span>
</ActionButton>
<ActionButton
Element="externalLink"
to="https://zoo.dev/docs/tutorials/text-to-cad"
iconStart={{ icon: 'sparkles' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">AI-unlocked CAD</span>
</ActionButton>
</div>
</div>
<div className={cardArea + ' col-span-2'}>
<h2 className="text-xl">
Built on the first infrastructure for hardware design
</h2>
<p className="text-xs my-4">
You can make your own niche hardware design tools with our
design and machine learning interfaces. We're building Modeling
App in the same way.
</p>
<div className="flex gap-4 flex-wrap items-center">
<ActionButton
Element="externalLink"
to="https://zoo.dev/design-api"
iconStart={{ icon: 'sketch' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">KittyCAD Design API</span>
</ActionButton>
<ActionButton
Element="externalLink"
to="https://zoo.dev/machine-learning-api"
iconStart={{ icon: 'elephant' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>
<span className="py-2 lg:py-0">
ML-ephant Machine Learning API
</span>
</ActionButton>
</div>
</div>
</div>
</div>
</div>
</main>
)
}

View File

@ -127,18 +127,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
name = "async-trait"
version = "0.1.81"
version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -149,7 +149,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -397,7 +397,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -591,7 +591,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -602,7 +602,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -657,7 +657,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
"synstructure",
]
@ -672,7 +672,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.25"
version = "0.1.26"
dependencies = [
"Inflector",
"anyhow",
@ -686,7 +686,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -697,7 +697,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -724,7 +724,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -896,7 +896,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -986,7 +986,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -1345,7 +1345,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.11"
version = "0.2.13"
dependencies = [
"anyhow",
"approx",
@ -1412,12 +1412,12 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
name = "kcl-test-server"
version = "0.1.9"
version = "0.1.10"
dependencies = [
"anyhow",
"hyper",
@ -1430,9 +1430,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb7c076d64ad00a29ae900108707d1bbb583944d4b2d005e1eca9914a18c7c2"
checksum = "94feea5b1cf851b33dd108aa35aa01bde99772aa74d2ba1590295aac0b7ca33e"
dependencies = [
"anyhow",
"async-trait",
@ -1799,7 +1799,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"structmeta",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -1852,7 +1852,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2016,7 +2016,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2029,7 +2029,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2491,7 +2491,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2565,7 +2565,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2576,7 +2576,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2600,7 +2600,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2621,7 +2621,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2752,7 +2752,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2763,7 +2763,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2807,9 +2807,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.76"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
@ -2830,7 +2830,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -2937,7 +2937,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -3008,9 +3008,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.3"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"bytes",
@ -3032,7 +3032,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -3185,7 +3185,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -3213,7 +3213,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -3290,7 +3290,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
"termcolor",
]
@ -3448,7 +3448,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]
@ -3509,7 +3509,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
"wasm-bindgen-shared",
]
@ -3544,7 +3544,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3869,7 +3869,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn 2.0.77",
]
[[package]]

View File

@ -16,7 +16,7 @@ gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["sync"] }
tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
@ -29,7 +29,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0"
reqwest = { version = "0.11.26", default-features = false }
tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "time"] }
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
@ -70,7 +70,7 @@ members = [
[workspace.dependencies]
http = "0.2.12"
kittycad = { version = "0.3.17", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.3.18", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4"
[[test]]

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.9"
version = "0.1.10"
edition = "2021"
license = "MIT"
@ -12,4 +12,4 @@ kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.11"
version = "0.2.13"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -13,14 +13,14 @@ keywords = ["kcl", "KittyCAD", "CAD"]
[dependencies]
anyhow = { version = "1.0.86", features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = "0.1.81"
async-trait = "0.1.82"
base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0"
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.24", path = "../derive-docs" }
derive-docs = { version = "0.1.26", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
@ -52,7 +52,7 @@ zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }
tokio = { version = "1.39.3", features = ["sync", "time"] }
tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42"
@ -94,7 +94,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
insta = { version = "1.38.0", features = ["json"] }
itertools = "0.13.0"
pretty_assertions = "1.4.0"
tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros", "time"] }
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8.0"
[[bench]]

View File

@ -2573,7 +2573,6 @@ impl ObjectExpression {
}
})
.collect();
dbg!(&format_items);
let end_indent = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {

View File

@ -9,6 +9,8 @@ use crate::{
executor::{KclValue, SourceRange, UserVal},
};
const KCL_NONE_ID: &str = "KCL_NONE_ID";
/// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration,
/// arguments are in the function call/application).
@ -20,6 +22,45 @@ pub struct KclNone {
// TODO: Convert this to be an Option<SourceRange>.
pub start: usize,
pub end: usize,
#[serde(deserialize_with = "deser_private")]
#[ts(skip)]
#[schemars(skip)]
__private: Private,
}
impl KclNone {
pub fn new(start: usize, end: usize) -> Self {
Self {
start,
end,
__private: Private {},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake, Default)]
#[databake(path = kcl_lib::ast::types)]
struct Private;
impl Serialize for Private {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(KCL_NONE_ID)
}
}
fn deser_private<'de, D>(deserializer: D) -> Result<Private, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s == KCL_NONE_ID {
Ok(Private {})
} else {
Err(serde::de::Error::custom("not a KCL none"))
}
}
impl From<&KclNone> for SourceRange {
@ -57,3 +98,24 @@ impl KclNone {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn other_types_will_not_deserialize() {
// This shouldn't deserialize into a KCL None,
// because it's missing the special Private tag.
let j = r#"{"start": 0, "end": 0}"#;
let _e = serde_json::from_str::<KclNone>(j).unwrap_err();
}
#[test]
fn serialize_then_deserialize() {
// Serializing, then deserializing a None should produce the same value.
let before = KclNone::default();
let j = serde_json::to_string_pretty(&before).unwrap();
let after: KclNone = serde_json::from_str(&j).unwrap();
assert_eq!(before, after);
}
}

View File

@ -2080,10 +2080,7 @@ fn assign_args_to_params(
if param.optional {
// If the corresponding parameter is optional,
// then it's fine, the user doesn't need to supply it.
let none = KclNone {
start: param.identifier.start,
end: param.identifier.end,
};
let none = KclNone::new(param.identifier.start, param.identifier.end);
fn_memory.add(
&param.identifier.name,
KclValue::from(&none),

View File

@ -135,13 +135,16 @@ fn non_code_node_no_leading_whitespace(i: TokenSlice) -> PResult<NonCodeNode> {
fn pipe_expression(i: TokenSlice) -> PResult<PipeExpression> {
let mut non_code_meta = NonCodeMeta::default();
let (head, noncode) = terminated(
(expression_but_not_pipe, preceded(whitespace, opt(non_code_node))),
let (head, noncode): (_, Vec<_>) = terminated(
(
expression_but_not_pipe,
repeat(0.., preceded(whitespace, non_code_node)),
),
peek(pipe_surrounded_by_whitespace),
)
.context(expected("an expression, followed by the |> (pipe) operator"))
.parse_next(i)?;
if let Some(nc) = noncode {
for nc in noncode {
non_code_meta.insert(0, nc);
}
let mut values = vec![head];
@ -3453,6 +3456,17 @@ mod snapshot_tests {
c: 3
}"
);
snapshot_test!(
ba,
r#"
const sketch001 = startSketchOn('XY')
// |> arc({
// angleEnd: 270,
// angleStart: 450,
// }, %)
|> startProfileAt(%)
"#
);
}
#[allow(unused)]

View File

@ -0,0 +1,150 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 133,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 1,
"end": 132,
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 132,
"id": {
"type": "Identifier",
"start": 7,
"end": 16,
"name": "sketch001",
"digest": null
},
"init": {
"type": "PipeExpression",
"type": "PipeExpression",
"start": 19,
"end": 132,
"body": [
{
"type": "CallExpression",
"type": "CallExpression",
"start": 19,
"end": 38,
"callee": {
"type": "Identifier",
"start": 19,
"end": 32,
"name": "startSketchOn",
"digest": null
},
"arguments": [
{
"type": "Literal",
"type": "Literal",
"start": 33,
"end": 37,
"value": "XY",
"raw": "'XY'",
"digest": null
}
],
"optional": false,
"digest": null
},
{
"type": "CallExpression",
"type": "CallExpression",
"start": 115,
"end": 132,
"callee": {
"type": "Identifier",
"start": 115,
"end": 129,
"name": "startProfileAt",
"digest": null
},
"arguments": [
{
"type": "PipeSubstitution",
"type": "PipeSubstitution",
"start": 130,
"end": 131,
"digest": null
}
],
"optional": false,
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"type": "NonCodeNode",
"start": 41,
"end": 52,
"value": {
"type": "blockComment",
"value": "|> arc({",
"style": "line"
},
"digest": null
},
{
"type": "NonCodeNode",
"start": 55,
"end": 74,
"value": {
"type": "blockComment",
"value": "angleEnd: 270,",
"style": "line"
},
"digest": null
},
{
"type": "NonCodeNode",
"start": 77,
"end": 98,
"value": {
"type": "blockComment",
"value": "angleStart: 450,",
"style": "line"
},
"digest": null
},
{
"type": "NonCodeNode",
"start": 101,
"end": 109,
"value": {
"type": "blockComment",
"value": "}, %)",
"style": "line"
},
"digest": null
}
]
},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -294,6 +294,13 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_sketch_groups_and_data<'a, T>(&'a self) -> Result<(Vec<SketchGroup>, Option<T>), KclError>
where
T: FromArgs<'a> + serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_optional_tag<'a, T>(&'a self) -> Result<(T, Option<FaceTag>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
@ -360,6 +367,13 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_float<'a, T>(&'a self) -> Result<(T, f64), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_number_sketch_group_set(&self) -> Result<(f64, SketchGroupSet), KclError> {
FromArgs::from_args(self, 0)
}
@ -495,6 +509,9 @@ where
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(arg) = args.args.get(i) else { return Ok(None) };
if crate::ast::types::KclNone::from_mem_item(arg).is_some() {
return Ok(None);
}
let Some(val) = T::from_mem_item(arg) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
@ -617,9 +634,12 @@ impl_from_arg_via_json!(super::revolve::RevolveData);
impl_from_arg_via_json!(super::sketch::SketchData);
impl_from_arg_via_json!(crate::std::import::ImportFormat);
impl_from_arg_via_json!(crate::std::polar::PolarCoordsData);
impl_from_arg_via_json!(crate::std::loft::LoftData);
impl_from_arg_via_json!(crate::std::planes::StandardPlane);
impl_from_arg_via_json!(SketchGroup);
impl_from_arg_via_json!(FaceTag);
impl_from_arg_via_json!(String);
impl_from_arg_via_json!(crate::ast::types::KclNone);
impl_from_arg_via_json!(u32);
impl_from_arg_via_json!(u64);
impl_from_arg_via_json!(f64);
@ -686,3 +706,13 @@ impl<'a> FromKclValue<'a> for SketchSurface {
}
}
}
impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<SketchGroup>>().map(|x| x.0)
}
}

View File

@ -181,6 +181,32 @@ pub(crate) async fn do_post_extrude(
vec![]
};
for face_info in face_infos.iter() {
if face_info.cap == kittycad::types::ExtrusionFaceCapType::None {
if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetOppositeEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,
},
)
.await?;
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,
},
)
.await?;
}
}
}
// Create a hashmap for quick id lookup
let mut face_id_map = std::collections::HashMap::new();
// creating fake ids for start and end caps is to make extrudes mock-execute safe

View File

@ -304,7 +304,7 @@ async fn inner_get_next_adjacent_edge(tag: TagIdentifier, args: Args) -> Result<
let resp = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetPrevAdjacentEdge {
ModelingCmd::Solid3DGetNextAdjacentEdge {
edge_id: tagged_path.id,
object_id: tagged_path.sketch_group,
face_id,
@ -312,7 +312,7 @@ async fn inner_get_next_adjacent_edge(tag: TagIdentifier, args: Args) -> Result<
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetPrevAdjacentEdge { data: ajacent_edge },
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetNextAdjacentEdge { data: ajacent_edge },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
@ -386,7 +386,7 @@ async fn inner_get_previous_adjacent_edge(tag: TagIdentifier, args: Args) -> Res
let resp = args
.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DGetNextAdjacentEdge {
ModelingCmd::Solid3DGetPrevAdjacentEdge {
edge_id: tagged_path.id,
object_id: tagged_path.sketch_group,
face_id,
@ -394,7 +394,7 @@ async fn inner_get_previous_adjacent_edge(tag: TagIdentifier, args: Args) -> Res
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetNextAdjacentEdge { data: ajacent_edge },
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetPrevAdjacentEdge { data: ajacent_edge },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {

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