Compare commits

...

44 Commits

Author SHA1 Message Date
f5ee346408 Release derive-docs 0.1.13 (#2044) 2024-04-09 17:02:16 +00:00
544a7565e3 Migrate to tauri v2 (#1400)
* Fix vite build (tauri build still broken)

* Fix yarn builds with a couple of shortcuts

* Fix file creation

* Fix documentDir behavior

* Got stream with default file

* Clean up

* Clean up

* Use 'unstable'; fix devtools callsite

The API changed a bit here, which forces us to use the unstable crate
feature. The call to open devtools is also new; it's now on the
webview not window.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>

* Bring back read_dir_recursive from v1

* Fix dates

* More fixes, incl. conf files

* cargo fmt

* Add Updater plugin

* Fix types

* Fix isTauri detection and updater bootup

* Schemas

* Clean up

* Disable devtools

* Attempt at fixing builds

* WIP Ubuntu dep

* WIP Ubuntu dep

* WIP keys in debug

* Enable updater only on release builds

* Reenable webtools on debug

* No linux bundles

* Typo

* Attemp at fixing --bundles none

* Manual tauri debug build

* Empty commit to trigger the CI

* Fix settings

* Empty commit to trigger the CI

* Merge branch 'main' into pierremtb/issue1349

* Add allow-create perm

* tauri-driver no cap

* Empty commit to trigger the CI

* Clean up

* Clean up

* Migrate to tauri v2
Fixes #1349

* Fix fmt

* Merge branch 'main' into pierremtb/issue1349

* Force BUILD_RELEASE: true

* Bump tauri to new beta

* Merge branch 'main' into pierremtb/issue1349

* Fix linux tests

* Fix types

* Add --verbose to tauri-action

* Move --verbose to front

* Back to tauri-driver@0.1.3 and single \ for win

* Back to latest driver, and windows wip

* Disable release conf temporarily

* Rollback to 2.0.0-beta.2

* Rollback to 2.0.0-beta.1

* Move bundle to root for src-tauri/tauri.release.conf.json

* All packages to latest (add http and shell to package.json)

* Testing latest commit for tauri-action

* Remove tauri action

* Add cat

* WIP

* Update ci.yml

* Disable release conf

* Disable rust cache

* Add tauri-action back for release builds

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

* Update .codespellrc

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

* Trigger CI

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

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

* Fix type

* Clean up

* More clean up

* Fix path concatenation with join

* Attempt at fixing linux tests

* Config clean up

* Downgrade to tauri-driver@0.1.3

* Looks like tauri v2 is actually doing better with linux package names ah!

* Change Linux apt packages

* Increase wdio connectionRetryTimeout

* Revert connectionRetryTimeout and bump tauri packages

* Back to latest tauri-driver

* Disable linux e2e tests

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

* Trigger CI

* Clean up

* Update snapshots

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

* Trigger CI

* Remove @sentry/react

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

* Rename migrated.json to desktop.json

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

* Trigger CI

* Change wasm url to http on Windows

---------

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
Co-authored-by: Paul R. Tagliamonte <paul@kittycad.io>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-04-09 08:04:36 -04:00
979046f7e6 Clean up batch code (#2041)
* Clean up batch code

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

* Remove 'flush_batch: bool' from send_modeling_cmd

It was always being set with false, and it was
bugged for true. If true was set, the cmd would
never actually be run.

* Fix derive-docs

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-09 00:18:35 -05:00
07ae5106b9 Update rust dep 'h2' (#2037) 2024-04-08 18:25:47 -05:00
e9ae484332 Regenerate docs (#2040)
Rebuild KCL docs
2024-04-08 22:28:54 +00:00
2a86ffc09a Cut release v0.17.3 (#2032)
* Cut release v0.17.3

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

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-05 18:21:30 -04:00
93903a8a47 update 20/20 (#2028) 2024-04-05 16:24:30 +00:00
45e85a1f81 refactor CustomIcon component (#2026) 2024-04-05 12:04:49 -04:00
c187989d18 Send a WsMsg::Close before we bail normally. (#2027)
Send a WsMsg::Close before we bail normally.

This will hopefully trigger engine-manager to release the engine back to
the pool faster, allowing us to increase the number of threads we can
run the modeling-app tests with.

Signed-off-by: Paul R. Tagliamonte <paul@zoo.dev>
2024-04-05 15:37:46 +00:00
47b5fa1459 Franknoirot/bugfix directory picker (#2025)
* Fix project directory setting input

* Remove unused imports

* Almost working Tauri test

* Finish Tauri e2e test

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

* fmt

* Try a different Webriver selector

* Update themeColor component to use new updateValue API

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

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-05 05:48:12 +00:00
d85781ef99 Swap out primary UI color for Zoo brand blue, add theme color setting to control its hue (#2017)
* Add a setting for themeColor

* Add primary-color to Tailwind, driven by themeColor setting

* Get rid of most uses of "energy" colors

* Change out the rest of the energy colors

* Tweak NetworkHealthIndicator light mode checkmarks

* Handful of other CSS tweaks while I'm here:
- remove the AppHeader bg and border
- pane margins
- better dark mode button styles

* Make Zoo logomark a badge

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

* Re-run CI post-snapshots

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

* Retrigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-05 00:59:02 -04:00
233f81a879 Make it possible to permanently dismiss the web banner from the settings (#2021)
* Make it possible to include a setting only on the Settings dialog, not also in the command bar.

* Add web-only setting to permanently dismiss banner

* Honor the dismiss web banner setting

* Remove unused state from useStore

* Make the banner only appear in production builds again
2024-04-05 00:30:11 -04:00
8ac0bf4953 Don't assume a command is found in "Find and select command" command bar action (#2019) 2024-04-04 19:15:26 -04:00
24caeece65 Update snapshots (#2020)
Engine changed rendering again, so, gotta update snapshots
2024-04-04 16:15:39 -05:00
f493cf11a0 Print WebSocket errors when we get them (#2018)
* Print WebSocket errors when we get them

Previously, we eat them and ignore them, but now we'll seek out
and actively print error messages to stderr. We'd previously get a
websocket closed error, but we'll usually get an Error message over the
WebSocket before its closed on us.

Here's some example output during a crash

```
got ws error: WebSocket protocol error: Connection reset without closing handshake

Caused by:
    Connection reset without closing handshake
thread 'serial_test_cube_mm' panicked at tests/executor/main.rs:1136:10:
called `Result::unwrap()` on an `Err` value: engine: KclErrorDetails { source_ranges: [SourceRange([180, 188])], message: "Modeling command failed: websocket closed early" }
test serial_test_cube_mm ... FAILED
got error message: {
  "error_code": "bad_request",
  "message": "Too many active connections, only 2 allowed per user."
}
```
2024-04-04 18:47:47 +00:00
594e888c12 Benchmark rust in CI with iai, not criterion (#1937)
* Rename cargo-criterion to cargo-bench

* Use iai not criterion in CI

We want to benchmark the KCL parser and tokenizer to make sure we don't
accidentally slow them down. Generally Rust projects use Criterion to
benchmark code. Criterion runs your functions a few thousand times to
get reliable wall-clock measurements.

This is good for locally benchmarking but bad for benchmarking in CI.
Why? Because in CI, you're running a container on some shared VM, so
wall-clock time might have a lot of interference from noisy neighbours.
Also, your benchmarks take a long time to run and eat up paid CI minutes.

A better approach for benchmarking in CI is to just count the number of
CPU instructions executed. This correlates with wall-clock time, but it
only needs to run the function once, so it takes much less time. It also
isn't changed by any noisy neighbours running on the same VM or hardware.

This PR adds a new benchmark suite which counts instructions using `iai`,
from the creator of criterion. He says iai and criterion complement each
other nicely. We can run criterion locally and run iai in CI.

* Update image in markdown docs
2024-04-04 09:50:34 -05:00
b32295e1d9 Cut release v0.17.2 (#2007)
* Cut release v0.17.2

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

* trigger ci

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

* trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-04 18:13:11 +11:00
e0838c1198 Fix home redirect in browser (#2008) 2024-04-04 00:41:40 +00:00
f03f34d8be dynamic cursor depending on mouse scene state (#1995)
* dynamic cursor depending on mouse scene state

* hover stuff

* bump min length

* clean up

* sketch on face failing randomly

* more time out for extrude snapshots

* Update src/clientSideScene/sceneEntities.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* move for profileStart handle, and select when no tool equiped

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-04-04 00:07:51 +00:00
108bb4ee90 Bump bson from 2.9.0 to 2.10.0 in /src/wasm-lib (#1997)
Bumps [bson](https://github.com/mongodb/bson-rust) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/mongodb/bson-rust/releases)
- [Commits](https://github.com/mongodb/bson-rust/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: bson
  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>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-04-03 21:43:42 +00:00
092d459026 Make the sketch larger so 20-20 failures are clearer (#1989) 2024-04-03 21:42:58 +00:00
c4f7296e32 Bump tokio from 1.36.0 to 1.37.0 in /src-tauri (#1961)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.36.0...tokio-1.37.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-04-03 21:35:05 +00:00
1cbd422d7f Bump vite from 5.2.2 to 5.2.6 (#2004)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.2 to 5.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 14:19:43 -07:00
849685a986 Bump @babel/preset-env from 7.23.3 to 7.24.3 (#1885)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.23.3 to 7.24.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.3/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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-04-03 14:19:05 -07:00
359b3c1f35 Bump @uiw/react-codemirror from 4.21.24 to 4.21.25 (#1960)
Bumps [@uiw/react-codemirror](https://github.com/uiwjs/react-codemirror) from 4.21.24 to 4.21.25.
- [Release notes](https://github.com/uiwjs/react-codemirror/releases)
- [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.21.24...v4.21.25)

---
updated-dependencies:
- dependency-name: "@uiw/react-codemirror"
  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-04-03 14:18:52 -07:00
f4ff5e43f2 Bump happy-dom from 14.3.1 to 14.3.10 (#1986)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 14.3.1 to 14.3.10.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v14.3.1...v14.3.10)

---
updated-dependencies:
- dependency-name: happy-dom
  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-04-03 14:18:06 -07:00
daf7350c9e Bump tauri-plugin-fs-extra from c6a5b7a to 73d8562 in /src-tauri (#1999)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `c6a5b7a` to `73d8562`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](c6a5b7ae47...73d8562849)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs-extra
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 14:17:54 -07:00
a829cdb006 Bump syn from 2.0.55 to 2.0.58 in /src/wasm-lib (#1998)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.55 to 2.0.58.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.55...2.0.58)

---
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-04-03 14:16:59 -07:00
1a7a19ee85 tweak uuid import (#2001) 2024-04-03 08:38:16 +00:00
b045a89854 New segments can be added in the middle of a sketch (#1953)
* get branch up to where it was before

* setup dots properly

* only show extra handle on hover

* use partical texture for plus button

* fix regression

* fix deleted line

* fix sketch on face test

* caluclate segment length in screen-space/in-pixels

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

* side small segment handles on resize

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

* Make sure this works on setup and update of segments

* Add to tangential arcs

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

This reverts commit 5dc1adacae.

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

This reverts commit b8ceea179c.

* try and fix sketch on face in CI

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

* more test fix

* convert scaling to be based on pixels

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

* trigger ci

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

This reverts commit 6287c943dd.

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

This reverts commit 1baa3819db.

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

* Update src/clientSideScene/segments.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

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

* reduce line thickness

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

* trigger CI

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

* trigger CI

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

* trigger CI

* try putting init script back in

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-04-03 02:22:56 +00:00
221f037eaa generate docs (#1994)
* generate docs

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

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-02 18:40:39 -07:00
a93b72f7e1 fix member expression in object expression (#1992)
* fix member expression in object expression

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

* new renders

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

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

* empty trigger

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

* empty

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-02 18:28:50 -07:00
63f36cbcbf Bump tokio from 1.36.0 to 1.37.0 in /src/wasm-lib (#1964)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.36.0...tokio-1.37.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-04-02 11:30:25 -05:00
79b50ef7d4 Bump regex from 1.10.3 to 1.10.4 in /src/wasm-lib (#1965)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.3...1.10.4)

---
updated-dependencies:
- dependency-name: regex
  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-04-02 11:30:15 -05:00
3d16dcd30d Bump insta from 1.37.0 to 1.38.0 in /src/wasm-lib (#1966)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.37.0 to 1.38.0.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.37.0...1.38.0)

---
updated-dependencies:
- dependency-name: insta
  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-04-02 11:30:05 -05:00
d605d4a029 Rearchitect settings system to be scoped (#1956)
* BROKEN: start of scopes for each setting

* Clean up later: mostly-functional scoped settings!
Broken command bar, unimplemented generated settings components

* Working persisted project settings in-folder

* Start working toward automatic commands and settings UI

* Relatively stable, settings-menu-editable

* Settings persistence tweaks after merge

* Custom settings UI working properly, cleaner types

* Allow boolean command types, create Settings UI for them

* Add support for option and string Settings input types

* Proof of concept settings from command bar

* Add all settings to command bar

* Allow settings to be hidden on a level

* Better command titles for settings

* Hide the settings the settings from the commands bar

* Derive command defaultValue from *current* settingsMachine context

* Fix generated settings UI for 'options' type settings

* Pretty settings modal 💅

* Allow for rollback to parent level setting

* fmt

* Fix tsc errors not related to loading from localStorage

* Better setting descriptions, better buttons

* Make displayName searchable in command bar

* Consolidate constants, get working in browser

* Start fixing tests, better types for saved settings payloads

* Fix playwright tests

* Add a test for the settings modal

* Add AtLeast to codespell ignore list

* Goofed merge of codespellrc

* Try fixing linux E2E tests

* Make codespellrc word lowercase

* fmt

* Fix data-testid in Tauri test

* Don't set text settings if nothing changed

* Turn off unimplemented settings

* Allow for multiple "execution-done" messages to have appeared in snapshot tests

* Try fixing up snapshot tests

* Switch from .json to .toml settings file format

* Use a different method for overriding the default units

* Try to force using the new common storage state in snapshot tests

* Update tests to use TOML

* fmt and remove console logs

* Restore units to export

* tsc errors, make snapshot tests use TOML

* Ensure that snapshot tests use the basicStorageState

* Re-organize use of test.use()

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

* Update snapshots one more time since lighting changed

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

* Fix broken "Show in folder" for project-level settings

* Fire all relevant actions after settings reset

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

* Properly reset the default directory

* Hide settings by platform

* Actually honor showDebugPanel

* Unify settings hiding logic

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

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

* fix first extrusion snapshot

* another attempt to fix extrustion snapshot

* Rerun test suite

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

* trigger CI

* more extrusion stuff

* Replace resetSettings console log with comment

---------

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-04-02 14:29:34 +00:00
77f51530f9 set selection as top level event only (#1988)
* set selection as top level event only

* target quirk fix

* fix old bug

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

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-02 09:20:42 +00:00
76480f1a43 Only send one SetSceneUnits call (#1981)
We only intended to send this once, at the start of execution. But the execute function is recursive, so it was sending many SetSceneUnits calls.

Fixes https://github.com/KittyCAD/modeling-app/issues/1971
2024-04-01 21:37:05 +00:00
f850f80de1 Update 20-20 snapshot outputs (#1982)
Engine added vantage-independent lighting, tests must be rerun.
2024-04-01 21:19:53 +00:00
15ebbe6947 Pipelines cause Z-fighting (#1976)
Bug: You can see here that the two programs under tests/ are equivalent, just one uses 
pipelines and one always assigns to a new sketchgroup. However, the pipeline
produces weird visual bugs. Jess did a git bisect to figure out this was the problem that
Mike was experiencing, around weird visual artifacts with filleting.

Ultimately the bug was that my rewritten `execute_pipe_body` function was executing
the first expression of the pipeline body twice! In most unit tests this didn't matter,
because the first expression in a pipeline was startSketchAt. No big deal to run that
twice. However, in Mike's program, the first expression was `make_circle` or `pentagon`,
user-defined functions that sent a lot of API calls. This meant the pipeline duplicated a lot
of geometry, causing Z-fighting and weird artifacts.
2024-04-01 19:48:57 +00:00
01beba42da Allow two 'serial_test_' to run simultaneously (#1978)
Running two modeling sessions simultaneously will:

 - Speed up unit testing
 - Stress test our API/engine better
2024-04-01 17:36:27 +00:00
509e372ed2 Add plumbus test (#1975)
* add plumbus test

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

* plumbus image

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

* twenty twenty

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-03-29 15:06:11 -07:00
b0417114af New test case: Paul's riddle, shortened (#1970)
This is discussed in https://github.com/KittyCAD/modeling-app/issues/1969
2024-03-29 20:56:32 +00:00
0360a4021b Onboarding updates (#1967)
* Make onboarding line references dynamic and error if they aren't found
Fixes https://github.com/KittyCAD/modeling-app/issues/1918

* More clear and correct Sketch onboarding step
Fixes https://github.com/KittyCAD/modeling-app/issues/1790
Fixes https://github.com/KittyCAD/modeling-app/issues/1789

* Make sample code line references dynamic and error on app start if they break so we can know before release
Fixes https://github.com/KittyCAD/modeling-app/issues/1918

* Better error message for searchText failure

* JB onboarding feedback

* Make list more explicit, instruct to hold down key first
2024-03-29 12:56:32 -04:00
373 changed files with 7121 additions and 4064 deletions

View File

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

View File

@ -7,23 +7,23 @@ on:
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'
- .github/workflows/cargo-criterion.yml - .github/workflows/cargo-bench.yml
pull_request: pull_request:
paths: paths:
- '**.rs' - '**.rs'
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'
- .github/workflows/cargo-criterion.yml - .github/workflows/cargo-bench.yml
workflow_dispatch: workflow_dispatch:
permissions: read-all permissions: read-all
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
name: cargo criterion name: cargo bench
jobs: jobs:
cargocriterion: cargo-bench:
name: cargo criterion name: Benchmark with iai
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -31,10 +31,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
cargo install cargo-criterion cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1 uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library - name: Benchmark kcl library
shell: bash shell: bash
run: |- run: |-
cd src/wasm-lib/kcl; cargo criterion cd src/wasm-lib/kcl; cargo bench -- iai

View File

@ -125,6 +125,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-14, ubuntu-latest, windows-latest] os: [macos-14, ubuntu-latest, windows-latest]
env:
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -144,10 +147,12 @@ jobs:
sudo apt-get update && sudo apt-get update &&
sudo apt-get install -y sudo apt-get install -y
libgtk-3-dev libgtk-3-dev
libgtksourceview-3.0-dev libayatana-appindicator3-dev
webkit2gtk-4.0
libappindicator3-dev
webkit2gtk-driver webkit2gtk-driver
libsoup-3.0-dev
libjavascriptcoregtk-4.1-dev
libwebkit2gtk-4.1-dev
at-spi2-core
xvfb xvfb
- name: Sync node version and setup cache - name: Sync node version and setup cache
@ -161,7 +166,9 @@ jobs:
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
- name: Setup Rust cache - name: Setup Rust cache
if: matrix.os != 'windows-latest'
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
with: with:
workspaces: './src-tauri -> target' workspaces: './src-tauri -> target'
@ -224,14 +231,14 @@ jobs:
with: with:
includeRelease: false includeRelease: false
includeDebug: true includeDebug: true
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Build the app (release) and sign - name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' }}
env: env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@ -240,7 +247,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}" TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with: with:
args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}" args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest' if: matrix.os != 'ubuntu-latest'
@ -250,10 +257,11 @@ jobs:
with: with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*" path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
# TODO: re-enable linux e2e tests when possible
- name: Run e2e tests (linux only) - name: Run e2e tests (linux only)
if: matrix.os == 'ubuntu-latest' if: false
run: | run: |
cargo install tauri-driver@0.1.3 cargo install tauri-driver
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }} source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri xvfb-run yarn test:e2e:tauri

1
.gitignore vendored
View File

@ -51,5 +51,6 @@ e2e/playwright/export-snapshots/*
## generated files ## generated files
src/**/*.typegen.ts src/**/*.typegen.ts
src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json src/wasm-lib/grackle/stdlib_cube_partial.json

View File

@ -281,7 +281,7 @@ https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f
<details> <details>
<summary> <summary>
Ps for the debug panel, the following JSON is useful for snapping the camera PS: for the debug panel, the following JSON is useful for snapping the camera
</summary> </summary>
```JSON ```JSON

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -1,10 +1,11 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme'
import { initialSettings } from '../../src/lib/settings/initialSettings'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
/* /*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -30,31 +31,14 @@ test.beforeEach(async ({ context, page }) => {
resources: ['tcp:3000'], resources: ['tcp:3000'],
timeout: 5000, timeout: 5000,
}) })
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
}, secrets.token)
// kill animations, speeds up tests and reduced flakiness // kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60000) test.setTimeout(60000)
test('Basic sketch', async ({ page }) => { test('Basic sketch', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -529,96 +513,133 @@ test('Auto complete works', async ({ page }) => {
}) })
// Stored settings validation test // Stored settings validation test
test('Stored settings are validated and fall back to defaults', async ({ test.describe('Settings persistence and validation tests', () => {
page, // Override test setup
context,
}) => {
// Override beforeEach test setup
// with corrupted settings // with corrupted settings
await context.addInitScript(async () => { const storageState = structuredClone(basicStorageState)
const storedSettings = JSON.parse( const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' settings: SaveSettingsPayload
) }
s.settings.app.theme = Themes.Dark
s.settings.app.projectDirectory = 123 as any
s.settings.modeling.defaultUnit = 'invalid' as any
s.settings.modeling.mouseControls = `() => alert('hack the planet')` as any
s.settings.projects.defaultProjectName = false as any
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
// Corrupt the settings test.use({ storageState })
storedSettings.baseUnit = 'invalid'
storedSettings.cameraControls = `() => alert('hack the planet')`
storedSettings.defaultDirectory = 123
storedSettings.defaultProjectName = false
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) test('Stored settings are validated and fall back to defaults', async ({
page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Check the settings were reset
const storedSettings = TOML.parse(
await page.evaluate(() => localStorage.getItem('/user.toml') || '{}')
) as { settings: SaveSettingsPayload }
expect(storedSettings.settings.app?.theme).toBe('dark')
// Check that the invalid settings were removed
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
}) })
await page.setViewportSize({ width: 1200, height: 500 }) test('Project settings can be set and override user settings', async ({
await page.goto('/', { waitUntil: 'domcontentloaded' }) page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Check the toast appeared // Open the settings modal with the browser keyboard shortcut
await expect( await page.keyboard.press('Meta+Shift+,')
page.getByText(`Error validating persisted settings:`, {
exact: false,
})
).toBeVisible()
// Check the settings were reset await expect(
const storedSettings = JSON.parse( page.getByRole('heading', { name: 'Settings', exact: true })
await page.evaluate( ).toBeVisible()
() => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' await page
) .locator('select[name="app-theme"]')
) .selectOption({ value: 'light' })
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
await expect(storedSettings.cameraControls).toBe( // Verify the toast appeared
initialSettings.cameraControls await expect(
) page.getByText(`Set theme to "light" for this project`)
await expect(storedSettings.defaultDirectory).toBe( ).toBeVisible()
initialSettings.defaultDirectory // Check that the theme changed
) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
await expect(storedSettings.defaultProjectName).toBe(
initialSettings.defaultProjectName // Check that the user setting was not changed
) await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
}) })
// Onboarding tests // Onboarding tests
test('Onboarding redirects and code updating', async ({ page, context }) => { test.describe('Onboarding tests', () => {
const u = getUtils(page) // Override test setup
const storageState = structuredClone(basicStorageState)
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
settings: SaveSettingsPayload
}
s.settings.app.onboardingStatus = '/export'
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
test.use({ storageState })
// Override beforeEach test setup test('Onboarding redirects and code updating', async ({ page, context }) => {
await context.addInitScript(async () => { const u = getUtils(page)
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
const storedSettings = JSON.parse( await page.setViewportSize({ width: 1200, height: 500 })
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
) )
storedSettings.onboardingStatus = '/export'
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) // Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}) })
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}) })
test('Selections work on fresh and edited sketch', async ({ page }) => { test('Selections work on fresh and edited sketch', async ({ page }) => {
@ -779,129 +800,134 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await selectionSequence() await selectionSequence()
}) })
test('Command bar works and can change a setting', async ({ page }) => { test.describe('Command bar tests', () => {
// Brief boilerplate test('Command bar works and can change a setting', async ({ page }) => {
const u = getUtils(page) // Brief boilerplate
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/', { waitUntil: 'domcontentloaded' })
await u.waitForAuthSkipAppStart()
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
// First try opening the command bar and closing it // First try opening the command bar and closing it
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively // It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
await page await page
.getByRole('button', { name: 'Ctrl+/' }) .getByRole('button', { name: 'Ctrl+/' })
.or(page.getByRole('button', { name: '⌘K' })) .or(page.getByRole('button', { name: '⌘K' }))
.click() .click()
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible() await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await page.keyboard.type('theme')
const themeOption = page.getByRole('option', { name: 'Set Theme' }) const themeOption = page.getByRole('option', {
await expect(themeOption).toBeVisible() name: 'Settings · app · theme',
await themeOption.click() })
const themeInput = page.getByPlaceholder('system') await expect(themeOption).toBeVisible()
await expect(themeInput).toBeVisible() await themeOption.click()
await expect(themeInput).toBeFocused() const themeInput = page.getByPlaceholder('Select an option')
// Select dark theme await expect(themeInput).toBeVisible()
await page.keyboard.press('ArrowDown') await expect(themeInput).toBeFocused()
await page.keyboard.press('ArrowUp') // Select dark theme
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute( await page.keyboard.press('ArrowDown')
'data-headlessui-state', await page.keyboard.press('ArrowDown')
'active' await page.keyboard.press('ArrowDown')
) await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
await page.keyboard.press('Enter') 'data-headlessui-state',
'active'
// Check the toast appeared
await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
})
test('Can extrude from the command bar', async ({ page, context }) => {
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`
const distance = sqrt(20)
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
) )
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}) })
const u = getUtils(page) // Override test setup code
await page.setViewportSize({ width: 1200, height: 500 }) const storageState = structuredClone(basicStorageState)
await page.goto('/') storageState.origins[0].localStorage[1].value = `const distance = sqrt(20)
await u.waitForAuthSkipAppStart() const part001 = startSketchOn('-XZ')
await u.openDebugPanel() |> startProfileAt([-6.95, 4.98], %)
await u.expectCmdLog('[data-message-type="execution-done"]') |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
test.use({ storageState })
let cmdSearchBar = page.getByPlaceholder('Search commands') test('Can extrude from the command bar', async ({ page, context }) => {
await page.keyboard.press('Meta+K') const u = getUtils(page)
await expect(cmdSearchBar).toBeVisible() await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Search for extrude command and choose it // Make sure the stream is up
await page.getByRole('option', { name: 'Extrude' }).click() await u.openDebugPanel()
await expect(page.locator('#arg-form > label')).toContainText( await u.expectCmdLog('[data-message-type="execution-done"]')
'Please select one face' await u.closeDebugPanel()
)
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
// Click to select face and set distance await expect(
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() page.getByRole('button', { name: 'Start Sketch' })
await page.getByRole('button', { name: 'Continue' }).click() ).not.toBeDisabled()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await expect(
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
// Assert that we're on the distance step let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
// Assert that the an alternative variable name is chosen, // Search for extrude command and choose it
// since the default variable name is already in use (distance) await page.getByRole('option', { name: 'Extrude' }).click()
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
// Review step and argument hotkeys // Assert that we're on the distance step
await expect( await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible() // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
// Check that the code was updated // Review step and argument hotkeys
await page.keyboard.press('Enter') await expect(
// Unfortunately this indentation seems to matter for the test page.getByRole('button', { name: 'Submit command' })
await expect(page.locator('.cm-content')).toHaveText( ).toBeEnabled()
`const distance = sqrt(20) await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
// Check that the code was updated
await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20)
const distance001 = 5 + 7 const distance001 = 5 + 7
const part001 = startSketchOn('-XZ') const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %) |> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
) )
})
}) })
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
@ -1470,9 +1496,13 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByText('startProfileAt([1.03, 1.03], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel()
await page.waitForTimeout(200) await page.waitForTimeout(200)
const pointToDragFirst = [691, 237] const pointToDragFirst = [787, 565]
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1]) await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
await page.mouse.down() await page.mouse.down()
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], { await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
@ -1486,7 +1516,9 @@ test('Sketch on face', async ({ page, context }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([1.03, 1.03], %)
|> line([2.81, -0.33], %) |> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.24 : 0.2
}], %)
|> line([-4.44, -2.13], %) |> line([-4.44, -2.13], %)
|> close(%)`) |> close(%)`)
@ -1501,6 +1533,7 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible() await expect(page.getByText('Confirm Extrude')).toBeVisible()
@ -1509,7 +1542,9 @@ test('Sketch on face', async ({ page, context }) => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([1.03, 1.03], %) |> startProfileAt([1.03, 1.03], %)
|> line([2.81, -0.33], %) |> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.24 : 0.2
}], %)
|> line([-4.44, -2.13], %) |> line([-4.44, -2.13], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)`) |> extrude(5 + 7, %)`)

View File

@ -7,30 +7,18 @@ import { spawn } from 'child_process'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import JSZip from 'jszip' import JSZip from 'jszip'
import path from 'path' import path from 'path'
import { basicSettings, basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ page }) => {
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
}, secrets.token)
// reducedMotion kills animations, which speeds up tests and reduces flakiness // reducedMotion kills animations, which speeds up tests and reduces flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.use({
storageState: structuredClone(basicStorageState),
})
test.setTimeout(60_000) test.setTimeout(60_000)
test('exports of each format should work', async ({ page, context }) => { test('exports of each format should work', async ({ page, context }) => {
@ -332,6 +320,22 @@ test('extrude on each default plane should be stable', async ({
page, page,
context, context,
}) => { }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %) |> startProfileAt([7.00, 4.40], %)
@ -353,29 +357,26 @@ test('extrude on each default plane should be stable', async ({
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
await page.getByText('Code').click()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
const runSnapshotsForOtherPlanes = async (plane = 'XY') => { const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code // clear code
await u.removeCurrentCode() await u.removeCurrentCode()
// add makeCode('XZ') // add makeCode('XZ')
await u.openAndClearDebugPanel()
await page.locator('.cm-content').fill(makeCode(plane)) await page.locator('.cm-content').fill(makeCode(plane))
// wait for execution done // wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByText('Code').click() await page.getByText('Code').click()
await page.waitForTimeout(150)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.getByText('Code').click() await page.getByText('Code').click()
} }
await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY') await runSnapshotsForOtherPlanes('-XY')
await runSnapshotsForOtherPlanes('XZ') await runSnapshotsForOtherPlanes('XZ')
@ -386,22 +387,6 @@ test('extrude on each default plane should be stable', async ({
}) })
test('Draft segments should look right', async ({ page, context }) => { test('Draft segments should look right', async ({ page, context }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -460,26 +445,9 @@ test('Draft segments should look right', async ({ page, context }) => {
}) })
}) })
test('Client side scene scale should match engine scale inch', async ({ test('Client side scene scale should match engine scale - Inch', async ({
page, page,
context,
}) => { }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -512,7 +480,7 @@ test('Client side scene scale should match engine scale inch', async ({
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`) |> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -522,8 +490,8 @@ test('Client side scene scale should match engine scale inch', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %) |> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`) |> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -532,9 +500,9 @@ test('Client side scene scale should match engine scale inch', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %) |> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %) |> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`) |> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it // click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -560,102 +528,101 @@ test('Client side scene scale should match engine scale inch', async ({
}) })
}) })
test('Client side scene scale should match engine scale mm', async ({ test.describe('Client side scene scale should match engine scale - Millimeters', () => {
page, const storageState = structuredClone(basicStorageState)
context, storageState.origins[0].localStorage[2].value = TOML.stringify({
}) => { settings: {
await context.addInitScript(async () => { ...basicSettings,
localStorage.setItem( modeling: {
'SETTINGS_PERSIST_KEY', ...basicSettings.modeling,
JSON.stringify({ defaultUnit: 'mm',
baseUnit: 'mm', },
cameraControls: 'KittyCAD', },
defaultDirectory: '', })
defaultProjectName: 'project-$nnn', test.use({
onboardingStatus: 'dismissed', storageState,
showDebugPanel: true, })
textWrapping: 'On',
theme: 'dark', test('Millimeters', async ({ page }) => {
unitSystem: 'metric', const u = getUtils(page)
}) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
) )
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect( // select a plane
page.getByRole('button', { name: 'Start Sketch' }) await page.mouse.click(700, 200)
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button await expect(page.locator('.cm-content')).toHaveText(
await u.clearCommandLogs() `const part001 = startSketchOn('-XZ')`
await u.doAndWaitForImageDiff( )
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText( const startXPx = 600
`const part001 = startSketchOn('-XZ')` await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await u.closeDebugPanel()
const startXPx = 600 await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel() await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await expect(page.locator('.cm-content'))
await page.waitForTimeout(100) .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) // screen shot should show the sketch
.toHaveText(`const part001 = startSketchOn('-XZ') await expect(page).toHaveScreenshot({
|> startProfileAt([230.03, -310.32], %) maxDiffPixels: 100,
|> line([232.2, 0], %) })
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() // exit sketch
await page.waitForTimeout(100) await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// screen shot should show the sketch // wait for execution done
await expect(page).toHaveScreenshot({ await u.expectCmdLog('[data-message-type="execution-done"]')
maxDiffPixels: 100, await u.clearAndCloseDebugPanel()
}) await page.waitForTimeout(200)
// exit sketch // second screen shot should look almost identical, i.e. scale should be the same.
await u.openAndClearDebugPanel() await expect(page).toHaveScreenshot({
await page.getByRole('button', { name: 'Exit Sketch' }).click() maxDiffPixels: 100,
})
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
}) })
}) })
@ -666,14 +633,14 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
'persistCode', 'persistCode',
`const part001 = startSketchOn('-XZ') `const part001 = startSketchOn('-XZ')
|> startProfileAt([1.4, 2.47], %) |> startProfileAt([1.4, 2.47], %)
|> line({ to: [9.31, 10.55], tag: 'seg01' }, %) |> line([9.31, 10.55], %, 'seg01')
|> line([11.91, -10.42], %) |> line([11.91, -10.42], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %) |> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01') const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([-2.89, 1.82], %) |> startProfileAt([-2.89, 1.82], %)
|> line([4.68, 3.05], %) |> line([4.68, 3.05], %)
|> line({ to: [0, -7.79], tag: 'seg02' }, %) |> line([0, -7.79], %, 'seg02')
|> close(%) |> close(%)
|> extrude(5 + 7, %) |> extrude(5 + 7, %)
` `
@ -700,6 +667,4 @@ const part002 = startSketchOn(part001, 'seg01')
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.waitForTimeout(200)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,40 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { secrets } from './secrets'
import * as TOML from '@iarna/toml'
import { Themes } from 'lib/theme'
export const basicSettings = {
app: {
theme: Themes.Dark,
onboardingStatus: 'dismissed',
projectDirectory: '',
},
modeling: {
defaultUnit: 'in',
mouseControls: 'KittyCAD',
showDebugPanel: true,
},
projects: {
defaultProjectName: 'project-$nnn',
},
textEditor: {
textWrapping: true,
},
} satisfies Partial<SaveSettingsPayload>
export const basicStorageState = {
cookies: [],
origins: [
{
origin: 'http://localhost:3000',
localStorage: [
{ name: 'TOKEN_PERSIST_KEY', value: secrets.token },
{ name: 'persistCode', value: '' },
{
name: '/user.toml',
value: TOML.stringify({ settings: basicSettings }),
},
],
},
],
}

View File

@ -33,7 +33,7 @@ async function clearCommandLogs(page: Page) {
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string) {
await expect(page.locator(locatorStr)).toBeVisible() await expect(page.locator(locatorStr).last()).toBeVisible()
} }
async function waitForDefaultPlanesToBeVisible(page: Page) { async function waitForDefaultPlanesToBeVisible(page: Page) {

View File

@ -1,7 +1,10 @@
import { browser, $, expect } from '@wdio/globals' import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises' import fs from 'fs/promises'
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects` const documentsDir = `${process.env.HOME}/Documents`
const userSettingsFile = `${process.env.HOME}/.config/dev.zoo.modeling-app/user.toml`
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
const newProjectDir = `${documentsDir}/a-different-directory`
const userCodeDir = '/tmp/kittycad_user_code' const userCodeDir = '/tmp/kittycad_user_code'
async function click(element: WebdriverIO.Element): Promise<void> { async function click(element: WebdriverIO.Element): Promise<void> {
@ -10,12 +13,25 @@ async function click(element: WebdriverIO.Element): Promise<void> {
await browser.execute('arguments[0].click();', element) await browser.execute('arguments[0].click();', element)
} }
/* Shoutout to @Sheap on Github for a great workaround utility:
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
*/
async function setDatasetValue(
field: WebdriverIO.Element,
property: string,
value: string
) {
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
}
describe('ZMA (Tauri, Linux)', () => { describe('ZMA (Tauri, Linux)', () => {
it('opens the auth page and signs in', async () => { it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests // Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultDir, { force: true, recursive: true }) await fs.rm(defaultProjectDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true }) await fs.rm(userCodeDir, { force: true })
await fs.rm(userSettingsFile, { force: true })
await fs.mkdir(newProjectDir, { recursive: true })
const signInButton = await $('[data-testid="sign-in-button"]') const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in') expect(await signInButton.getText()).toEqual('Sign in')
@ -65,13 +81,25 @@ describe('ZMA (Tauri, Linux)', () => {
const settingsButton = await $('[data-testid="settings-button"]') const settingsButton = await $('[data-testid="settings-button"]')
await click(settingsButton) await click(settingsButton)
const defaultDirInput = await $('[data-testid="default-directory-input"]') const projectDirInput = await $('[data-testid="project-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir) expect(await projectDirInput.getValue()).toEqual(defaultProjectDir)
const nameInput = await $('[data-testid="name-input"]') /*
* We've set up the project directory input (in initialSettings.tsx)
* to be able to skip the folder selection dialog if data-testValue
* has a value, allowing us to test the input otherwise works.
*/
await setDatasetValue(projectDirInput, 'testValue', newProjectDir)
const projectDirButton = await $('[data-testid="project-directory-button"]')
await click(projectDirButton)
await new Promise((resolve) => setTimeout(resolve, 500))
// This line is broken. I need a different way to grab the toast
await expect(await $('div*=Set project directory to')).toBeDisplayed()
const nameInput = await $('[data-testid="projects-defaultProjectName"]')
expect(await nameInput.getValue()).toEqual('project-$nnn') expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="close-button"]') const closeButton = await $('[data-testid="settings-close-button"]')
await click(closeButton) await click(closeButton)
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.17.1", "version": "0.17.3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.15.0", "@codemirror/autocomplete": "^6.15.0",
@ -10,12 +10,18 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18", "@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@iarna/toml": "^2.2.5",
"@kittycad/lib": "^0.0.56", "@kittycad/lib": "^0.0.56",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0", "@replit/codemirror-interact": "^6.3.0",
"@tauri-apps/api": "^1.5.3", "@tauri-apps/api": "^2.0.0-beta.7",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.2",
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
@ -24,11 +30,12 @@
"@types/node": "^18.19.26", "@types/node": "^18.19.26",
"@types/react": "^18.2.73", "@types/react": "^18.2.73",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"@uiw/react-codemirror": "^4.21.24", "@uiw/react-codemirror": "^4.21.25",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2", "debounce-promise": "^3.1.2",
"decamelize": "^6.0.0",
"formik": "^2.4.3", "formik": "^2.4.3",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
@ -46,7 +53,6 @@
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.2", "swr": "^2.2.2",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"three": "^0.160.0", "three": "^0.160.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@ -84,7 +90,7 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src", "lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen", "postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"" "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
}, },
@ -108,9 +114,9 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.23.3", "@babel/preset-env": "^7.24.3",
"@playwright/test": "^1.39.0", "@playwright/test": "^1.39.0",
"@tauri-apps/cli": "^1.5.11", "@tauri-apps/cli": "^2.0.0-beta.12",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/debounce-promise": "^3.1.9", "@types/debounce-promise": "^3.1.9",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
@ -132,7 +138,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^14.3.1", "happy-dom": "^14.3.10",
"husky": "^9.0.11", "husky": "^9.0.11",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
@ -141,7 +147,7 @@
"prettier": "^2.8.0", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"vite": "^5.2.2", "vite": "^5.2.6",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test' import { defineConfig, devices } from '@playwright/test'
import { basicStorageState } from './e2e/playwright/storageStates'
/** /**
* Read environment variables from file. * Read environment variables from file.
@ -28,6 +29,9 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
/* Use a common shared localStorage */
storageState: basicStorageState,
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

2549
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@ license = ""
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app" default-run = "app"
edition = "2021" edition = "2021"
rust-version = "1.60" rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.5.1", features = [] } tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
@ -20,9 +20,14 @@ kittycad = "0.2.63"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.6.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] } tauri = { version = "2.0.0-beta", features = [ "devtools", "unstable"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-dialog = { version = "2.0.0-beta.0" }
tokio = { version = "1.36.0", features = ["time"] } tauri-plugin-fs = { version = "2.0.0-beta.0" }
tauri-plugin-http = { version = "2.0.0-beta.0" }
tauri-plugin-os = { version = "2.0.0-beta.0" }
tauri-plugin-shell = { version = "2.0.0-beta.0" }
tauri-plugin-updater = { version = "2.0.0-beta.0" }
tokio = { version = "1.37.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

@ -0,0 +1,87 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}

View File

@ -4,11 +4,15 @@
use std::env; use std::env;
use std::fs; use std::fs;
use std::io::Read; use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use oauth2::TokenResponse; use oauth2::TokenResponse;
use serde::Serialize;
use std::process::Command; use std::process::Command;
use tauri::{InvokeError, Manager}; use tauri::ipc::InvokeError;
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.kittycad.io"; const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path. /// This command returns the a json string parse from a toml file at the path.
@ -24,6 +28,56 @@ fn read_toml(path: &str) -> Result<String, InvokeError> {
Ok(value) Ok(value)
} }
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[derive(Debug, Serialize)]
pub struct DiskEntry {
/// The path to the entry.
pub path: PathBuf,
/// The name of the entry (file name with extension or directory name).
pub name: Option<String>,
/// The children of this entry if it's a directory.
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command]
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
let mut files_and_dirs: Vec<DiskEntry> = vec![];
// let path = path.as_ref();
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
let path = entry
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.path();
if let Ok(flag) = is_dir(&path) {
files_and_dirs.push(DiskEntry {
path: path.clone(),
children: if flag {
Some(read_dir_recursive(path.to_str().expect("No path"))?)
} else {
None
},
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
}
}
Ok(files_and_dirs)
}
/// This command returns a string that is the contents of a file at the path. /// This command returns a string that is the contents of a file at the path.
#[tauri::command] #[tauri::command]
fn read_txt_file(path: &str) -> Result<String, InvokeError> { fn read_txt_file(path: &str) -> Result<String, InvokeError> {
@ -85,7 +139,8 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
fs::write("/tmp/kittycad_user_code", details.user_code().secret()) fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file"); .expect("Unable to write /tmp/kittycad_user_code file");
} else { } else {
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None) app.shell()
.open(auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
} }
@ -165,12 +220,15 @@ fn show_in_folder(path: String) {
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|_app| { .setup(|_app| {
#[cfg(debug_assertions)] // only include this code on debug builds #[cfg(debug_assertions)]
{ {
let window = _app.get_window("main").unwrap(); use tauri::Manager;
// comment out the below if you don't devtools to open everytime. _app.get_webview("main").unwrap().open_devtools();
// it's useful because otherwise devtools shuts everytime rust code changes. }
window.open_devtools(); #[cfg(not(debug_assertions))]
{
_app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
} }
Ok(()) Ok(())
}) })
@ -179,9 +237,14 @@ fn main() {
login, login,
read_toml, read_toml,
read_txt_file, read_txt_file,
read_dir_recursive,
show_in_folder, show_in_folder,
]) ])
.plugin(tauri_plugin_fs_extra::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,90 +1,9 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": { "app": {
"beforeDevCommand": "yarn start",
"devPath": "http://localhost:3000",
"distDir": "../build"
},
"package": {
"productName": "zoo-modeling-app",
"version": "0.17.1"
},
"tauri": {
"allowlist": {
"all": false,
"dialog": {
"all": true,
"ask": true,
"confirm": true,
"message": true,
"open": true,
"save": true
},
"fs": {
"scope": [
"$HOME/**/*",
"$APPCONFIG",
"$APPCONFIG/**/*",
"$DOCUMENT",
"$DOCUMENT/**/*"
],
"all": true
},
"http": {
"request": true,
"scope": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os": {
"all": true
},
"shell": {
"open": true
},
"path": {
"all": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "dev.zoo.modeling-app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all"
},
"security": { "security": {
"csp": null "csp": null
}, },
"updater": {
"active": false
},
"windows": [ "windows": [
{ {
"fullscreen": false, "fullscreen": false,
@ -94,5 +13,47 @@
"width": 1800 "width": 1800
} }
] ]
} },
"build": {
"beforeDevCommand": "yarn start",
"devUrl": "http://localhost:3000",
"frontendDist": "../build"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"depends": []
}
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all"
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.17.3"
} }

View File

@ -1,6 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
}
}

View File

@ -1,6 +1,13 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": { "bundle": {
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
},
"plugins": {
"updater": { "updater": {
"active": true, "active": true,
"endpoints": [ "endpoints": [
@ -8,14 +15,6 @@
], ],
"dialog": true, "dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"bundle": {
"identifier": "io.kittycad.modeling-app",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
} }
} }
} }

View File

@ -1,6 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
}
}

View File

@ -1,6 +1,6 @@
import { useCallback, MouseEventHandler, useEffect } from 'react' import { useCallback, MouseEventHandler, useEffect } from 'react'
import { DebugPanel } from './components/DebugPanel' import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid' import { uuidv4 } from 'lib/utils'
import { PaneType, useStore } from './useStore' import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs' import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel' import { CollapsiblePanel } from './components/CollapsiblePanel'
@ -33,10 +33,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { useValidateSettings } from 'hooks/useValidateSettings' import { useRefreshSettings } from 'hooks/useRefreshSettings'
export function App() { export function App() {
useValidateSettings() useRefreshSettings(paths.FILE + 'SETTINGS')
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
@ -64,10 +64,14 @@ export function App() {
})) }))
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} const {
modeling: { showDebugPanel },
app: { theme, onboardingStatus },
} = settings.context
const { state, send } = useModelingContext() const { state, send } = useModelingContext()
const editorTheme = theme === Themes.System ? getSystemTheme() : theme const editorTheme =
theme.current === Themes.System ? getSystemTheme() : theme.current
// Pane toggling keyboard shortcuts // Pane toggling keyboard shortcuts
const togglePane = useCallback( const togglePane = useCallback(
@ -95,7 +99,7 @@ export function App() {
) )
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some( const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus (p) => p === onboardingStatus.current
) )
? 'opacity-20' ? 'opacity-20'
: didDragInStream : didDragInStream
@ -149,7 +153,7 @@ export function App() {
<ModalContainer /> <ModalContainer />
<Resizable <Resizable
className={ className={
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' + 'pointer-events-none h-full flex flex-col flex-1 z-10 my-2 ml-2 pr-1 transition-opacity transition-duration-75 ' +
+paneOpacity +paneOpacity
} }
defaultSize={{ defaultSize={{
@ -162,8 +166,8 @@ export function App() {
maxHeight={'auto'} maxHeight={'auto'}
handleClasses={{ handleClasses={{
right: right:
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' + 'hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus === 'camera' (buttonDownInStream || onboardingStatus.current === 'camera'
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto'), : 'pointer-events-auto'),
}} }}
@ -198,13 +202,13 @@ export function App() {
theme={editorTheme} theme={editorTheme}
open={openPanes.includes('kclErrors')} open={openPanes.includes('kclErrors')}
title="KCL Errors" title="KCL Errors"
iconClassNames={{ icon: 'group-open:text-destroy-30' }} iconClassNames={{ bg: 'group-open:bg-destroy-70' }}
/> />
</section> </section>
</div> </div>
</Resizable> </Resizable>
<Stream className="absolute inset-0 z-0" /> <Stream className="absolute inset-0 z-0" />
{showDebugPanel && ( {showDebugPanel.current && (
<DebugPanel <DebugPanel
title="Debug" title="Debug"
className={ className={

View File

@ -22,19 +22,18 @@ import { paths } from 'lib/paths'
import { import {
fileLoader, fileLoader,
homeLoader, homeLoader,
indexLoader,
onboardingRedirectLoader, onboardingRedirectLoader,
settingsLoader,
} from 'lib/routeLoaders' } from 'lib/routeLoaders'
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider' import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider' import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider' import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
export const BROWSER_FILE_NAME = 'new'
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
loader: indexLoader, loader: settingsLoader,
id: paths.INDEX, id: paths.INDEX,
element: ( element: (
<CommandBarProvider> <CommandBarProvider>
@ -47,14 +46,14 @@ const router = createBrowserRouter([
</KclContextProvider> </KclContextProvider>
</CommandBarProvider> </CommandBarProvider>
), ),
errorElement: <ErrorPage />,
children: [ children: [
{ {
path: paths.INDEX, path: paths.INDEX,
loader: () => loader: () =>
isTauri() isTauri()
? redirect(paths.HOME) ? redirect(paths.HOME)
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME), : redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
errorElement: <ErrorPage />,
}, },
{ {
loader: fileLoader, loader: fileLoader,
@ -67,29 +66,29 @@ const router = createBrowserRouter([
<Outlet /> <Outlet />
<App /> <App />
<CommandBar /> <CommandBar />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</FileMachineProvider> </FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth> </Auth>
), ),
children: [ children: [
{ {
loader: onboardingRedirectLoader, id: paths.FILE + 'SETTINGS',
index: true, loader: settingsLoader,
element: <></>,
},
{
children: [ children: [
{
loader: onboardingRedirectLoader,
index: true,
element: <></>,
},
{ {
path: makeUrlPathRelative(paths.SETTINGS), path: makeUrlPathRelative(paths.SETTINGS),
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
element: <Settings />, element: <Settings />,
}, },
{ {
path: makeUrlPathRelative(paths.ONBOARDING.INDEX), path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
element: <Onboarding />, element: <Onboarding />,
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
children: onboardingRoutes, children: onboardingRoutes,
}, },
], ],
@ -108,8 +107,15 @@ const router = createBrowserRouter([
id: paths.HOME, id: paths.HOME,
loader: homeLoader, loader: homeLoader,
children: [ children: [
{
index: true,
element: <></>,
id: paths.HOME + 'SETTINGS',
loader: settingsLoader,
},
{ {
path: makeUrlPathRelative(paths.SETTINGS), path: makeUrlPathRelative(paths.SETTINGS),
loader: settingsLoader,
element: <Settings />, element: <Settings />,
}, },
], ],

View File

@ -18,8 +18,12 @@ export const Toolbar = () => {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const iconClassName =
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-chalkboard-10 group-pressed:!text-chalkboard-10'
const bgClassName = const bgClassName =
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80' 'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary group-pressed:bg-primary'
const buttonClassName =
'bg-chalkboard-10 dark:bg-chalkboard-100 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100'
const pathId = useMemo(() => { const pathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) { if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
return false return false
@ -64,12 +68,14 @@ export const Toolbar = () => {
{state.nextEvents.includes('Enter sketch') && ( {state.nextEvents.includes('Enter sketch') && (
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
onClick={() => onClick={() =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } }) send({ type: 'Enter sketch', data: { forceNewSketch: true } })
} }
icon={{ icon={{
icon: 'sketch', icon: 'sketch',
iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons} disabled={disableAllButtons}
@ -81,10 +87,12 @@ export const Toolbar = () => {
{state.nextEvents.includes('Enter sketch') && pathId && ( {state.nextEvents.includes('Enter sketch') && pathId && (
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
onClick={() => send({ type: 'Enter sketch' })} onClick={() => send({ type: 'Enter sketch' })}
icon={{ icon={{
icon: 'sketch', icon: 'sketch',
iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons} disabled={disableAllButtons}
@ -96,10 +104,12 @@ export const Toolbar = () => {
{state.nextEvents.includes('Cancel') && !state.matches('idle') && ( {state.nextEvents.includes('Cancel') && !state.matches('idle') && (
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
onClick={() => send({ type: 'Cancel' })} onClick={() => send({ type: 'Cancel' })}
icon={{ icon={{
icon: 'arrowLeft', icon: 'arrowLeft',
iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons} disabled={disableAllButtons}
@ -112,6 +122,7 @@ export const Toolbar = () => {
<> <>
<li className="contents" key="line-button"> <li className="contents" key="line-button">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
onClick={() => onClick={() =>
state?.matches('Sketch.Line tool') state?.matches('Sketch.Line tool')
@ -119,9 +130,9 @@ export const Toolbar = () => {
: send('Equip Line tool') : send('Equip Line tool')
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{ icon={{
icon: 'line', icon: 'line',
iconClassName,
bgClassName, bgClassName,
}} }}
disabled={disableAllButtons} disabled={disableAllButtons}
@ -131,6 +142,7 @@ export const Toolbar = () => {
</li> </li>
<li className="contents" key="tangential-arc-button"> <li className="contents" key="tangential-arc-button">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
onClick={() => onClick={() =>
state.matches('Sketch.Tangential arc to') state.matches('Sketch.Tangential arc to')
@ -138,9 +150,9 @@ export const Toolbar = () => {
: send('Equip tangential arc to') : send('Equip tangential arc to')
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{ icon={{
icon: 'arc', icon: 'arc',
iconClassName,
bgClassName, bgClassName,
}} }}
disabled={ disabled={
@ -179,8 +191,8 @@ export const Toolbar = () => {
.map((eventName) => ( .map((eventName) => (
<li className="contents" key={eventName}> <li className="contents" key={eventName}>
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
className="text-sm"
key={eventName} key={eventName}
onClick={() => send(eventName)} onClick={() => send(eventName)}
disabled={ disabled={
@ -191,6 +203,7 @@ export const Toolbar = () => {
title={eventName} title={eventName}
icon={{ icon={{
icon: 'line', icon: 'line',
iconClassName,
bgClassName, bgClassName,
}} }}
> >
@ -203,8 +216,8 @@ export const Toolbar = () => {
{state.matches('idle') && ( {state.matches('idle') && (
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
className={buttonClassName}
Element="button" Element="button"
className="text-sm"
onClick={() => onClick={() =>
commandBarSend({ commandBarSend({
type: 'Find and select command', type: 'Find and select command',
@ -219,6 +232,7 @@ export const Toolbar = () => {
} }
icon={{ icon={{
icon: 'extrude', icon: 'extrude',
iconClassName,
bgClassName, bgClassName,
}} }}
> >
@ -231,16 +245,16 @@ export const Toolbar = () => {
} }
return ( return (
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative"> <div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0"> <menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap border-solid border border-primary/30 dark:border-chalkboard-90 border-r-0">
<ToolbarButtons /> <ToolbarButtons />
</menu> </menu>
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => commandBarSend({ type: 'Open' })} onClick={() => commandBarSend({ type: 'Open' })}
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10" className="rounded-r-full pr-4 self-stretch border-primary/30 hover:border-primary dark:border-chalkboard-80 dark:bg-chalkboard-80 text-primary"
> >
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'} {platform === 'macos' ? '⌘K' : 'Ctrl+/'}
</ActionButton> </ActionButton>
</div> </div>
) )

View File

@ -21,7 +21,7 @@ import {
Subscription, Subscription,
EngineCommandManager, EngineCommandManager,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid' import { uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'

View File

@ -4,10 +4,15 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
import { sceneInfra } from 'lib/singletons' import { sceneInfra } from 'lib/singletons'
import {
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
getParentGroup,
} from './sceneEntities'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -37,10 +42,10 @@ export const ClientSideScene = ({
}: { }: {
cameraControls: ReturnType< cameraControls: ReturnType<
typeof useSettingsAuthContext typeof useSettingsAuthContext
>['settings']['context']['cameraControls'] >['settings']['context']['modeling']['mouseControls']['current']
}) => { }) => {
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send } = useModelingContext() const { state, send, context } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene() const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({ const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange, setHighlightRange: s.setHighlightRange,
@ -76,9 +81,33 @@ export const ClientSideScene = ({
} }
}, []) }, [])
let cursor = 'default'
if (state.matches('Sketch')) {
if (
context.mouseState.type === 'isHovering' &&
getParentGroup(context.mouseState.on, [
ARROWHEAD,
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
])
) {
cursor = 'move'
} else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing'
} else if (
state.matches('Sketch.Line tool') ||
state.matches('Sketch.Tangential arc to')
) {
cursor = 'crosshair'
} else {
cursor = 'default'
}
}
return ( return (
<div <div
ref={canvasRef} ref={canvasRef}
style={{ cursor: cursor }}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${ className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100' hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${ } ${hideServer ? 'bg-black' : ''} ${

View File

@ -28,12 +28,15 @@ export function createGridHelper({
gridHelper.rotation.x = Math.PI / 2 gridHelper.rotation.x = Math.PI / 2
return gridHelper return gridHelper
} }
const fudgeFactor = 72.66985970437086
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) => export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
0.55 / cam.zoom (0.55 * fudgeFactor) / cam.zoom / window.innerHeight
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) => export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000 (group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
4000 /
window.innerHeight
export function isQuaternionVertical(q: Quaternion) { export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q) const v = new Vector3(0, 0, 1).applyQuaternion(q)

View File

@ -12,6 +12,7 @@ import {
OrthographicCamera, OrthographicCamera,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Points,
Quaternion, Quaternion,
Scene, Scene,
Shape, Shape,
@ -81,20 +82,25 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers' import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid' import { uuidv4 } from 'lib/utils'
import { SketchDetails } from 'machines/modelingMachine' import { SketchDetails } from 'machines/modelingMachine'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
export const EXTRA_SEGMENT_OFFSET_PX = 8
export const PROFILE_START = 'profile-start'
export const STRAIGHT_SEGMENT = 'straight-segment' export const STRAIGHT_SEGMENT = 'straight-segment'
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body' export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed' export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH = export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed' 'tangential-arc-to-segment-body-dashed'
export const PROFILE_START = 'profile-start' export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const SEGMENT_WIDTH_PX = 1.6
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
// This singleton Class is responsible for all of the things the user sees and interacts with. // This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements. // That mostly mean sketch elements.
@ -111,8 +117,12 @@ export class SceneEntities {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
this.scene = sceneInfra?.scene this.scene = sceneInfra?.scene
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange) sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
window.addEventListener('resize', this.onWindowResize)
} }
onWindowResize = () => {
this.onCamChange()
}
onCamChange = () => { onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -282,7 +292,6 @@ export class SceneEntities {
sketchGroup: SketchGroup sketchGroup: SketchGroup
variableDeclarationName: string variableDeclarationName: string
}> { }> {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane() this.createIntersectionPlane()
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
@ -295,7 +304,7 @@ export class SceneEntities {
}) })
const sketchGroup = sketchGroupFromPathToNode({ const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
ast: kclManager.ast, ast: maybeModdedAst,
programMemory, programMemory,
}) })
if (!Array.isArray(sketchGroup?.value)) if (!Array.isArray(sketchGroup?.value))
@ -383,6 +392,7 @@ export class SceneEntities {
pathToNode: segPathToNode, pathToNode: segPathToNode,
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
texture: sceneInfra.extraSegmentTexture,
}) })
} else { } else {
seg = straightSegment({ seg = straightSegment({
@ -393,6 +403,7 @@ export class SceneEntities {
isDraftSegment, isDraftSegment,
scale: factor, scale: factor,
callExpName, callExpName,
texture: sceneInfra.extraSegmentTexture,
}) })
} }
seg.layers.set(SKETCH_LAYER) seg.layers.set(SKETCH_LAYER)
@ -435,6 +446,7 @@ export class SceneEntities {
) => { ) => {
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false }) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,
forward, forward,
@ -442,7 +454,12 @@ export class SceneEntities {
position: origin, position: origin,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
}) })
this.setupSketchIdleCallbacks(sketchPathToNode) this.setupSketchIdleCallbacks({
forward,
up,
position: origin,
pathToNode: sketchPathToNode,
})
} }
setUpDraftSegment = async ( setUpDraftSegment = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
@ -467,19 +484,20 @@ export class SceneEntities {
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1` const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
let modifiedAst = addNewSketchLn({ const mod = addNewSketchLn({
node: kclManager.ast, node: _ast,
programMemory: kclManager.programMemory, programMemory: kclManager.programMemory,
to: [lastSeg.to[0], lastSeg.to[1]], to: [lastSeg.to[0], lastSeg.to[1]],
from: [lastSeg.to[0], lastSeg.to[1]], from: [lastSeg.to[0], lastSeg.to[1]],
fnName: segmentName, fnName: segmentName,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
}).modifiedAst })
modifiedAst = parse(recast(modifiedAst)) const modifiedAst = parse(recast(mod.modifiedAst))
const draftExpressionsIndices = { start: index, end: index } const draftExpressionsIndices = { start: index, end: index }
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } = const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,
@ -546,13 +564,104 @@ export class SceneEntities {
}, },
}) })
}, },
...mouseEnterLeaveCallbacks(), ...this.mouseEnterLeaveCallbacks(),
}) })
} }
setupSketchIdleCallbacks = (pathToNode: PathToNode) => { setupSketchIdleCallbacks = ({
pathToNode,
up,
forward,
position,
}: {
pathToNode: PathToNode
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
}) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => { onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
// setting up the callbacks again resets value in closures
this.setupSketchIdleCallbacks({
pathToNode,
up,
forward,
position,
})
}
},
onDrag: async ({
selected,
intersectionPoint,
mouseEvent,
intersects,
}) => {
if (mouseEvent.which !== 1) return if (mouseEvent.which !== 1) return
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
if (group?.name === EXTRA_SEGMENT_HANDLE) {
const segGroup = getParentGroup(selected)
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
const pathToNodeIndex = pathToNode.findIndex(
(x) => x[1] === 'PipeExpression'
)
const sketchGroup = sketchGroupFromPathToNode({
pathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketchGroup.value[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: [prevSegment.from[0], prevSegment.from[1]],
// TODO assuming it's always a straight segments being added
// as this is easiest, and we'll need to add "tabbing" behavior
// to support other segment types
fnName: 'line',
pathToNode: pathToNode,
spliceBetween: true,
})
addingNewSegmentStatus = 'pending'
await kclManager.executeAstMock(mod.modifiedAst, {
updates: 'code',
})
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
addingNewSegmentStatus = 'added'
} else if (addingNewSegmentStatus === 'added') {
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
this.onDragSegment({
sketchPathToNode: pathToNodeForNewSegment,
object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
})
}
return
}
this.onDragSegment({ this.onDragSegment({
object: selected, object: selected,
intersection2d: intersectionPoint.twoD, intersection2d: intersectionPoint.twoD,
@ -577,7 +686,7 @@ export class SceneEntities {
if (!event) return if (!event) return
sceneInfra.modelingSend(event) sceneInfra.modelingSend(event)
}, },
...mouseEnterLeaveCallbacks(), ...this.mouseEnterLeaveCallbacks(),
}) })
} }
prepareTruncatedMemoryAndAst = ( prepareTruncatedMemoryAndAst = (
@ -755,8 +864,7 @@ export class SceneEntities {
group.userData.to = to group.userData.to = to
group.userData.prevSegment = prevSegment group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
arrowGroup.position.set(to[0], to[1], 0)
const previousPoint = const previousPoint =
prevSegment?.type === 'TangentialArcTo' prevSegment?.type === 'TangentialArcTo'
@ -774,13 +882,49 @@ export class SceneEntities {
obtuse: true, obtuse: true,
}) })
const arrowheadAngle = const pxLength = arcInfo.arcLength / scale
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1) const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.quaternion.setFromUnitVectors( const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) const hoveredParent =
) sceneInfra.hoveredObject &&
arrowGroup.scale.set(scale, scale, scale) getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0)
const arrowheadAngle =
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
if (extraSegmentGroup) {
const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle =
arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * arcInfo.radius,
Math.sin(extraSegmentAngle) * arcInfo.radius
)
extraSegmentGroup.position.set(
arcInfo.center[0] + extraSegmentOffset.x,
arcInfo.center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
}
const tangentialArcToSegmentBody = group.children.find( const tangentialArcToSegmentBody = group.children.find(
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY (child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
@ -827,10 +971,26 @@ export class SceneEntities {
group.userData.from = from group.userData.from = from
group.userData.to = to group.userData.to = to
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, -0.08 * scale) shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, 0.08 * scale) // The width of the line shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const pxLength = length / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [STRAIGHT_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) { if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
@ -842,6 +1002,21 @@ export class SceneEntities {
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
} }
const straightSegmentBody = group.children.find( const straightSegmentBody = group.children.find(
@ -1019,6 +1194,119 @@ export class SceneEntities {
}, },
}) })
} }
mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = dragSelected ? 0 : 1
}
})
}
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
}
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = 0
}
})
}
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}
} }
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ' export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
@ -1160,7 +1448,7 @@ function colorSegment(object: any, color: number) {
]) ])
if (straightSegmentBody) { if (straightSegmentBody) {
straightSegmentBody.traverse((child) => { straightSegmentBody.traverse((child) => {
if (child instanceof Mesh) { if (child instanceof Mesh && !child.userData.ignoreColorChange) {
child.material.color.set(color) child.material.color.set(color)
} }
}) })
@ -1261,53 +1549,3 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2]) ? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z) : new Vector3(a.x, a.y, a.z)
} }
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -18,6 +18,8 @@ import {
Intersection, Intersection,
Object3D, Object3D,
Object3DEventMap, Object3DEventMap,
TextureLoader,
Texture,
} from 'three' } from 'three'
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -25,9 +27,10 @@ import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm' import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections' import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes' import { type BaseUnit } from 'lib/settings/settingsTypes'
import { SETTINGS_PERSIST_KEY } from 'lib/constants'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { settings } from 'lib/settings/initialSettings'
import { MouseState } from 'machines/modelingMachine'
type SendType = ReturnType<typeof useModelingContext>['send'] type SendType = ReturnType<typeof useModelingContext>['send']
@ -54,6 +57,7 @@ export const ARROWHEAD = 'arrowhead'
export interface OnMouseEnterLeaveArgs { export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap> selected: Object3D<Object3DEventMap>
dragSelected?: Object3D<Object3DEventMap>
mouseEvent: MouseEvent mouseEvent: MouseEvent
} }
@ -98,18 +102,26 @@ export class SceneInfra {
isFovAnimationInProgress = false isFovAnimationInProgress = false
_baseUnit: BaseUnit = 'mm' _baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1 _baseUnitMultiplier = 1
extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' }
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {} onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {} onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {} onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {} onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
setCallbacks = (callbacks: { setCallbacks = (callbacks: {
onDragStart?: (arg: OnDragCallbackArgs) => void
onDragEnd?: (arg: OnDragCallbackArgs) => void
onDrag?: (arg: OnDragCallbackArgs) => void onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: OnMoveCallbackArgs) => void onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg: OnClickCallbackArgs) => void onClick?: (arg: OnClickCallbackArgs) => void
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => { }) => {
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
this.onDragCallback = callbacks.onDrag || this.onDragCallback this.onDragCallback = callbacks.onDrag || this.onDragCallback
this.onMoveCallback = callbacks.onMove || this.onMoveCallback this.onMoveCallback = callbacks.onMove || this.onMoveCallback
this.onClickCallback = callbacks.onClick || this.onClickCallback this.onClickCallback = callbacks.onClick || this.onClickCallback
@ -128,6 +140,8 @@ export class SceneInfra {
} }
resetMouseListeners = () => { resetMouseListeners = () => {
this.setCallbacks({ this.setCallbacks({
onDragStart: () => {},
onDragEnd: () => {},
onDrag: () => {}, onDrag: () => {},
onMove: () => {}, onMove: () => {},
onClick: () => {}, onClick: () => {},
@ -170,9 +184,7 @@ export class SceneInfra {
// CAMERA // CAMERA
const camHeightDistanceRatio = 0.5 const camHeightDistanceRatio = 0.5
const baseUnit: BaseUnit = const baseUnit: BaseUnit = settings.modeling.defaultUnit.current
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || ('{}' as any))
.baseUnit || 'mm'
const baseRadius = 5.6 const baseRadius = 5.6
const length = baseUnitTomm(baseUnit) * baseRadius const length = baseUnitTomm(baseUnit) * baseRadius
const ang = Math.atan(camHeightDistanceRatio) const ang = Math.atan(camHeightDistanceRatio)
@ -212,6 +224,13 @@ export class SceneInfra {
const light = new AmbientLight(0x505050) // soft white light const light = new AmbientLight(0x505050) // soft white light
this.scene.add(light) this.scene.add(light)
const textureLoader = new TextureLoader()
this.extraSegmentTexture = textureLoader.load(
'/clientSideSceneAssets/extra-segment-texture.png'
)
this.extraSegmentTexture.anisotropy =
this.renderer?.capabilities?.getMaxAnisotropy?.()
SceneInfra.instance = this SceneInfra.instance = this
} }
@ -321,8 +340,6 @@ export class SceneInfra {
planeIntersectPoint.twoD && planeIntersectPoint.twoD &&
planeIntersectPoint.threeD planeIntersectPoint.threeD
) { ) {
// // console.log('onDrag', this.selected)
this.onDragCallback({ this.onDragCallback({
mouseEvent, mouseEvent,
intersectionPoint: { intersectionPoint: {
@ -332,6 +349,10 @@ export class SceneInfra {
intersects, intersects,
selected: this.selected.object, selected: this.selected.object,
}) })
this.updateMouseState({
type: 'isDragging',
on: this.selected.object,
})
} }
} else if ( } else if (
planeIntersectPoint && planeIntersectPoint &&
@ -351,25 +372,34 @@ export class SceneInfra {
if (intersects[0]) { if (intersects[0]) {
const firstIntersectObject = intersects[0].object const firstIntersectObject = intersects[0].object
if (this.hoveredObject !== firstIntersectObject) { if (this.hoveredObject !== firstIntersectObject) {
if (this.hoveredObject) { const hoveredObj = this.hoveredObject
this.onMouseLeave({ this.hoveredObject = null
selected: this.hoveredObject, this.onMouseLeave({
mouseEvent: mouseEvent, selected: hoveredObj,
}) mouseEvent: mouseEvent,
} })
this.hoveredObject = firstIntersectObject this.hoveredObject = firstIntersectObject
this.onMouseEnter({ this.onMouseEnter({
selected: this.hoveredObject, selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent, mouseEvent: mouseEvent,
}) })
if (!this.selected)
this.updateMouseState({
type: 'isHovering',
on: this.hoveredObject,
})
} }
} else { } else {
if (this.hoveredObject) { if (this.hoveredObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
this.onMouseLeave({ this.onMouseLeave({
selected: this.hoveredObject, selected: hoveredObj,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent, mouseEvent: mouseEvent,
}) })
this.hoveredObject = null if (!this.selected) this.updateMouseState({ type: 'idle' })
} }
} }
} }
@ -426,6 +456,11 @@ export class SceneInfra {
(a, b) => a.distance - b.distance (a, b) => a.distance - b.distance
) )
} }
updateMouseState(mouseState: MouseState) {
if (this.lastMouseState.type === mouseState.type) return
this.lastMouseState = mouseState
this.modelingSend({ type: 'Set mouse state', data: mouseState })
}
onMouseDown = (event: MouseEvent) => { onMouseDown = (event: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1 this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
@ -455,8 +490,26 @@ export class SceneInfra {
if (this.selected) { if (this.selected) {
if (this.selected.hasBeenDragged) { if (this.selected.hasBeenDragged) {
// this is where we could fire a onDragEnd event // TODO do the types properly here
// console.log('onDragEnd', this.selected) this.onDragEndCallback({
intersectionPoint: {
twoD: planeIntersectPoint?.twoD as any,
threeD: planeIntersectPoint?.threeD as any,
},
intersects,
mouseEvent,
selected: this.selected as any,
})
if (intersects.length) {
this.updateMouseState({
type: 'isHovering',
on: intersects[0].object,
})
} else {
this.updateMouseState({
type: 'idle',
})
}
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) { } else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
// fire onClick event as there was no drags // fire onClick event as there was no drags
this.onClickCallback({ this.onClickCallback({

View File

@ -12,15 +12,22 @@ import {
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
NormalBufferAttributes, NormalBufferAttributes,
Points,
PointsMaterial,
Shape, Shape,
SphereGeometry, SphereGeometry,
Texture,
Vector2, Vector2,
Vector3, Vector3,
} from 'three' } from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm' import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import { import {
EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX,
HIDE_SEGMENT_LENGTH,
PROFILE_START, PROFILE_START,
SEGMENT_WIDTH_PX,
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH, STRAIGHT_SEGMENT_DASH,
@ -44,7 +51,7 @@ export function profileStart({
}) { }) {
const group = new Group() const group = new Group()
const geometry = new BoxGeometry(0.8, 0.8, 0.8) const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const body = new MeshBasicMaterial({ color: 0xffffff }) const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body) const mesh = new Mesh(geometry, body)
@ -71,6 +78,7 @@ export function straightSegment({
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
callExpName, callExpName,
texture,
}: { }: {
from: Coords2d from: Coords2d
to: Coords2d to: Coords2d
@ -79,12 +87,13 @@ export function straightSegment({
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
callExpName: string callExpName: string
texture: Texture
}): Group { }): Group {
const group = new Group() const group = new Group()
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, -0.08 * scale) shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
let geometry let geometry
if (isDraftSegment) { if (isDraftSegment) {
@ -122,24 +131,44 @@ export function straightSegment({
} }
group.name = STRAIGHT_SEGMENT group.name = STRAIGHT_SEGMENT
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const arrowGroup = createArrowhead(scale) const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3() const dir = new Vector3()
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0)) .subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
.normalize() .normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
const pxLength = length / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
group.add(mesh) group.add(mesh)
if (callExpName !== 'close') group.add(arrowGroup) if (callExpName !== 'close') group.add(arrowGroup)
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(extraSegmentGroup)
return group return group
} }
function createArrowhead(scale = 1): Group { function createArrowhead(scale = 1): Group {
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff }) const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial) // specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px)
arrowheadMesh.position.set(0, -0.6, 0) // we'll scale the group to the correct size later to match these sizes in screen space
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial) const arrowheadMesh = new Mesh(new ConeGeometry(4.5, 20, 12), arrowMaterial)
arrowheadMesh.position.set(0, -9, 0)
const sphereMesh = new Mesh(new SphereGeometry(4, 12, 12), arrowMaterial)
const arrowGroup = new Group() const arrowGroup = new Group()
arrowGroup.userData.type = ARROWHEAD arrowGroup.userData.type = ARROWHEAD
@ -150,6 +179,36 @@ function createArrowhead(scale = 1): Group {
return arrowGroup return arrowGroup
} }
function createExtraSegmentHandle(scale: number, texture: Texture): Group {
const particleMaterial = new PointsMaterial({
size: 12, // in pixels
map: texture,
transparent: true,
opacity: 0,
depthTest: false,
})
const mat = new MeshBasicMaterial({
transparent: true,
color: 0xffffff,
opacity: 0,
})
const particleGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
])
const sphereMesh = new Mesh(new SphereGeometry(6, 12, 12), mat) // sphere radius in pixels
const particle = new Points(particleGeometry, particleMaterial)
particle.userData.ignoreColorChange = true
particle.userData.type = EXTRA_SEGMENT_HANDLE
const extraSegmentGroup = new Group()
extraSegmentGroup.userData.type = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.name = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.add(sphereMesh)
extraSegmentGroup.add(particle)
extraSegmentGroup.scale.set(scale, scale, scale)
return extraSegmentGroup
}
export function tangentialArcToSegment({ export function tangentialArcToSegment({
prevSegment, prevSegment,
from, from,
@ -158,6 +217,7 @@ export function tangentialArcToSegment({
pathToNode, pathToNode,
isDraftSegment, isDraftSegment,
scale = 1, scale = 1,
texture,
}: { }: {
prevSegment: SketchGroup['value'][number] prevSegment: SketchGroup['value'][number]
from: Coords2d from: Coords2d
@ -166,6 +226,7 @@ export function tangentialArcToSegment({
pathToNode: PathToNode pathToNode: PathToNode
isDraftSegment?: boolean isDraftSegment?: boolean
scale?: number scale?: number
texture: Texture
}): Group { }): Group {
const group = new Group() const group = new Group()
@ -178,12 +239,13 @@ export function tangentialArcToSegment({
) )
: prevSegment.from : prevSegment.from
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({ const { center, radius, startAngle, endAngle, ccw, arcLength } =
arcStartPoint: from, getTangentialArcToInfo({
arcEndPoint: to, arcStartPoint: from,
tanPreviousPoint: previousPoint, arcEndPoint: to,
obtuse: true, tanPreviousPoint: previousPoint,
}) obtuse: true,
})
const geometry = createArcGeometry({ const geometry = createArcGeometry({
center, center,
@ -219,8 +281,28 @@ export function tangentialArcToSegment({
new Vector3(0, 1, 0), new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0) new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
) )
const pxLength = arcLength / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
group.add(mesh, arrowGroup) const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
const circumferenceInPx = (2 * Math.PI * radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle = startAngle + (ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * radius,
Math.sin(extraSegmentAngle) * radius
)
extraSegmentGroup.position.set(
center[0] + extraSegmentOffset.x,
center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(mesh, arrowGroup, extraSegmentGroup)
return group return group
} }
@ -242,8 +324,8 @@ export function createArcGeometry({
isDashed?: boolean isDashed?: boolean
scale?: number scale?: number
}): BufferGeometry { }): BufferGeometry {
const dashSize = 1.2 * scale const dashSizePx = 18 * scale
const gapSize = 1.2 * scale const gapSizePx = 18 * scale
const arcStart = new EllipseCurve( const arcStart = new EllipseCurve(
center[0], center[0],
center[1], center[1],
@ -265,8 +347,8 @@ export function createArcGeometry({
0 0
) )
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, -0.08 * scale) shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
if (!isDashed) { if (!isDashed) {
const points = arcStart.getPoints(50) const points = arcStart.getPoints(50)
@ -281,7 +363,7 @@ export function createArcGeometry({
} }
const length = arcStart.getLength() const length = arcStart.getLength()
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place const totalDashes = length / (dashSizePx + gapSizePx) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = [] const dashGeometries = []
@ -289,8 +371,8 @@ export function createArcGeometry({
// Function to create a dash at a specific t value (0 to 1 along the curve) // Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = (t: number, curve: EllipseCurve) => { const createDashAt = (t: number, curve: EllipseCurve) => {
const startVec = curve.getPoint(t) const startVec = curve.getPoint(t)
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length)) const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2)) const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2))
const dashCurve = new CurvePath<Vector3>() const dashCurve = new CurvePath<Vector3>()
dashCurve.add( dashCurve.add(
new CatmullRomCurve3([ new CatmullRomCurve3([
@ -314,7 +396,8 @@ export function createArcGeometry({
} }
// fill in the remaining arc // fill in the remaining arc
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize) const remainingArcLength =
length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx)
if (remainingArcLength > 0) { if (remainingArcLength > 0) {
const remainingArcStartT = dashesAtEachEnd / totalDashes const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT const remainingArcEndT = 1 - remainingArcStartT
@ -359,8 +442,8 @@ export function dashedStraight(
shape: Shape, shape: Shape,
scale = 1 scale = 1
): BufferGeometry<NormalBufferAttributes> { ): BufferGeometry<NormalBufferAttributes> {
const dashSize = 1.2 * scale const dashSize = 18 * scale
const gapSize = 1.2 * scale // todo: gabSize is not respected const gapSize = 18 * scale // TODO: gapSize is not respected
const dashLine = new LineCurve3( const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0), new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0) new Vector3(to[0], to[1], 0)

View File

@ -1,11 +1,21 @@
:root { :root {
--primary-hue: 264.48;
--primary-chroma: 0.2167;
--primary-lightness: 60%;
--_primary: var(--primary-lightness) var(--primary-chroma)
var(--primary-hue, 264.48);
--primary: oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue, 264.48) /
var(--opacity, 1)
);
/* /*
Generated using Catmosphere Theme Builder Generated using Catmosphere Theme Builder
by KittyCAD by KittyCAD
https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D
*/ */
/* Chalkboard */ /* Chalkboard */
--chalkboard-10: oklch(99.7% 0.008766 102.8deg); --chalkboard-10: oklch(99.9% 0.003766 102.8deg);
--chalkboard-20: oklch(91.34% 0.009353 109deg); --chalkboard-20: oklch(91.34% 0.009353 109deg);
--chalkboard-30: oklch(82.99% 0.00994 115.2deg); --chalkboard-30: oklch(82.99% 0.00994 115.2deg);
--chalkboard-40: oklch(74.63% 0.01053 121.4deg); --chalkboard-40: oklch(74.63% 0.01053 121.4deg);

View File

@ -30,9 +30,9 @@ export const ActionIcon = ({
children, children,
}: ActionIconProps) => { }: ActionIconProps) => {
// By default, we reverse the icon color and background color in dark mode // By default, we reverse the icon color and background color in dark mode
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}` const computedIconClassName = `h-auto text-primary dark:text-current !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}` const computedBgClassName = `bg-primary/10 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
return ( return (
<div <div

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