Compare commits

...

66 Commits

Author SHA1 Message Date
1beb6b5186 Cut release v0.22.3 (#2729) 2024-06-21 13:17:14 +10:00
17978ab1d7 Reset code on critical onboarding steps (#2727)
* Make sure we always reset the code on important steps no matter what the user did to it

* Convert comments in codeManager to JSDoc comments so they appear in diagnostics

* Was using the wrong codeManager callback

* Make sure editorView is available before resetting code

* Add Playwright test that shows the code being reset

* Fix up text that looks like linksÏ

* fmt

* Skip test on MacOS, make test more reliable on Chrome

* Update cargo-clippy to run based on paths on PRs as well

* playw fix

* try keep reports

* add fix me

* try one last thing

* fmt

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-06-21 11:39:01 +10:00
a1bcad9dfb Implement Core Dump for modeling app state (#2653) 2024-06-20 19:36:28 -04:00
2e7bdf02cf Franknoirot/onboarding avatar text (#2726)
* Add failing playwright test

* Fix the problem to get the test passing

* Give the avatar button a tooltip too
2024-06-20 14:06:11 -04:00
6f76196b72 pin ts-rs to release (#2725)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-20 10:55:53 -07:00
e7af064518 Fix onboarding example code loading (#2723)
* Add Playwright test to verify that example code loads

* Just let the loaded code be null if it's null
2024-06-20 12:07:21 -04:00
674d49e2ae fix clear diagnostics when not wasm (#2715)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-19 19:45:55 -07:00
4cb48674c6 add a feature flag to disable printlns in kcl-lib for the lsp (#2712)
* updates

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

* updates

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

* updates

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

* updates

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

* cleanup weird printlns

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

* updates

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

* check

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

* rename file

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-19 19:38:56 -07:00
82daec2aff more pyo3 methods (#2711)
more pyo3

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-19 18:04:56 -07:00
f1ef9d5200 add pyo3 as a feature flag for python bindings (#2710)
* updates

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

* thing

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

* add feature flag for pyo3 for gregs stuff;

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

* add more

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-19 17:32:08 -07:00
dc226d3270 Disable SSAO temporarily (#2709) 2024-06-19 18:54:22 -04:00
7bf50d8fe0 get responses back from batch (#2687)
* updates

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

* updates

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

* updates

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

* fixes

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

* remove my stupid println

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

* updates

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

* weird typescript

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

* better batch stuff;

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

* updates

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

* ckeanup

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

* fixes

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

* updates

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

* fixes

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

* typpo

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

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

* batch more

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

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

* thing

* updates

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

* up[dates

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

* updates

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

* fixes

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

* updates

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

* fix tests

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

* fixces

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

* cleanups

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

* images

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

* fixes

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

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

* empty

* cleanups

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

* console log all the things

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

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

* fixups

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

* updates

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

* console log cleanup

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

* fixes

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

* nicer types

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

* updates

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

* remove logs

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

* remove logs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-19 13:57:50 -07:00
b26764bc9a Clear the AST if there was a parse error. (#2706)
Clear the AST if there was a parse error.

This leads to an unfortunate loop (good -> invalid -> original) that
wouldn't clear the diagnostics from the invalid step.
2024-06-19 16:15:22 -04:00
1b0c6298d7 Revert "Bump dashmap from 5.5.3 to 6.0.0 in /src/wasm-lib" (#2707)
Revert "Bump dashmap from 5.5.3 to 6.0.0 in /src/wasm-lib (#2704)"

This reverts commit bd42ea037b.
2024-06-19 12:19:17 -07:00
fe9a483726 Bump url from 2.5.1 to 2.5.2 in /src/wasm-lib (#2705)
Bumps [url](https://github.com/servo/rust-url) from 2.5.1 to 2.5.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.1...v2.5.2)

---
updated-dependencies:
- dependency-name: url
  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-06-18 22:51:07 -07:00
bd42ea037b Bump dashmap from 5.5.3 to 6.0.0 in /src/wasm-lib (#2704)
Bumps [dashmap](https://github.com/xacrimon/dashmap) from 5.5.3 to 6.0.0.
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v.5.5.3...v6.0.0)

---
updated-dependencies:
- dependency-name: dashmap
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 20:46:12 -07:00
fdb1b21af3 Bump dawidd6/action-download-artifact from 5 to 6 (#2649)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 5 to 6.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 19:32:59 -07:00
630ef316b8 Bump serde_tokenstream from 0.2.0 to 0.2.1 in /src/wasm-lib (#2648)
Bumps [serde_tokenstream](https://github.com/oxidecomputer/serde_tokenstream) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/oxidecomputer/serde_tokenstream/releases)
- [Commits](https://github.com/oxidecomputer/serde_tokenstream/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: serde_tokenstream
  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-06-18 19:32:50 -07:00
e322926be9 Bump url from 2.5.0 to 2.5.1 in /src/wasm-lib (#2644)
Bumps [url](https://github.com/servo/rust-url) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.0...v2.5.1)

---
updated-dependencies:
- dependency-name: url
  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-06-18 19:31:21 -07:00
a9e61da8b5 Recast bug fix (#2703)
* fix gregs recast bug

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-18 18:48:30 -07:00
e2a835a437 rename radius to length for chamfer; (#2702)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-18 18:33:57 -07:00
c61273085f KCL execution server (#2686)
Adds a new library, the kcl-test-server. It lets you easily start a HTTP server with one endpoint, which accepts JSON. The JSON body contains a KCL program and a test name. The server has a pool of active engine sessions, and when it gets a KCL program, it executes it on one of those engine sessions.

This addresses part of #2580 but currently the sketch-on-face tests don't pass with this new test server yet.

This is a library, not a binary, because I want to use it in both the wasm-lib unit tests and in the zoo CLI.
2024-06-18 14:38:25 -05:00
a79e365c0f Slight tauri e2e cleanup (#2659)
* WIP: Break the tauri e2e tests apart
Will fix #2658

* Clean up

* Longer before timeout

* Also exclude tauri tests from vitest

* Utils fn back in app.spec.ts

* Remove utils

* Change before back to it

* Remove explicit mocha dep

* Revert other attemps at fixing the browser issues. mocha dep was the issue

* Clean up

* Signin/out sep with auth flows

* Lint

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-06-18 14:01:39 -04:00
2386ba24e5 Better styling for pane and KCL editor focus (#2691)
* Pane styling first steps

* More style tweaks

* Make pane background nearly opaque when focus is within them
2024-06-18 12:42:47 -04:00
e42a891df8 Add nix flake and direnv config (#2694) 2024-06-18 11:32:08 -04:00
98200565bf Add a dismiss button to the command bar (#2647)
* Remove tab hotkey from selection input

* Add dismiss button to the command bar

* update Cargo.lock

* tweak close button styles

* Switch from padding to margin for positioning without messing up focus outline

* Revert "update Cargo.lock"

This reverts commit 862a6897ba.

* Restore Cargo.lock I hate VSCode sometimes

* Update Cargo.lock in src-tauri, fix clippy

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-06-18 09:06:46 -04:00
570fd827ed fix zoom issues with sketch mode (#2664)
* cam stuff start

* more progress

* mostly done

* fix snapshot tests

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

* fix

* fix ubuntu

* more tweaks fixes

* add test

* more FE fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-18 16:08:41 +10:00
0add26cf61 Cut release v0.22.2 (#2685) 2024-06-17 17:44:30 -04:00
b54fc534c2 Patterning a pattern should always work (#2680)
* patterning a pattern should alwayus work

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

* add images;

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

* std lib

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

* bu,mp

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

* fix tests

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

* update lock

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

* bump

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

* fixes

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

* fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-17 13:35:44 -07:00
c66f851a3f add shell (#2683)
* add shell

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

* add shell

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-17 13:10:40 -07:00
13b8ab71d8 Bump tokio-tungstenite from 0.23.0 to 0.23.1 in /src/wasm-lib (#2663)
Bumps [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) from 0.23.0 to 0.23.1.
- [Changelog](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/snapview/tokio-tungstenite/commits)

---
updated-dependencies:
- dependency-name: tokio-tungstenite
  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-06-17 14:36:56 -05:00
bdeab4f87d Bump clap from 4.5.4 to 4.5.7 in /src/wasm-lib (#2643)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.4 to 4.5.7.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.4...v4.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 14:16:10 -05:00
05ccf5e2f4 Chamfer is just a fancy fillet so easy to add (#2681)
* add chamfer

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

* generate docs

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

* generate docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-17 12:13:19 -07:00
7ab015d783 Ensure settings are persisted before we navigate for onboarding dismissal (#2678)
* Nicer types on OnboardingPaths

* Update persistSettings to be a service
2024-06-17 15:10:30 -04:00
3d6cfa980f Release kcl-lib 0.1.59 (#2679) 2024-06-17 13:29:32 -05:00
9f5f1eb8c3 Bump kittycad libs (#2665)
* Bump kittycad.rs lib

* Bump kittycad.ts lib

* Update cargo.lock again

* Bump lib again, and fix fillet typing

* Update kittycad.rs to v0.3.5

* Revert schemars to v0.8.17

* Update to kcl spec
2024-06-17 18:01:45 +00:00
50fcdff879 Prevent stale Cargo.lock (#2652)
Sometimes the `src-tauri/` project gets out of date Cargo.lock. This
adds a CI check to prevent it.

This can happen because `src-tauri` is a separate Cargo project from
`src/wasm-lib`, but the former includes the latter as a dependency. So,
when wasm-lib updates a dep (e.g. bump databake from 1.7 to 1.8), the
former will, upon recompilation, pull in the newer databake dep. But
programmers in the wasm-lib repo don't usually work in the src-tauri repo
and so the src-tauri repo doesn't get updated.
2024-06-14 11:48:31 +02:00
efaae2b193 Bump bson from 2.10.0 to 2.11.0 in /src/wasm-lib (#2614)
Bumps [bson](https://github.com/mongodb/bson-rust) from 2.10.0 to 2.11.0.
- [Release notes](https://github.com/mongodb/bson-rust/releases)
- [Commits](https://github.com/mongodb/bson-rust/compare/v2.10.0...v2.11.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>
2024-06-13 15:42:47 -04:00
7e4ebacb72 Stale Cargo.lock, clippy 1.79 fixes (#2651)
* Update Cargo.lock in src-tauri, fix clippy

* Update circular pattern 3d test
2024-06-13 15:42:21 -04:00
72482506c3 add lint playwright test (#2646)
add lint test

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-06-11 17:05:25 -07:00
a51b5b09a3 Add in a prototype KCL linter (#2521)
* Add in a prototype KCL linter

This is a fork-and-replce of an experimental project I hacked up called
"kcl-vet", which was mostly the same code. This integrates kcl-vet into
the kcl_lib crate, which will let us use this from the zoo cli, as well
as via wasm in the lsp. this contains the intial integration with the
lsp, adding all lints as informational to start.

I need to go back and clean some of this up (and merge some of this back
into other parts of kcl_lib); but this has some pretty good progress
already.

Co-authored-by: jess@zoo.dev
Signed-off-by: Paul R. Tagliamonte <paul@zoo.dev>

* ty clippy :)

* add in a lint test

* add in some docstrings

* whoops

* sigh

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

* uno reverse card

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

* wtf stop it robot fuck

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

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

This reverts commit 5b18f3c035.

* hurm

* try harder to type slower

* try harder? this all passes locally.

* try this now

* simplify, add debugging for trace

* fix enter use

* re-order again

* reorder a bit more

* enter

* ok fine no other enters?

* nerd

* wip

* move control of clearing to typescript

* move result out

* err check

* remove log

* remove clear

* remove add to diag

* THERE CAN BE ONLY ONE

* _err

* dedupe

* Revert "dedupe"

This reverts commit f66de88200.

* attempt to dedupe

* clear diagnostics on mock execute, too

* handle dupe diagnostics

* fmt

* dedupe tsc

* == vs ===

* fix dedupe

* return this to the wasm for now

* clear the map every go around

this is different than the old code isnce it won't republish

---------

Signed-off-by: Paul R. Tagliamonte <paul@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-11 16:23:35 -07:00
53ccc1ed6c Swap out icons for bug and refresh, tweak tooltip appearance (#2641)
* add bug icon, swap out refresh icon

* remove lame theme color outline from tooltips

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-11 14:37:15 -04:00
8106749ccf Bump databake from 0.1.7 to 0.1.8 in /src/wasm-lib (#2545)
Bumps [databake](https://github.com/unicode-org/icu4x) from 0.1.7 to 0.1.8.
- [Release notes](https://github.com/unicode-org/icu4x/releases)
- [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unicode-org/icu4x/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-06-11 07:49:30 -05:00
081e34a600 Bump regex from 1.10.4 to 1.10.5 in /src/wasm-lib (#2637)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.4 to 1.10.5.
- [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.4...1.10.5)

---
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-06-10 17:29:24 -07:00
541400f4be Cut release v0.22.1 (#2634) 2024-06-07 14:49:29 -04:00
39d249030d remove more page.clicks (#2630)
* remove more page.clicks

* fmt
2024-06-07 12:31:22 +10:00
f8a69fac73 Remove page.clicks from test utils (#2629)
* remove page.clicks from test utils

* timeout
2024-06-07 00:48:42 +00:00
24f4bf160f Add a right-click (or "context") menu to file tree and gizmo (#2628)
* Basic context menu components

* Working context menu!

* Show keyboard shortcuts in file tree context menu

* Add context menu to Gizmo

* Little polish on components

* Add a test for the gizmo, firm up other gizmo tests

* Updated Cargo lock

* Updated Cargo.lock
2024-06-06 19:56:46 -04:00
8011594e24 Fix most trackpad zoom jank (#2613)
* Remove zoom throttling

And use the mouse zoom for sketch mode

* test tweaks

* test tweak

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-06-07 09:48:54 +10:00
0e09affb8f Remove debug logging from Engine Connection (#2623)
* Remove debug logging from Engine Connection

Left console.log('connectionstatechange: ' + event.target?.connectionState) intentionally

* Bring that beat back

@lf94 request that we keep this one and also make sure it's in coredump.
2024-06-07 07:16:55 +10:00
197a47346a Refactor: Break functions into smaller functions (#2622)
* Factor ExecutionCtx into its own fn

* Add hyper for tests

* Further factor out functions
2024-06-07 07:01:41 +10:00
9d083710e0 Bump actions/cache from 3 to 4 (#2616)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-06 13:05:20 -07:00
afa7c1dc4e Bump toml from 0.8.13 to 0.8.14 in /src/wasm-lib (#2615)
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.13 to 0.8.14.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.13...toml-v0.8.14)

---
updated-dependencies:
- dependency-name: toml
  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-06-06 13:05:01 -07:00
c74b695a71 Remove an orphaned grackle file (#2611) 2024-06-06 14:48:58 -05:00
d0c244e05e Do not aggressively disconnect when video stream goes down (#2621) 2024-06-06 11:40:39 -04:00
a315b77f02 More selection verification (#2619) 2024-06-06 11:55:22 +00:00
15c854ff18 verify sketches can be selected outside of sketches (#2618) 2024-06-06 08:07:42 +00:00
acd3a5717d improve selections and remove redundant edit_mode (#2617) 2024-06-06 16:03:10 +10:00
8a2555550f Adding a sample using a custom axis in revolve.rs (#2596)
* Adding a sample using a custom axis in revolve.rs

* Adding updated docs and snapshot of generated part

* Running fmt
2024-06-05 19:48:59 +00:00
62e75c852a Bump dawidd6/action-download-artifact from 4 to 5 (#2601)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 4 to 5.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 09:44:13 -07:00
max
dd3601ea7b Gizmo Normal Snapping (#2539)
* gizmo 2.0

nice and clickable

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

* initial mouse position fix

when the scene first loads, mouse position is 0,0, which renders the gizmo selected.

* animation loop / disposal optimization

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

* reset camera tweak

* add cam target to debug panel

* test stub

* reset camera position handle removed from gizmo

it is now a button in the debug panel

* gizmo refactoring

* small fix

* reset camera view

bug fix

* nicer updateCameraToAxis

now gizmo rotates around the target instead of world 0,0,0

* micro refactoring

* playwright update

* playwright remove timeout + fmt

* hide gizmo while loading stream

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

This reverts commit f0a506d6b9.

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

This reverts commit 2781261331.

* try make gizmo test more realiable

* tweak

* refactoring

* increase timeout time

* 1 sec wait after mouse click

* 3 sec timeout

* better clickPosition

* test with 10 sec timeout

* 0.5 sec timeout

* add passive update for gizmo to avoid some edge cases

* default_camera_get_settings after click

* try and remove timeouts

---------

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-06-05 12:43:12 +00:00
a5e7782d9a Re-enable rust cache for src-tauri on Windows (#2586) 2024-06-05 06:06:25 -04:00
79b0b70688 Bump ts-rs from badbac0 to be0349d in /src/wasm-lib (#2602) 2024-06-05 03:40:42 -05:00
1d134c1be0 Timeout ahead of flaky sign out (#2593) 2024-06-05 04:36:26 -04:00
1c58572234 cache playwright follow up (#2605)
cache plawwright follow up
2024-06-05 05:53:21 +00:00
ecee51e82b cache playwright browsers (#2604) 2024-06-05 05:10:49 +00:00
141 changed files with 15388 additions and 4826 deletions

View File

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

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake .

40
.github/workflows/cargo-check.yml vendored Normal file
View File

@ -0,0 +1,40 @@
on:
push:
branches:
- main
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- '**.rs'
- .github/workflows/cargo-check.yml
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo check
jobs:
cargocheck:
name: cargo check
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v4
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Run check
run: |
cd "${{ matrix.dir }}"
# We specifically want to test the disable-println feature
# Since it is not enabled by default, we need to specify it
# This is used in kcl-lsp
cargo check --all --features disable-println --features pyo3

View File

@ -9,6 +9,12 @@ on:
- '**.rs'
- .github/workflows/cargo-clippy.yml
pull_request:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- '**.rs'
- .github/workflows/cargo-clippy.yml
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
@ -54,3 +60,8 @@ jobs:
run: |
cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating
run: |
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"

View File

@ -180,9 +180,7 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
- name: Setup Rust cache
if: matrix.os != 'windows-latest'
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'

View File

@ -46,12 +46,18 @@ jobs:
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v4
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@ -127,7 +133,7 @@ jobs:
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
name: playwright-report-ubuntu
path: playwright-report/
retention-days: 30
@ -143,12 +149,20 @@ jobs:
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v4
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@ -190,6 +204,6 @@ jobs:
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
name: playwright-report-macos
path: playwright-report/
retention-days: 30

1
.gitignore vendored
View File

@ -17,6 +17,7 @@
.env.development.local
.env.test.local
.env.production.local
.direnv
npm-debug.log*
yarn-debug.log*

View File

@ -319,7 +319,7 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
```
yarn install
yarn build:wasm
yarn build:wasm-dev
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
yarn vite build --mode development
yarn tauri build --debug -b

323
docs/kcl/chamfer.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -23,6 +23,7 @@ layout: manual
* [`atan`](kcl/atan)
* [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)
* [`circle`](kcl/circle)
* [`close`](kcl/close)
* [`cos`](kcl/cos)
@ -64,6 +65,7 @@ layout: manual
* [`segEndX`](kcl/segEndX)
* [`segEndY`](kcl/segEndY)
* [`segLen`](kcl/segLen)
* [`shell`](kcl/shell)
* [`sin`](kcl/sin)
* [`sqrt`](kcl/sqrt)
* [`startProfileAt`](kcl/startProfileAt)

View File

@ -9,7 +9,7 @@ A circular pattern on a 2D sketch.
```js
patternCircular2d(data: CircularPattern2dData, sketch_group: SketchGroup) -> [SketchGroup]
patternCircular2d(data: CircularPattern2dData, sketch_group_set: SketchGroupSet) -> [SketchGroup]
```
### Examples
@ -48,7 +48,7 @@ const example = extrude(1, exampleSketch)
rotateDuplicates: string,
}
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
* `sketch_group_set`: `SketchGroupSet` - A sketch group or a group of sketch groups. (REQUIRED)
```js
{
// The plane id or face id of the sketch group.
@ -129,6 +129,7 @@ const example = extrude(1, exampleSketch)
// The to point.
to: [number, number],
},
type: "sketchGroup",
// The paths in the sketch group.
value: [{
// The from point.
@ -212,6 +213,9 @@ const example = extrude(1, exampleSketch)
y: number,
z: number,
},
} |
{
type: "sketchGroups",
}
```

View File

@ -9,7 +9,7 @@ A circular pattern on a 3D model.
```js
patternCircular3d(data: CircularPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
patternCircular3d(data: CircularPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
```
### Examples
@ -47,7 +47,7 @@ const example = extrude(-5, exampleSketch)
rotateDuplicates: string,
}
```
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
```js
{
// The id of the extrusion end cap
@ -127,6 +127,7 @@ const example = extrude(-5, exampleSketch)
}],
// The id of the extrusion start cap
startCapId: uuid,
type: "extrudeGroup",
// The extrude surfaces.
value: [{
// The face id for the extrude plane.
@ -176,6 +177,9 @@ const example = extrude(-5, exampleSketch)
y: number,
z: number,
},
} |
{
type: "extrudeGroups",
}
```

View File

@ -9,7 +9,7 @@ A linear pattern on a 3D model.
```js
patternLinear3d(data: LinearPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
patternLinear3d(data: LinearPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
```
### Examples
@ -45,7 +45,7 @@ const example = extrude(1, exampleSketch)
repetitions: number,
}
```
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
```js
{
// The id of the extrusion end cap
@ -125,6 +125,7 @@ const example = extrude(1, exampleSketch)
}],
// The id of the extrusion start cap
startCapId: uuid,
type: "extrudeGroup",
// The extrude surfaces.
value: [{
// The face id for the extrude plane.
@ -174,6 +175,9 @@ const example = extrude(1, exampleSketch)
y: number,
z: number,
},
} |
{
type: "extrudeGroups",
}
```

File diff suppressed because one or more lines are too long

311
docs/kcl/shell.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -405,17 +405,16 @@ test('Draft segments should look right', async ({ page, context }) => {
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)`)
code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -427,10 +426,9 @@ test('Draft segments should look right', async ({ page, context }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
code += `
|> line([7.25, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(code)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -513,17 +511,16 @@ test.describe('Client side scene scale should match engine scale', () => {
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)`)
code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -531,21 +528,18 @@ test.describe('Client side scene scale should match engine scale', () => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
code += `
|> line([7.25, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
code += `
|> tangentialArcTo([21.7, -2.44], %)`
await expect(u.codeLocator).toHaveText(code)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -616,17 +610,16 @@ test.describe('Client side scene scale should match engine scale', () => {
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
let code = `const sketch001 = startSketchOn('XZ')`
await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([230.03, -310.32], %)`)
code += `
|> startProfileAt([182.59, -246.32], %)`
await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -634,21 +627,18 @@ test.describe('Client side scene scale should match engine scale', () => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
code += `
|> line([184.3, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
code += `
|> tangentialArcTo([551.2, -62.01], %)`
await expect(u.codeLocator).toHaveText(code)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,5 +1,6 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
import { onboardingPaths } from 'routes/Onboarding/paths'
export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS = {
@ -22,9 +23,22 @@ export const TEST_SETTINGS = {
},
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_USER_MENU = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.USER_MENU },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_EXPORT = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.EXPORT },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
onboardingStatus: onboardingPaths.PARAMETRIC_MODELING,
},
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_START = {
@ -50,3 +64,25 @@ export const TEST_SETTINGS_CORRUPTED = {
textWrapping: true,
},
} satisfies Partial<SaveSettingsPayload>
export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|> startProfileAt([20, 0], %)
|> line([7.13, 4 + 0], %)
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> lineTo([20.14 + 0, -0.14 + 0], %)
|> xLineTo(29 + 0, %)
|> yLine(-3.14 + 0, %, 'a')
|> xLine(1.63, %)
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|> angledLineThatIntersects({
angle: 3.14,
intersectTag: 'a',
offset: 0
}, %)
|> tangentialArcTo([13.14 + 0, 13.14], %)
|> close(%)
|> extrude(5 + 7, %)
`

View File

@ -12,14 +12,16 @@ async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' })
await page
.getByTestId('loading')
.waitFor({ state: 'detached', timeout: 20_000 })
await page.getByTestId('start-sketch').waitFor()
}
async function removeCurrentCode(page: Page) {
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.click('.cm-content')
await page.locator('.cm-content').click()
await page.keyboard.down(hotkey)
await page.keyboard.press('a')
await page.keyboard.up(hotkey)
@ -28,12 +30,12 @@ async function removeCurrentCode(page: Page) {
}
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
await page.click('[data-testid="custom-cmd-send-button"]')
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
await page.getByTestId('custom-cmd-send-button').click()
}
async function clearCommandLogs(page: Page) {
await page.click('[data-testid="clear-commands"]')
await page.getByTestId('clear-commands').click()
}
async function expectCmdLog(page: Page, locatorStr: string) {
@ -130,8 +132,8 @@ export const getMovementUtils = (opts: any) => {
// NOTE: these pretty much can't be perfect because of screen scaling.
// Handle on a case-by-case.
const toU = (x: number, y: number) => [
kcRound(x * 0.0854),
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
kcRound(x * 0.0678),
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
]
// Turn the array into a string with specific formatting
@ -162,12 +164,7 @@ export const getMovementUtils = (opts: any) => {
return ret.then(() => [last.x, last.y])
}
const expectCodeToBe = async (str: string) => {
await expect(opts.page.locator('.cm-content')).toHaveText(str)
await opts.page.waitForTimeout(100)
}
return { toSU, click00r, expectCodeToBe }
return { toSU, click00r }
}
export async function getUtils(page: Page) {
@ -228,6 +225,8 @@ export async function getUtils(page: Page) {
.locator(locator)
.boundingBox()
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'),
canvasLocator: page.getByTestId('client-side-scene'),
doAndWaitForCmd: async (
fn: () => Promise<void>,
commandType: string,

View File

@ -2,6 +2,7 @@ import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { click, setDatasetValue } from '../utils'
const isWin32 = os.platform() === 'win32'
const documentsDir = path.join(os.homedir(), 'Documents')
@ -15,25 +16,8 @@ const newProjectDir = path.join(documentsDir, 'a-different-directory')
const tmp = process.env.TEMP || '/tmp'
const userCodeDir = path.join(tmp, 'kittycad_user_code')
async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
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)', () => {
it('opens the auth page and signs in', async () => {
describe('ZMA sign in flow', () => {
before(async () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultProjectDir, { force: true, recursive: true })
@ -42,7 +26,9 @@ describe('ZMA (Tauri)', () => {
await fs.rm(userSettingsDir, { force: true, recursive: true })
await fs.mkdir(defaultProjectDir, { recursive: true })
await fs.mkdir(newProjectDir, { recursive: true })
})
it('opens the auth page and signs in', async () => {
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
@ -82,6 +68,10 @@ describe('ZMA (Tauri)', () => {
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New project')
})
})
describe('ZMA authorized user flows', () => {
// Note: each flow below is intended to start *and* end from the home page
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
@ -150,8 +140,11 @@ describe('ZMA (Tauri)', () => {
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
await browser.execute(`window.location.href = "${base}/home"`)
})
})
describe('ZMA sign out flow', () => {
it('signs out', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')

18
e2e/tauri/utils.ts Normal file
View File

@ -0,0 +1,18 @@
import { browser } from '@wdio/globals'
export async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
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
*/
export async function setDatasetValue(
field: WebdriverIO.Element,
property: string,
value: string
) {
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
}

62
flake.lock generated Normal file
View File

@ -0,0 +1,62 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1718470082,
"narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1718681902,
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

70
flake.nix Normal file
View File

@ -0,0 +1,70 @@
{
description = "modeling-app development environment";
# Flake inputs
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
rust-overlay.url = "github:oxalica/rust-overlay"; # A helper for Rust + Nix
};
# Flake outputs
outputs = { self, nixpkgs, rust-overlay }:
let
# Overlays enable you to customize the Nixpkgs attribute set
overlays = [
# Makes a `rust-bin` attribute available in Nixpkgs
(import rust-overlay)
# Provides a `rustToolchain` attribute for Nixpkgs that we can use to
# create a Rust environment
(self: super: {
rustToolchain = super. rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rustfmt" "llvm-tools-preview" ];
};
})
];
# Systems supported
allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux
"x86_64-darwin" # 64-bit Intel macOS
"aarch64-darwin" # 64-bit ARM macOS
];
# Helper to provide system-specific attributes
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit overlays system; };
});
in
{
# Development environment output
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = (with pkgs; [
# The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt,
# rustdoc, rustfmt, and other tools.
rustToolchain
cargo-llvm-cov
cargo-nextest
just
postgresql.lib
openssl
pkg-config
nodejs_22
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
libiconv
darwin.apple_sdk.frameworks.Security
]);
TARGET_CC = "${pkgs.stdenv.cc}/bin/${pkgs.stdenv.cc.targetPrefix}cc";
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
};
});
};
}

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.22.0",
"version": "0.22.3",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",
@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.64",
"@kittycad/lib": "^0.0.67",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1",

1495
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,11 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
kittycad = "0.3.5"
log = "0.4.21"
oauth2 = "4.4.2"
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri = { version = "2.0.0-beta.22", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }

View File

@ -63,16 +63,17 @@
"subcommands": {}
},
"deep-link": {
"domains": [
{
"host": "app.zoo.dev"
}
]
"mobile": [],
"desktop": {
"schemes": [
"app.zoo.dev"
]
}
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.22.0"
"version": "0.22.3"
}

View File

@ -127,7 +127,7 @@ export function App() {
/>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<Stream className="absolute inset-0 z-0" />
<Stream />
{/* <CamToggle /> */}
<LowerRightControls>
<Gizmo />

View File

@ -48,12 +48,14 @@ export type ReactCameraProperties =
type: 'perspective'
fov?: number
position: [number, number, number]
target: [number, number, number]
quaternion: [number, number, number, number]
}
| {
type: 'orthographic'
zoom?: number
position: [number, number, number]
target: [number, number, number]
quaternion: [number, number, number, number]
}
@ -172,41 +174,6 @@ export class CameraControls {
}
}
throttledUpdateEngineFov = throttle(
(vals: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
target: Vector3
}) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...vals,
isPerspective: true,
}),
fov_y: vals.fov,
...calculateNearFarFromFOV(vals.fov),
},
}
this.engineCommandManager.sendSceneCommand(cmd)
this.lastPerspectiveCmd = cmd
this.lastPerspectiveCmdTime = Date.now()
if (this.lastPerspectiveCmdTimeoutId !== null) {
clearTimeout(this.lastPerspectiveCmdTimeoutId)
}
this.lastPerspectiveCmdTimeoutId = setTimeout(
this.sendLastPerspectiveReliableChannel,
lastCmdDelay
) as any as number
},
1000 / 30
)
constructor(
isOrtho = false,
domElement: HTMLCanvasElement,
@ -442,7 +409,7 @@ export class CameraControls {
this.handleEnd()
return
}
this.throttledEngCmd({
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
@ -454,11 +421,11 @@ export class CameraControls {
return
}
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
// Else "clientToEngine" (Sketch Mode) or forceUpdate
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
// From onMouseMove zoom handling which seems to be really smooth
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
this.pendingZoom *= 1 + event.deltaY * 0.01
this.handleEnd()
}
@ -532,26 +499,28 @@ export class CameraControls {
direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance)
}
usePerspectiveCamera = () => {
usePerspectiveCamera = async () => {
this._usePerspectiveCamera()
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y:
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
if (this.syncDirection === 'clientToEngine') {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y:
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
},
},
},
})
})
}
this.onCameraChange()
this.update()
return this.camera
}
dollyZoom = (newFov: number) => {
dollyZoom = async (newFov: number, splitEngineCalls = false) => {
if (!(this.camera instanceof PerspectiveCamera)) {
console.warn('Dolly zoom is only applicable to perspective cameras.')
return
@ -602,13 +571,52 @@ export class CameraControls {
this.camera.near = z_near
this.camera.far = z_far
this.throttledUpdateEngineFov({
fov: newFov,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
})
if (splitEngineCalls) {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
...convertThreeCamValuesToEngineCam({
isPerspective: true,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
}),
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y: newFov,
z_near: 0.01,
z_far: 1000,
},
},
})
} else {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
isPerspective: true,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
}),
fov_y: newFov,
z_near: 0.01,
z_far: 1000,
},
})
}
}
update = (forceUpdate = false) => {
@ -773,6 +781,75 @@ export class CameraControls {
})
}
async updateCameraToAxis(
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
): Promise<void> {
const distance = this.camera.position.distanceTo(this.target)
const vantage = this.target.clone()
let up = { x: 0, y: 0, z: 1 }
if (axis === 'x') {
vantage.x += distance
} else if (axis === 'y') {
vantage.y += distance
} else if (axis === 'z') {
vantage.z += distance
up = { x: -1, y: 0, z: 0 }
} else if (axis === '-x') {
vantage.x -= distance
} else if (axis === '-y') {
vantage.y -= distance
} else if (axis === '-z') {
vantage.z -= distance
up = { x: -1, y: 0, z: 0 }
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: this.target,
vantage: vantage,
up: up,
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}
async resetCameraPosition(): Promise<void> {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: this.target,
vantage: {
x: this.target.x,
y: this.target.y - 128,
z: this.target.z + 64,
},
up: { x: 0, y: 0, z: 1 },
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
},
})
}
async tweenCameraToQuaternion(
targetQuaternion: Quaternion,
targetPosition = new Vector3(),
@ -944,6 +1021,29 @@ export class CameraControls {
.onComplete(onComplete)
.start()
})
snapToPerspectiveBeforeHandingBackControlToEngine = async (
targetCamUp = new Vector3(0, 0, 1)
) => {
if (this.syncDirection === 'engineToClient') {
console.warn(
'animate To Perspective not design to work with engineToClient syncDirection.'
)
}
this.isFovAnimationInProgress = true
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4
let currentFov = 4
const initialCameraUp = this.camera.up.clone()
this.usePerspectiveCamera()
const tempVec = new Vector3()
currentFov = this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov)
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, 1)
this.camera.up.copy(currentUp)
await this.dollyZoom(currentFov, true)
this.isFovAnimationInProgress = false
}
get reactCameraProperties(): ReactCameraProperties {
return {
@ -957,6 +1057,11 @@ export class CameraControls {
roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2),
],
target: [
roundOff(this.target.x, 2),
roundOff(this.target.y, 2),
roundOff(this.target.z, 2),
],
quaternion: [
roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2),
@ -1011,7 +1116,7 @@ function calculateNearFarFromFOV(fov: number) {
// const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far: 1000 }
return { z_near: 0.01, z_far: 1000 }
}
function convertThreeCamValuesToEngineCam({
@ -1030,11 +1135,6 @@ function convertThreeCamValuesToEngineCam({
// leaving for now since it's working but maybe revisit later
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
if (isPerspective) {
return {
@ -1043,6 +1143,10 @@ function convertThreeCamValuesToEngineCam({
vantage: position,
}
}
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
const direction = lookAtVector.clone().sub(position).normalize()

View File

@ -136,6 +136,7 @@ export const ClientSideScene = ({
<div
ref={canvasRef}
style={{ cursor: cursor }}
data-testid="client-side-scene"
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${
@ -699,6 +700,15 @@ export const CamDebugSettings = () => {
}
}}
/>
<div>
<button
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset Camera Position
</button>
</div>
{camSettings.type === 'perspective' && (
<input
type="range"
@ -816,6 +826,71 @@ export const CamDebugSettings = () => {
</li>
</ul>
</div>
<div>
target
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-target"
value={camSettings.target[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
parseFloat(e.target.value),
camSettings.target[1],
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-target"
value={camSettings.target[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
parseFloat(e.target.value),
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-target"
value={camSettings.target[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
camSettings.target[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div>
)
}

View File

@ -32,9 +32,7 @@ import {
SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER,
X_AXIS,
XZ_PLANE,
Y_AXIS,
YZ_PLANE,
} from './sceneInfra'
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import {
@ -1329,13 +1327,6 @@ export class SceneEntities {
to,
})
}
async animateAfterSketch() {
// if (isReducedMotion()) {
// sceneInfra.camControls.usePerspectiveCamera()
// return
// }
await sceneInfra.camControls.animateToPerspective()
}
removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup)
}
@ -1399,114 +1390,135 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type)
},
onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise<
['face' | 'plane' | 'other', string]
> => {
const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
if (!entity_id) return ['other', '']
if (
engineCommandManager.defaultPlanes?.xy === entity_id ||
engineCommandManager.defaultPlanes?.xz === entity_id ||
engineCommandManager.defaultPlanes?.yz === entity_id
) {
return ['plane', entity_id]
const { streamDimensions } = useStore.getState()
const { entity_id, ...rest } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions
)
let _entity_id = entity_id
console.log('things', _entity_id, rest)
if (!_entity_id) return
if (
engineCommandManager.defaultPlanes?.xy === _entity_id ||
engineCommandManager.defaultPlanes?.xz === _entity_id ||
engineCommandManager.defaultPlanes?.yz === _entity_id ||
engineCommandManager.defaultPlanes?.negXy === _entity_id ||
engineCommandManager.defaultPlanes?.negXz === _entity_id ||
engineCommandManager.defaultPlanes?.negYz === _entity_id
) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
}
const artifact = this.engineCommandManager.artifactMap[entity_id]
// If we clicked on an extrude wall, we climb up the parent Id
// to get the sketch profile's face ID. If we clicked on an endcap,
// we already have it.
const targetId =
'additionalData' in artifact &&
artifact.additionalData?.type === 'cap'
? entity_id
: artifact.parentId
// TODO can we get this information from rust land when it creates the default planes?
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const target = this.engineCommandManager.artifactMap?.[
targetId || ''
] as ArtifactMapCommand & { extrusions?: string[] }
// get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions =
this.engineCommandManager.artifactMap?.[
target?.extrusions?.[0] || ''
]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return ['other', entity_id]
const faceInfo = await getFaceDetails(entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return ['other', entity_id]
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
if (engineCommandManager.defaultPlanes?.xy === _entity_id) {
console.log('XY')
zAxis = [0, 0, 1]
yAxis = [0, 1, 0]
if (camVector.z < 0) {
zAxis = [0, 0, -1]
_entity_id = engineCommandManager.defaultPlanes?.negXy || ''
}
} else if (engineCommandManager.defaultPlanes?.yz === _entity_id) {
console.log('YZ')
zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
if (camVector.x < 0) {
zAxis = [-1, 0, 0]
_entity_id = engineCommandManager.defaultPlanes?.negYz || ''
}
} else if (engineCommandManager.defaultPlanes?.xz === _entity_id) {
console.log('XZ')
zAxis = [0, 1, 0]
yAxis = [0, 0, 1]
_entity_id = engineCommandManager.defaultPlanes?.negXz || ''
if (camVector.y < 0) {
zAxis = [0, -1, 0]
_entity_id = engineCommandManager.defaultPlanes?.xz || ''
}
}
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: entity_id,
type: 'defaultPlane',
planeId: _entity_id,
plane: defaultPlaneStrMap[_entity_id],
zAxis,
yAxis,
},
})
return ['face', entity_id]
return
}
const artifact = this.engineCommandManager.artifactMap[_entity_id]
// If we clicked on an extrude wall, we climb up the parent Id
// to get the sketch profile's face ID. If we clicked on an endcap,
// we already have it.
const targetId =
'additionalData' in artifact &&
artifact.additionalData?.type === 'cap'
? _entity_id
: artifact.parentId
const faceResult = await checkExtrudeFaceClick()
if (faceResult[0] === 'face') return
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const target = this.engineCommandManager.artifactMap?.[
targetId || ''
] as ArtifactMapCommand & { extrusions?: string[] }
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions =
this.engineCommandManager.artifactMap?.[target?.extrusions?.[0] || '']
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') return
const faceInfo = await getFaceDetails(_entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return
const { intersects } = args
const type = intersects?.[0].object.name || ''
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
let yAxis: [number, number, number] = [0, 1, 0]
if (type === YZ_PLANE) {
planeString = posNorm ? 'YZ' : '-YZ'
zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
yAxis = [0, 0, 1]
} else if (type === XZ_PLANE) {
planeString = posNorm ? '-XZ' : 'XZ'
zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
yAxis = [0, 0, 1]
}
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
plane: planeString,
zAxis,
yAxis,
planeId: faceResult[1],
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap:
artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info
: 'none',
faceId: _entity_id,
},
})
return
},
})
}

View File

@ -6,6 +6,8 @@ import CommandComboBox from '../CommandComboBox'
import CommandBarReview from './CommandBarReview'
import { useLocation } from 'react-router-dom'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
export const CommandBar = () => {
const { pathname } = useLocation()
@ -103,7 +105,7 @@ export const CommandBar = () => {
leaveTo="opacity-0 scale-95"
>
<WrapperComponent.Panel
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded rounded-tl-none shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
data-testid="command-bar"
>
@ -116,6 +118,19 @@ export const CommandBar = () => {
<CommandBarReview stepBack={stepBack} />
)
)}
<button
onClick={() => commandBarSend({ type: 'Close' })}
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
>
<CustomIcon
name="close"
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
/>
<Tooltip position="bottom" delay={500}>
Cancel{' '}
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
</WrapperComponent.Panel>
</Transition.Child>
</WrapperComponent>

View File

@ -7,10 +7,8 @@ import {
getSelectionType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { StateFrom } from 'xstate'
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
@ -41,24 +39,10 @@ function CommandBarSelectionInput({
canSubmitSelectionArg(selectionsByType, arg)
)
useHotkeys('tab', () => onSubmit(selection), {
enableOnFormTags: true,
enableOnContentEditable: true,
keyup: true,
})
useEffect(() => {
inputRef.current?.focus()
}, [selection, inputRef])
// Exit engine's edit mode when this input step is active,
// and re-enter it when it's not.
// In future the engine's edit mode will go away and this will be handled differently.
useEffect(() => {
kclManager.exitEditMode()
return () => kclManager.defaultSelectionFilter()
}, [])
// Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already
useEffect(() => {

View File

@ -0,0 +1,199 @@
import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon'
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { Dialog } from '@headlessui/react'
interface ContextMenuProps
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
items?: React.ReactElement[]
menuTargetElement?: RefObject<HTMLElement>
}
const DefaultContextMenuItems = [
<ContextMenuItemRefresh />,
<ContextMenuItemCopy />,
// add more default context menu items here
]
export function ContextMenu({
items = DefaultContextMenuItems,
menuTargetElement,
className,
...props
}: ContextMenuProps) {
const dialogRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const [windowSize, setWindowSize] = useState({
width: globalThis?.window?.innerWidth,
height: globalThis?.window?.innerHeight,
})
const [position, setPosition] = useState({ x: 0, y: 0 })
useHotkeys('esc', () => setOpen(false), {
enabled: open,
})
const dialogPositionStyle = useMemo(() => {
if (!dialogRef.current)
return {
top: 0,
left: 0,
right: 'auto',
bottom: 'auto',
}
return {
top:
position.y + dialogRef.current.clientHeight > windowSize.height
? 'auto'
: position.y,
left:
position.x + dialogRef.current.clientWidth > windowSize.width
? 'auto'
: position.x,
right:
position.x + dialogRef.current.clientWidth > windowSize.width
? windowSize.width - position.x
: 'auto',
bottom:
position.y + dialogRef.current.clientHeight > windowSize.height
? windowSize.height - position.y
: 'auto',
}
}, [position, windowSize, dialogRef.current])
// Listen for window resize to update context menu position
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: globalThis?.window?.innerWidth,
height: globalThis?.window?.innerHeight,
})
}
globalThis?.window?.addEventListener('resize', handleResize)
return () => {
globalThis?.window?.removeEventListener('resize', handleResize)
}
}, [])
// Add context menu listener to target once mounted
useEffect(() => {
const handleContextMenu = (e: MouseEvent) => {
console.log('context menu', e)
e.preventDefault()
setPosition({ x: e.x, y: e.y })
setOpen(true)
}
menuTargetElement?.current?.addEventListener(
'contextmenu',
handleContextMenu
)
return () => {
menuTargetElement?.current?.removeEventListener(
'contextmenu',
handleContextMenu
)
}
}, [menuTargetElement?.current])
return (
<Dialog open={open} onClose={() => setOpen(false)}>
<div
className="fixed inset-0 z-50 w-screen h-screen"
onContextMenu={(e) => e.preventDefault()}
>
<Dialog.Backdrop className="fixed z-10 inset-0" />
<Dialog.Panel
ref={dialogRef}
className={`w-48 fixed bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
shadow-lg backdrop:fixed backdrop:inset-0 backdrop:bg-primary ${className}`}
style={{
...dialogPositionStyle,
...props.style,
}}
>
<ul
{...props}
className="relative flex flex-col gap-0.5 items-stretch content-stretch"
onClick={() => setOpen(false)}
>
{...items}
</ul>
</Dialog.Panel>
</div>
</Dialog>
)
}
export function ContextMenuDivider() {
return <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
}
interface ContextMenuItemProps {
children: React.ReactNode
icon?: ActionIconProps['icon']
onClick?: () => void
hotkey?: string
}
export function ContextMenuItem({
children,
icon,
onClick,
hotkey,
}: ContextMenuItemProps) {
return (
<button
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={onClick}
>
{icon && <ActionIcon icon={icon} bgClassName="!bg-transparent" />}
<div className="flex-1">{children}</div>
{hotkey && (
<kbd className="px-1.5 py-0.5 rounded bg-primary/10 text-primary dark:bg-chalkboard-80 dark:text-chalkboard-40">
{hotkey}
</kbd>
)}
</button>
)
}
export function ContextMenuItemRefresh() {
return (
<ContextMenuItem
icon="arrowRotateRight"
onClick={() => globalThis?.window?.location.reload()}
>
Refresh
</ContextMenuItem>
)
}
interface ContextMenuItemCopyProps {
toBeCopiedContent?: string
toBeCopiedLabel?: string
}
export function ContextMenuItemCopy({
toBeCopiedContent = globalThis.window?.getSelection()?.toString(),
toBeCopiedLabel = 'selection',
}: ContextMenuItemCopyProps) {
return (
<ContextMenuItem
icon="clipboardPlus"
onClick={() => {
if (toBeCopiedContent) {
globalThis?.navigator?.clipboard
.writeText(toBeCopiedContent)
.then(() => toast.success(`Copied ${toBeCopiedLabel} to clipboard`))
.catch(() =>
toast.error(`Failed to copy ${toBeCopiedLabel} to clipboard`)
)
}
}}
>
Copy
</ContextMenuItem>
)
}

View File

@ -71,6 +71,16 @@ const CustomIconMap = {
/>
</svg>
),
bug: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
fill="currentColor"
/>
</svg>
),
checkmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -18,6 +18,8 @@ import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
@ -125,6 +127,7 @@ const FileTreeItem = ({
const navigate = useNavigate()
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback(
@ -185,7 +188,7 @@ const FileTreeItem = ({
}
return (
<>
<div className="contents" ref={itemRef}>
{fileOrDir.children === undefined ? (
<li
className={
@ -321,7 +324,41 @@ const FileTreeItem = ({
setIsOpen={setIsConfirmingDelete}
/>
)}
</>
<FileTreeContextMenu
itemRef={itemRef}
onRename={addCurrentItemToRenaming}
onDelete={() => setIsConfirmingDelete(true)}
/>
</div>
)
}
interface FileTreeContextMenuProps {
itemRef: React.RefObject<HTMLElement>
onRename: () => void
onDelete: () => void
}
function FileTreeContextMenu({
itemRef,
onRename,
onDelete,
}: FileTreeContextMenuProps) {
const platform = usePlatform()
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
return (
<ContextMenu
menuTargetElement={itemRef}
items={[
<ContextMenuItem onClick={onRename} hotkey="Enter">
Rename
</ContextMenuItem>,
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
Delete
</ContextMenuItem>,
]}
/>
)
}

View File

@ -1,5 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons'
import { useEffect, useRef } from 'react'
import { MutableRefObject, useEffect, useRef } from 'react'
import {
WebGLRenderer,
Scene,
@ -12,21 +13,52 @@ import {
Clock,
Quaternion,
ColorRepresentation,
Vector2,
Raycaster,
Camera,
Intersection,
Object3D,
} from 'three'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
} from './ContextMenu'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
const AXIS_LENGTH = 0.35
const AXIS_WIDTH = 0.02
const AXIS_COLORS = {
x: '#fa6668',
y: '#11eb6b',
z: '#6689ef',
gray: '#c6c7c2',
enum AxisColors {
X = '#fa6668',
Y = '#11eb6b',
Z = '#6689ef',
Gray = '#c6c7c2',
}
enum AxisNames {
X = 'x',
Y = 'y',
Z = 'z',
NEG_X = '-x',
NEG_Y = '-y',
NEG_Z = '-z',
}
const axisNamesSemantic: Record<AxisNames, string> = {
[AxisNames.X]: 'Right',
[AxisNames.Y]: 'Back',
[AxisNames.Z]: 'Top',
[AxisNames.NEG_X]: 'Left',
[AxisNames.NEG_Y]: 'Front',
[AxisNames.NEG_Z]: 'Bottom',
}
export default function Gizmo() {
const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0)
useEffect(() => {
if (!canvasRef.current) return
@ -41,35 +73,89 @@ export default function Gizmo() {
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
scene.add(...gizmoAxes, ...gizmoAxisHeads)
const raycaster = new Raycaster()
const { mouse, disposeMouseEvents } = initializeMouseEvents(
canvas,
raycasterIntersect,
sceneInfra
)
const raycasterObjects = [...gizmoAxisHeads]
const clock = new Clock()
const clientCamera = sceneInfra.camControls.camera
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
const animate = () => {
requestAnimationFrame(animate)
const delta = clock.getDelta()
updateCameraOrientation(
camera,
currentQuaternion,
sceneInfra.camControls.camera.quaternion,
clock.getDelta()
delta,
cameraPassiveUpdateTimer
)
updateRayCaster(
raycasterObjects,
raycaster,
mouse,
camera,
raycasterIntersect,
delta,
raycasterPassiveUpdateTimer
)
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
animate()
return () => {
renderer.dispose()
disposeMouseEvents()
}
}, [])
return (
<div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
<canvas ref={canvasRef} />
</div>
<>
<div
ref={wrapperRef}
aria-label="View orientation gizmo"
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto"
>
<canvas ref={canvasRef} />
<ContextMenu
menuTargetElement={wrapperRef}
items={[
...Object.entries(axisNamesSemantic).map(
([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls.updateCameraToAxis(
axisName as AxisNames
)
}}
>
{axisSemantic} view
</ContextMenuItem>
)
),
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
]}
/>
</div>
</>
)
}
const createCamera = () => {
const createCamera = (): OrthographicCamera => {
return new OrthographicCamera(
-FRUSTUM_SIZE,
FRUSTUM_SIZE,
@ -82,21 +168,21 @@ const createCamera = () => {
const createGizmo = () => {
const gizmoAxes = [
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.x, 0, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI / 2, 'y'),
]
const gizmoAxisHeads = [
createAxisHead(AXIS_LENGTH, AXIS_COLORS.x, 0, 'z'),
createAxisHead(AXIS_LENGTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
createAxisHead(AXIS_LENGTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI, 'z'),
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
]
return { gizmoAxes, gizmoAxisHeads }
@ -108,12 +194,9 @@ const createAxis = (
color: ColorRepresentation,
rotation = 0,
axis = 'x'
) => {
const geometry = new BoxGeometry(length, width, width).translate(
length / 2,
0,
0
)
): Mesh => {
const geometry = new BoxGeometry(length, width, width)
geometry.translate(length / 2, 0, 0)
const material = new MeshBasicMaterial({ color: new Color(color) })
const mesh = new Mesh(geometry, material)
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
@ -121,15 +204,17 @@ const createAxis = (
}
const createAxisHead = (
length: number,
name: AxisNames,
color: ColorRepresentation,
rotation = 0,
axis = 'x'
) => {
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
position: number[]
): Mesh => {
const geometry = new SphereGeometry(0.065, 16, 8)
const material = new MeshBasicMaterial({ color: new Color(color) })
const mesh = new Mesh(geometry, material)
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
mesh.position.set(position[0], position[1], position[2])
mesh.updateMatrixWorld()
mesh.name = name
return mesh
}
@ -137,10 +222,97 @@ const updateCameraOrientation = (
camera: OrthographicCamera,
currentQuaternion: Quaternion,
targetQuaternion: Quaternion,
deltaTime: number
deltaTime: number,
cameraPassiveUpdateTimer: MutableRefObject<number>
) => {
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
camera.quaternion.copy(currentQuaternion)
cameraPassiveUpdateTimer.current += deltaTime
if (
!quaternionsEqual(currentQuaternion, targetQuaternion) ||
cameraPassiveUpdateTimer.current >= 5
) {
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
camera.quaternion.copy(currentQuaternion)
cameraPassiveUpdateTimer.current = 0
}
}
const quaternionsEqual = (
q1: Quaternion,
q2: Quaternion,
tolerance: number = 0.001
): boolean => {
return (
Math.abs(q1.x - q2.x) < tolerance &&
Math.abs(q1.y - q2.y) < tolerance &&
Math.abs(q1.z - q2.z) < tolerance &&
Math.abs(q1.w - q2.w) < tolerance
)
}
const initializeMouseEvents = (
canvas: HTMLCanvasElement,
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
sceneInfra: SceneInfra
): { mouse: Vector2; disposeMouseEvents: () => void } => {
const mouse = new Vector2()
mouse.x = 1 // fix initial mouse position issue
const handleMouseMove = (event: MouseEvent) => {
const { left, top, width, height } = canvas.getBoundingClientRect()
mouse.x = ((event.clientX - left) / width) * 2 - 1
mouse.y = ((event.clientY - top) / height) * -2 + 1
}
const handleClick = () => {
if (raycasterIntersect.current) {
const axisName = raycasterIntersect.current.object.name as AxisNames
sceneInfra.camControls.updateCameraToAxis(axisName)
}
}
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('click', handleClick)
const disposeMouseEvents = () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('click', handleClick)
}
return { mouse, disposeMouseEvents }
}
const updateRayCaster = (
objects: Object3D[],
raycaster: Raycaster,
mouse: Vector2,
camera: Camera,
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
deltaTime: number,
raycasterPassiveUpdateTimer: MutableRefObject<number>
) => {
raycasterPassiveUpdateTimer.current += deltaTime
// check if mouse is outside the canvas bounds and stop raycaster
if (raycasterPassiveUpdateTimer.current < 2) {
if (mouse.x < -1 || mouse.x > 1 || mouse.y < -1 || mouse.y > 1) {
raycasterIntersect.current = null
return
}
}
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(objects)
objects.forEach((object) => object.scale.set(1, 1, 1))
if (intersects.length) {
intersects[0].object.scale.set(1.5, 1.5, 1.5)
raycasterIntersect.current = intersects[0] // filter first object
} else {
raycasterIntersect.current = null
}
if (raycasterPassiveUpdateTimer.current > 2) {
raycasterPassiveUpdateTimer.current = 0
}
}

View File

@ -33,7 +33,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
rel="noopener noreferrer"
>
<CustomIcon
name="exclamationMark"
name="bug"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top">Report a bug</Tooltip>

View File

@ -47,7 +47,6 @@ import {
TANGENTIAL_ARC_TO_SEGMENT,
getParentGroup,
getSketchOrientationDetails,
getSketchQuaternion,
} from 'clientSideScene/sceneEntities'
import {
moveValueIntoNewVariablePath,
@ -76,6 +75,7 @@ import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -121,7 +121,24 @@ export const ModelingMachineProvider = ({
htmlRef,
token
)
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true))
useHotkeyWrapper(['meta + shift + .'], () => {
console.warn('CoreDump: Initializing core dump')
toast.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
})
// Settings machine setup
// const retrievedSettings = useRef(
@ -141,7 +158,41 @@ export const ModelingMachineProvider = ({
{
actions: {
'sketch exit execute': () => {
kclManager.executeCode(true)
;(async () => {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
sceneInfra.camControls.syncDirection = 'engineToClient'
const settings: Models['CameraSettings_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
)?.data?.data?.settings
if (settings.up.z !== 1) {
// workaround for gimbal lock situation
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: settings.center,
vantage: {
...settings.pos,
y:
settings.pos.y +
(settings.center.z - settings.pos.z > 0 ? 2 : -2),
},
up: { x: 0, y: 0, z: 1 },
},
})
}
kclManager.executeCode(true)
})()
},
'Set mouse state': assign({
mouseState: (_, event) => event.data,
@ -464,7 +515,7 @@ export const ModelingMachineProvider = ({
engineCommandManager,
data.faceId
)
sceneInfra.camControls.syncDirection = 'clientToEngine'
return {
sketchPathToNode: pathToNewSketchNode,
zAxis: data.zAxis,
@ -478,8 +529,10 @@ export const ModelingMachineProvider = ({
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.syncDirection = 'clientToEngine'
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
data.planeId
)
return {
sketchPathToNode: pathToNode,
zAxis: data.zAxis,

View File

@ -2,7 +2,7 @@
@apply relative z-0 rounded-r max-w-full h-full flex-1;
display: grid;
grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;
scroll-margin-block-start: 41px;
}
@ -12,7 +12,7 @@
}
:global(.dark) .panel {
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
@apply bg-chalkboard-100/50 focus-within:bg-chalkboard-100/90 backdrop-blur-[3px] border-chalkboard-80;
}
.header {

View File

@ -46,7 +46,11 @@ export const ModelingPane = ({
data-testid={detailsTestId}
id={id}
className={
pointerEventsCssClass + styles.panel + ' group ' + (className || '')
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
pointerEventsCssClass +
styles.panel +
' group ' +
(className || '')
}
>
<ModelingPaneHeader title={title} Menu={Menu} />

View File

@ -123,70 +123,73 @@ function ModelingSidebarSection({
}, [showDebugPanel.current, togglePane, openPanes])
return (
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab.List
className={
'pointer-events-auto ' +
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 ' +
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
<div className="group contents">
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</Tab.List>
<Tab.Panels
as="article"
className={
'col-start-2 col-span-1 ' +
(openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`
: ``)
}
>
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
<Tab.List
className={
'pointer-events-auto ' +
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
}
>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</Tab.List>
<Tab.Panels
as="article"
className={
'col-start-2 col-span-1 ' +
(openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`
: ``)
}
>
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
</div>
)
}

View File

@ -1,8 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { APP_NAME } from 'lib/constants'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'

View File

@ -24,9 +24,9 @@ export function RefreshButton() {
return (
<button
onClick={refresh}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-10 dark:border-chalkboard-100"
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
>
<CustomIcon name="arrowRotateRight" className="w-5 h-5" />
<CustomIcon name="exclamationMark" className="w-5 h-5" />
<Tooltip position="bottom-right">
<span>Refresh and report</span>
<br />

View File

@ -171,7 +171,9 @@ export const SettingsAuthProviderBase = ({
})
},
'Execute AST': () => kclManager.executeCode(true, true),
persistSettings: (context) =>
},
services: {
'Persist settings': (context) =>
saveSettings(context, loadedProject?.project?.path),
},
}

View File

@ -126,8 +126,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
return (
<div
id="stream"
className={className}
className="absolute inset-0 z-0"
data-testid="stream"
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()}
@ -142,7 +142,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
onMouseMoveCapture={handleMouseMove}
className="w-full cursor-pointer h-full"
disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
id="video-stream"
/>
<ClientSideScene

View File

@ -11,30 +11,8 @@
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
--_p-block: 4px;
--_bg: var(--chalkboard-10);
--_shadow-alpha: 5%;
--_shadow-alpha: 8%;
--_theme-alpha: 0.15;
--_theme-outline: drop-shadow(
0 1px 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
)
drop-shadow(
1px 0 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
);
pointer-events: none;
user-select: none;
@ -61,16 +39,15 @@
background: var(--_bg);
@apply text-chalkboard-110;
will-change: filter;
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha)))
var(--_theme-outline);
filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
}
:global(.dark) .tooltip {
--_bg: var(--chalkboard-110);
--_bg: var(--chalkboard-90);
--_theme-alpha: 40%;
--_shadow-alpha: 16%;
@apply text-chalkboard-10;
filter: var(--_theme-outline);
}
.tooltip:dir(rtl) {
@ -109,7 +86,7 @@
}
/* Sometimes there's no visible label,
* so we'll use the tooltip as the label
* so we'll use the tooltip as the label
*/
.tooltip:only-child::before {
content: 'Tooltip:';

View File

@ -39,7 +39,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<Popover className="relative">
{user?.image && !imageLoadFailed ? (
<Popover.Button
className="border-0 rounded-full w-fit min-w-max p-0 group"
className="relative border-0 rounded-full w-fit min-w-max p-0 group"
data-testid="user-sidebar-toggle"
>
<div className="rounded-full border overflow-hidden">
@ -51,6 +51,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
onError={() => setImageLoadFailed(true)}
/>
</div>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</Popover.Button>
) : (
<ActionButton
@ -59,7 +62,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
className="border-transparent !px-0"
data-testid="user-sidebar-toggle"
>
<Tooltip position="left" delay={1000}>
<Tooltip position="bottom-right" delay={1000}>
User menu
</Tooltip>
</ActionButton>

View File

@ -7,7 +7,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension'
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
}
export default class EditorManager {
private _editorView: EditorView | null = null
@ -91,11 +95,38 @@ export default class EditorManager {
}
}
clearDiagnostics(): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
}
addDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
forEachDiagnostic(this.editorView.state, function (diag) {
diagnostics.push(diag)
})
const uniqueDiagnostics = new Set<Diagnostic>()
diagnostics.forEach((diagnostic) => {
for (const knownDiagnostic of uniqueDiagnostics.values()) {
if (diagnosticIsEqual(diagnostic, knownDiagnostic)) {
return
}
}
uniqueDiagnostics.add(diagnostic)
})
this.editorView.dispatch(
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
}
undo() {
if (this._editorView) {
undo(this._editorView)

View File

@ -382,9 +382,14 @@ export class LanguageServerPlugin implements PluginValue {
try {
switch (notification.method) {
case 'textDocument/publishDiagnostics':
//const params = notification.params as PublishDiagnosticsParams
console.log(
'[lsp] [window/publishDiagnostics]',
this.client.getName(),
notification.params
)
const params = notification.params as PublishDiagnosticsParams
// this is sometimes slower than our actual typing.
//this.processDiagnostics(params)
this.processDiagnostics(params)
break
case 'window/logMessage':
console.log(

View File

@ -147,15 +147,33 @@ code {
#code-mirror-override .cm-activeLine,
#code-mirror-override .cm-activeLineGutter {
@apply bg-primary/10;
@apply bg-primary/5;
}
.dark #code-mirror-override .cm-activeLine,
.dark #code-mirror-override .cm-activeLineGutter {
@apply bg-primary/20;
@apply bg-chalkboard-70/20;
mix-blend-mode: lighten;
}
#code-mirror-override .cm-focused .cm-activeLine,
#code-mirror-override .cm-focused .cm-activeLineGutter {
@apply bg-primary/10;
}
.dark #code-mirror-override .cm-focused .cm-activeLine,
.dark #code-mirror-override .cm-focused .cm-activeLineGutter {
@apply bg-chalkboard-70/40;
}
#code-mirror-override .cm-matchingBracket {
@apply bg-primary/20;
}
.dark #code-mirror-override .cm-matchingBracket {
@apply bg-chalkboard-70/60;
}
#code-mirror-override .cm-gutters {
@apply bg-chalkboard-10/30;
}
@ -171,22 +189,8 @@ code {
@apply caret-chalkboard-10;
}
#code-mirror-override .cm-focused .cm-cursor {
width: 0px;
}
#code-mirror-override .cm-cursor {
display: block;
width: 1ch;
@apply mix-blend-multiply;
@apply border-l-primary;
}
.dark #code-mirror-override .cm-cursor {
@apply border-l-chalkboard-10;
}
#code-mirror-override.blink .cm-cursor {
animation: blink 1200ms ease-out infinite;
#code-mirror-override .cm-focused {
outline: none;
}
@keyframes blink {
@ -249,3 +253,10 @@ code {
.cm-ghostText * {
color: rgb(120, 120, 120, 0.8) !important;
}
@layer components {
kbd.hotkey {
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
}
}

View File

@ -41,7 +41,10 @@ export class KclManager {
engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => {
const ast = this.safeParse(code)
if (!ast) return
if (!ast) {
this.clearAst()
return
}
try {
const fmtAndStringify = (ast: Program) =>
JSON.stringify(parse(recast(ast)))
@ -91,7 +94,7 @@ export class KclManager {
set kclErrors(kclErrors) {
this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.setDiagnostics(diagnostics)
editorManager.addDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors)
}
@ -145,6 +148,18 @@ export class KclManager {
this._executeCallback = callback
}
clearAst() {
this._ast = {
body: [],
start: 0,
end: 0,
nonCodeMeta: {
nonCodeNodes: {},
start: [],
},
}
}
safeParse(code: string): Program | null {
try {
const ast = parse(code)
@ -185,6 +200,11 @@ export class KclManager {
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
this.isExecuting = true
await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({
@ -234,6 +254,7 @@ export class KclManager {
} = { updates: 'none' }
) {
await this.ensureWasmInit()
const newCode = recast(ast)
const newAst = this.safeParse(newCode)
if (!newAst) return
@ -243,6 +264,11 @@ export class KclManager {
await this?.engineCommandManager?.waitForReady
this._ast = { ...newAst }
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
const { logs, errors, programMemory } = await executeAst({
ast: newAst,
engineCommandManager: this.engineCommandManager,
@ -281,14 +307,20 @@ export class KclManager {
if (!force) return this._defferer(codeManager.code)
const ast = this.safeParse(codeManager.code)
if (!ast) return
if (!ast) {
this.clearAst()
return
}
this.ast = { ...ast }
return this.executeAst(ast, zoomToFit)
}
format() {
const originalCode = codeManager.code
const ast = this.safeParse(originalCode)
if (!ast) return
if (!ast) {
this.clearAst()
return
}
const code = recast(ast)
if (originalCode === code) return
@ -352,25 +384,55 @@ export class KclManager {
return this?.engineCommandManager?.defaultPlanes
}
showPlanes() {
if (!this.defaultPlanes) return
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
showPlanes(all = false) {
if (!this.defaultPlanes) return Promise.all([])
const thePromises = [
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false),
]
if (all) {
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negXy,
false
)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negYz,
false
)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(
this.defaultPlanes.negXz,
false
)
)
}
return Promise.all(thePromises)
}
hidePlanes() {
if (!this.defaultPlanes) return
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
}
exitEditMode() {
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
hidePlanes(all = false) {
if (!this.defaultPlanes) return Promise.all([])
const thePromises = [
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true),
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true),
]
if (all) {
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXy, true)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negYz, true)
)
thePromises.push(
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.negXz, true)
)
}
return Promise.all(thePromises)
}
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
@ -386,24 +448,11 @@ function defaultSelectionFilter(
) as SketchGroup | ExtrudeGroup
firstSketchOrExtrudeGroup &&
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_batch_req',
batch_id: uuidv4(),
responses: false,
requests: [
{
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: firstSketchOrExtrudeGroup.id,
},
},
{
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'edge', 'solid2d'],
},
},
],
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'edge', 'solid2d', 'curve'],
},
})
}

View File

@ -22,7 +22,7 @@ export default class CodeManager {
return
}
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN)
// TODO #819 remove zustand persistence logic in a few months
// short term migration, shouldn't make a difference for tauri app users
// anyway since that's filesystem based.
@ -68,7 +68,9 @@ export default class CodeManager {
this._currentFilePath = path
}
// This updates the code state and calls the updateState function.
/**
* This updates the code state and calls the updateState function.
*/
updateCodeState(code: string): void {
if (this._code !== code) {
this.code = code
@ -76,7 +78,9 @@ export default class CodeManager {
}
}
// Update the code in the editor.
/**
* Update the code in the editor.
*/
updateCodeEditor(code: string): void {
this.code = code
if (editorManager.editorView) {
@ -90,7 +94,9 @@ export default class CodeManager {
}
}
// Update the code, state, and the code the code mirror editor sees.
/**
* Update the code, state, and the code the code mirror editor sees.
*/
updateCodeStateEditor(code: string): void {
if (this._code !== code) {
this.code = code

View File

@ -58,6 +58,9 @@ function isHighlightSetEntity_type(
type WebSocketResponse = Models['WebSocketResponse_type']
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
type BatchResponseMap = {
[key: string]: Models['BatchResponse_type']
}
type ResultCommand = CommandInfo & {
type: 'result'
@ -147,7 +150,6 @@ export enum ConnectionError {
Unset = 0,
LongLoadingTime,
LostVideoStream,
ICENegotiate,
DataChannelError,
WebSocketError,
@ -168,8 +170,6 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
[ConnectionError.Unset]: '',
[ConnectionError.LongLoadingTime]:
'Loading is taking longer than expected...',
[ConnectionError.LostVideoStream]:
'Lost connection to video stream... Reconnecting...',
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
@ -315,8 +315,6 @@ class EngineConnection extends EventTarget {
if (next.type === EngineConnectionStateType.Disconnecting) {
const sub = next.value
if (sub.type === DisconnectingType.Error) {
console.log(sub)
// Record the last step we failed at.
// (Check the current state that we're about to override that
// it was a Connecting state.)
@ -759,8 +757,6 @@ class EngineConnection extends EventTarget {
// when assuming we're the only consumer or that all messages will
// be carefully formatted here.
console.log(event)
if (typeof event.data !== 'string') {
return
}
@ -781,7 +777,6 @@ class EngineConnection extends EventTarget {
`Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}`
)
console.log(artifactThatFailed)
} else {
console.error(`Error from server:\n${errorsString}`)
}
@ -872,7 +867,6 @@ class EngineConnection extends EventTarget {
this.pc
?.createOffer()
.then((offer: RTCSessionDescriptionInit) => {
console.log(offer)
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -944,7 +938,6 @@ class EngineConnection extends EventTarget {
case 'trickle_ice':
let candidate = resp.data?.candidate
console.log('trickle_ice: using this candidate: ', candidate)
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
break
@ -1326,7 +1319,8 @@ export class EngineCommandManager extends EventTarget {
)
if (
message.success &&
message.resp.type === 'modeling' &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
@ -1347,20 +1341,10 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
console.log('received track', mediaStream)
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
if (this.engineConnection) {
this.engineConnection.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Error,
value: {
error: ConnectionError.LostVideoStream,
},
},
}
}
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
setMediaStream(mediaStream)
@ -1400,19 +1384,60 @@ export class EngineCommandManager extends EventTarget {
id: string,
raw: WebSocketResponse
) {
if (message.type !== 'modeling') {
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
return
}
const modelingResponse = message.data.modeling_response
const command = this.artifactMap[id]
let modelingResponse: Models['OkModelingCmdResponse_type'] = {
type: 'empty',
}
if ('modeling_response' in message.data) {
modelingResponse = message.data.modeling_response
}
if (
command?.type === 'pending' &&
command.commandType === 'batch' &&
command?.additionalData?.type === 'batch-ids'
) {
command.additionalData.ids.forEach((id) => {
this.handleModelingCommand(message, id, raw)
})
if ('responses' in message.data) {
const batchResponse = message.data.responses as BatchResponseMap
// Iterate over the map of responses.
Object.entries(batchResponse).forEach(([key, response]) => {
// If the response is a success, we resolve the promise.
if ('response' in response && response.response) {
this.handleModelingCommand(
{
type: 'modeling',
data: {
modeling_response: response.response,
},
},
key,
{
request_id: key,
resp: {
type: 'modeling',
data: {
modeling_response: response.response,
},
},
success: true,
}
)
} else if ('errors' in response) {
this.handleFailedModelingCommand(key, {
request_id: key,
success: false,
errors: response.errors,
})
}
})
} else {
command.additionalData.ids.forEach((id) => {
this.handleModelingCommand(message, id, raw)
})
}
// batch artifact is just a container, we don't need to keep it
// once we process all the commands inside it
const resolve = command.resolve
@ -1421,7 +1446,6 @@ export class EngineCommandManager extends EventTarget {
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
raw,
})
return
@ -1673,7 +1697,6 @@ export class EngineCommandManager extends EventTarget {
(command.cmd.type === 'highlight_set_entity' ||
command.cmd.type === 'mouse_move' ||
command.cmd.type === 'camera_drag_move' ||
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any))
)
) {
@ -1688,7 +1711,6 @@ export class EngineCommandManager extends EventTarget {
command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage
) {
console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type
}
if (command.type === 'modeling_cmd_batch_req') {
@ -1702,7 +1724,6 @@ export class EngineCommandManager extends EventTarget {
if (
(cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move' ||
cmd.type === 'default_camera_look_at' ||
cmd.type === ('default_camera_perspective_settings' as any)) &&
this.engineConnection?.unreliableDataChannel &&
!forceWebsocket
@ -1756,7 +1777,7 @@ export class EngineCommandManager extends EventTarget {
command: EngineCommand
ast: Program
idToRangeMap?: { [key: string]: SourceRange }
}): Promise<any> {
}): Promise<ResolveCommand | void> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
@ -1825,11 +1846,13 @@ export class EngineCommandManager extends EventTarget {
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
) {
): Promise<ResolveCommand | void> {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') return command.path
if (command.type === 'solid3d_get_extrusion_face_info') {
@ -1890,11 +1913,13 @@ export class EngineCommandManager extends EventTarget {
idToRangeMap?: { [key: string]: SourceRange },
ast?: Program,
range?: SourceRange
) {
): Promise<ResolveCommand | void> {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
if (!idToRangeMap) {
throw new Error('idToRangeMap is required for batch commands')
@ -1914,7 +1939,7 @@ export class EngineCommandManager extends EventTarget {
resolve,
}
await Promise.all(
Promise.all(
commands.map((c) =>
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
)
@ -1926,7 +1951,7 @@ export class EngineCommandManager extends EventTarget {
rangeStr: string,
commandStr: string,
idToRangeStr: string
): Promise<any> {
): Promise<string | void> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
@ -1955,13 +1980,13 @@ export class EngineCommandManager extends EventTarget {
command,
ast: this.getAst(),
idToRangeMap,
}).then(({ raw }: { raw: WebSocketResponse | undefined | null }) => {
if (raw === undefined || raw === null) {
}).then((resp) => {
if (!resp) {
throw new Error(
'returning modeling cmd response to the rust side is undefined or null'
)
}
return JSON.stringify(raw)
return JSON.stringify(resp.raw)
})
}
commandResult(id: string): Promise<any> {

View File

@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory {
export async function coreDump(
coreDumpManager: CoreDumpManager,
openGithubIssue: boolean = false
): Promise<AppInfo> {
): Promise<CoreDumpInfo> {
try {
const dump: AppInfo = await coredump(coreDumpManager)
const dump: CoreDumpInfo = await coredump(coreDumpManager)
/* NOTE: this console output of the coredump should include the field
`github_issue_url` which is not in the uploaded coredump file.
`github_issue_url` is added after the file is uploaded
and is only needed for the openWindow operation which creates
a new GitHub issue for the user.
*/
if (openGithubIssue && dump.github_issue_url) {
openWindow(dump.github_issue_url)
} else {
console.error(
'github_issue_url undefined. Unable to create GitHub issue for coredump.'
)
}
console.log('CoreDump: final coredump', dump)
console.log('CoreDump: final coredump JSON', JSON.stringify(dump))
return dump
} catch (e: any) {
console.error('CoreDump: error', e)
throw new Error(`Error getting core dump: ${e}`)
}
}

View File

@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot'
import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env'
// This is a class for getting all the values from the JS world to pass to the Rust world
// for a core dump.
/**
* CoreDumpManager module
* - for getting all the values from the JS world to pass to the Rust world for a core dump.
* @module lib/coredump
* @class
*/
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast
export class CoreDumpManager {
engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null
@ -144,6 +151,293 @@ export class CoreDumpManager {
})
}
// Currently just a placeholder to begin loading singleton and xstate data into
getClientState(): Promise<string> {
/**
* Deep clone a JavaScript Object
* - NOTE: this function throws on parse errors from things like circular references
* - It is also synchronous and could be more performant
* - There is a whole rabbit hole to explore here if you like.
* - This works for our use case.
* @param {object} obj - The object to clone.
*/
const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj))
/**
* Check if a function is private method
*/
const isPrivateMethod = (key: string) => {
return key.length && key[0] === '_'
}
// Turn off verbose logging by default
const verboseLogging = false
/**
* Toggle verbose debug logging of step-by-step client state coredump data
*/
const debugLog = verboseLogging ? console.log : () => {}
console.warn('CoreDump: Gathering client state')
// Initialize the clientState object
let clientState = {
// singletons
engine_command_manager: {
artifact_map: {},
command_logs: [],
engine_connection: { state: { type: '' } },
default_planes: {},
scene_command_artifacts: {},
},
kcl_manager: {
ast: {},
kcl_errors: [],
},
scene_infra: {},
scene_entities_manager: {},
editor_manager: {},
// xstate
auth_machine: {},
command_bar_machine: {},
file_machine: {},
home_machine: {},
modeling_machine: {},
settings_machine: {},
}
debugLog('CoreDump: initialized clientState', clientState)
debugLog('CoreDump: globalThis.window', globalThis.window)
try {
// Singletons
// engine_command_manager
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
// artifact map - this.engineCommandManager.artifactMap
if (this.engineCommandManager?.artifactMap) {
debugLog(
'CoreDump: Engine Command Manager artifact map',
this.engineCommandManager.artifactMap
)
clientState.engine_command_manager.artifact_map = deepClone(
this.engineCommandManager.artifactMap
)
}
// command logs - this.engineCommandManager.commandLogs
if (this.engineCommandManager?.commandLogs) {
debugLog(
'CoreDump: Engine Command Manager command logs',
this.engineCommandManager.commandLogs
)
clientState.engine_command_manager.command_logs = deepClone(
this.engineCommandManager.commandLogs
)
}
// default planes - this.engineCommandManager.defaultPlanes
if (this.engineCommandManager?.defaultPlanes) {
debugLog(
'CoreDump: Engine Command Manager default planes',
this.engineCommandManager.defaultPlanes
)
clientState.engine_command_manager.default_planes = deepClone(
this.engineCommandManager.defaultPlanes
)
}
// engine connection state
if (this.engineCommandManager?.engineConnection?.state) {
debugLog(
'CoreDump: Engine Command Manager engine connection state',
this.engineCommandManager.engineConnection.state
)
clientState.engine_command_manager.engine_connection.state =
this.engineCommandManager.engineConnection.state
}
// in sequence - this.engineCommandManager.inSequence
if (this.engineCommandManager?.inSequence) {
debugLog(
'CoreDump: Engine Command Manager in sequence',
this.engineCommandManager.inSequence
)
;(clientState.engine_command_manager as any).in_sequence =
this.engineCommandManager.inSequence
}
// out sequence - this.engineCommandManager.outSequence
if (this.engineCommandManager?.outSequence) {
debugLog(
'CoreDump: Engine Command Manager out sequence',
this.engineCommandManager.outSequence
)
;(clientState.engine_command_manager as any).out_sequence =
this.engineCommandManager.outSequence
}
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
if (this.engineCommandManager?.sceneCommandArtifacts) {
debugLog(
'CoreDump: Engine Command Manager scene command artifacts',
this.engineCommandManager.sceneCommandArtifacts
)
clientState.engine_command_manager.scene_command_artifacts = deepClone(
this.engineCommandManager.sceneCommandArtifacts
)
}
// KCL Manager - globalThis?.window?.kclManager
const kclManager = (globalThis?.window as any)?.kclManager
debugLog('CoreDump: kclManager', kclManager)
if (kclManager) {
// KCL Manager AST
debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
if (kclManager?.ast) {
clientState.kcl_manager.ast = deepClone(kclManager.ast)
}
// KCL Errors
debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
if (kclManager?.kclErrors) {
clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors)
}
// KCL isExecuting
debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
if (kclManager?.isExecuting) {
;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
}
// KCL logs
debugLog('CoreDump: KCL logs', kclManager?.logs)
if (kclManager?.logs) {
;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs)
}
// KCL programMemory
debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
if (kclManager?.programMemory) {
;(clientState.kcl_manager as any).programMemory = deepClone(
kclManager.programMemory
)
}
// KCL wasmInitFailed
debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
if (kclManager?.wasmInitFailed) {
;(clientState.kcl_manager as any).wasmInitFailed =
kclManager.wasmInitFailed
}
}
// Scene Infra - globalThis?.window?.sceneInfra
const sceneInfra = (globalThis?.window as any)?.sceneInfra
debugLog('CoreDump: Scene Infra', sceneInfra)
if (sceneInfra) {
const sceneInfraSkipKeys = ['camControls']
const sceneInfraKeys = Object.keys(sceneInfra)
.sort()
.filter((entry) => {
return (
typeof sceneInfra[entry] !== 'function' &&
!sceneInfraSkipKeys.includes(entry)
)
})
debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
sceneInfraKeys.forEach((key: string) => {
debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
try {
;(clientState.scene_infra as any)[key] = sceneInfra[key]
} catch (error) {
console.error(
'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
error
)
}
})
}
// Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
const sceneEntitiesManager = (globalThis?.window as any)
?.sceneEntitiesManager
debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
if (sceneEntitiesManager) {
// Scene Entities Manager active segments
debugLog(
'CoreDump: Scene Entities Manager active segments',
sceneEntitiesManager?.activeSegments
)
if (sceneEntitiesManager?.activeSegments) {
;(clientState.scene_entities_manager as any).activeSegments =
deepClone(sceneEntitiesManager.activeSegments)
}
}
// Editor Manager - globalThis?.window?.editorManager
const editorManager = (globalThis?.window as any)?.editorManager
debugLog('CoreDump: editorManager', editorManager)
if (editorManager) {
const editorManagerSkipKeys = ['camControls']
const editorManagerKeys = Object.keys(editorManager)
.sort()
.filter((entry) => {
return (
typeof editorManager[entry] !== 'function' &&
!isPrivateMethod(entry) &&
!editorManagerSkipKeys.includes(entry)
)
})
debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
editorManagerKeys.forEach((key: string) => {
debugLog('CoreDump: Editor Manager', key, editorManager[key])
try {
;(clientState.editor_manager as any)[key] = deepClone(
editorManager[key]
)
} catch (error) {
console.error(
'CoreDump: unable to parse Editor Manager ' +
key +
' data due to ',
error
)
}
})
}
// enableMousePositionLogs - Not coredumped
// See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
debugLog(
'CoreDump: enableMousePositionLogs [not coredumped]',
(globalThis?.window as any)?.enableMousePositionLogs
)
// XState Machines
debugLog(
'CoreDump: xstate services',
(globalThis?.window as any)?.__xstate__?.services
)
debugLog('CoreDump: final clientState', clientState)
const clientStateJson = JSON.stringify(clientState)
debugLog('CoreDump: final clientState JSON', clientStateJson)
return Promise.resolve(clientStateJson)
} catch (error) {
console.error('CoreDump: unable to return data due to ', error)
return Promise.reject(JSON.stringify(error))
}
}
// Return a data URL (png format) of the screenshot of the current page.
screenshot(): Promise<string> {
return screenshot(this.htmlRef)

View File

@ -17,15 +17,17 @@ const prependRoutes =
)
}
type OnboardingPaths = {
[K in keyof typeof onboardingPaths]: `/onboarding${(typeof onboardingPaths)[K]}`
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`

View File

@ -540,7 +540,7 @@ function codeToIdSelections(
.filter(Boolean) as any
}
export function sendSelectEventToEngine(
export async function sendSelectEventToEngine(
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
el: HTMLVideoElement,
streamDimensions: { streamWidth: number; streamHeight: number }
@ -551,7 +551,7 @@ export function sendSelectEventToEngine(
el,
...streamDimensions,
})
const result: Promise<Models['SelectWithPoint_type']> = engineCommandManager
const result: Models['SelectWithPoint_type'] = await engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {

View File

@ -157,7 +157,7 @@ export function createSettings() {
),
}),
enableSSAO: new Setting<boolean>({
defaultValue: true,
defaultValue: false,
description:
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
validate: (v) => typeof v === 'boolean',

File diff suppressed because one or more lines are too long

View File

@ -11,98 +11,98 @@ import {
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IAbAFZN+AOwAWAIwAOYwE4AzGYBM+-ZosAaEAE9Eh62LP51ls+v0LMWt1awMAX3DnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BA9vQ0N1CwCxdVbdY1DnNwQzPp8zTTFje1D1QwtjSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmotNY3w7YysHRuNDTXV1bvdG7w-IKTcbaazWCzTEAxOZ4QiLJIrFL4dJZMAXUpXSrVDTGazPMQPR4GXRBAx-XoGfDWIadMx4n6EqEwuLwxLLVbrTbbNKYKBpLb8tAAUWgcAxMjk11uoHumn0+DEw2sJkMulCgWsFL6YnwfX0ELsYg61jMumZs1ZCXIACU4OgAATLWGiSSXKXYu4aLz4UwWBr6DqBYYUzSePXqUlBLxmyZmC2xeZs8gxB3UYjEJ2W+YSsoem5VL0IKy6z6EsJifQ2Czq0MTHzq8Yjfz6MQmBMu5NkABUuaxBZxCDD+DD5lJgUjxssFP0xl0I+0pm06uMmg8kSiIGwXPgpRZ83dFQHRYAtA0LCO2tZjGIHJNdB5fq5EK3dc1OsNGkrA+oO1bFoe0qFrKiAnlol7BDed5zo+FK+Doc7qhCNaVv4UwbkAA */
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */
id: 'Settings',
predictableActionArguments: true,
context: {} as ReturnType<typeof createSettings>,
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
entry: ['setThemeClass', 'setClientSideSceneUnits'],
on: {
'*': {
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'toastSuccess', 'persistSettings'],
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.app.onboardingStatus': {
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
target: 'persisting settings',
// No toast
actions: ['setSettingAtLevel'],
},
'set.app.themeColor': {
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
target: 'persisting settings',
// No toast
actions: ['setSettingAtLevel'],
},
'set.modeling.defaultUnit': {
target: 'idle',
internal: true,
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
],
},
'set.app.theme': {
target: 'idle',
internal: true,
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setThemeClass',
'setEngineTheme',
'persistSettings',
'setClientTheme',
],
},
'set.modeling.highlightEdges': {
target: 'idle',
internal: true,
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineEdges',
'persistSettings',
],
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
},
'Reset settings': {
target: 'idle',
internal: true,
target: 'persisting settings',
actions: [
'resetSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
'setClientTheme',
],
},
'Set all settings': {
target: 'idle',
internal: true,
target: 'persisting settings',
actions: [
'setAllSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
'setClientTheme',
],
},
},
},
'persisting settings': {
invoke: {
src: 'Persist settings',
id: 'persistSettings',
onDone: 'idle',
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {

View File

@ -1,8 +1,9 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
export default function CodeEditor() {
export default function OnboardingCodeEditor() {
useDemoCode()
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))

View File

@ -1,24 +1,19 @@
import { OnboardingButtons, useDismiss } from '.'
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
import { useEffect } from 'react'
import { bracket } from 'lib/exampleKcl'
import { codeManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { APP_NAME } from 'lib/constants'
import { onboardingPaths } from './paths'
import { sceneInfra } from 'lib/singletons'
export default function FutureWork() {
const { send } = useModelingContext()
const dismiss = useDismiss()
// Reset the code, the camera, and the modeling state
useDemoCode()
useEffect(() => {
// We do want to update both the state and editor here.
codeManager.updateCodeEditor(bracket)
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.executeCode(true, true)
}
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
sceneInfra.camControls.resetCameraPosition()
}, [send])
return (

View File

@ -1,9 +1,16 @@
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
import { bracketWidthConstantLine } from 'lib/exampleKcl'
export default function InteractiveNumbers() {
export default function OnboardingInteractiveNumbers() {
useDemoCode()
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
@ -33,8 +40,10 @@ export default function InteractiveNumbers() {
<kbd className={kbdClasses}>Option</kbd>) key
</li>
<li>
Hover over the number assigned to <code>width</code> on line{' '}
{bracketWidthConstantLine}
Hover over the number assigned to "width" on{' '}
<em>
<strong>line {bracketWidthConstantLine}</strong>
</em>
</li>
<li>Drag the number left and right to change its value</li>
</ol>

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
@ -10,7 +10,6 @@ import {
import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import { useEffect } from 'react'
import { codeManager, kclManager } from 'lib/singletons'
import { join } from '@tauri-apps/api/path'
import {
@ -92,7 +91,7 @@ function OnboardingWithNewFile() {
)
}
export default function Introduction() {
export default function OnboardingIntroduction() {
const {
settings: {
state: {
@ -112,9 +111,7 @@ export default function Introduction() {
const currentCode = codeManager.code
const isStarterCode = currentCode === '' || currentCode === bracket
useEffect(() => {
if (codeManager.code === '') codeManager.updateCodeEditor(bracket)
}, [])
useDemoCode()
return isStarterCode ? (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
@ -159,6 +156,12 @@ export default function Introduction() {
! We are trying to release as early as possible to get feedback from
users like you.
</p>
<p>
As you go through the onboarding, we'll be changing and resetting
your code occasionally, so that we can reference specific code
features. So hold off on writing production KCL code until you're
done with the onboarding 😉
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.INDEX}

View File

@ -1,11 +1,12 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { bracketThicknessCalculationLine } from 'lib/exampleKcl'
export default function ParametricModeling() {
export default function OnboardingParametricModeling() {
useDemoCode()
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
@ -44,8 +45,10 @@ export default function ParametricModeling() {
<p className="my-4">
We've received this sketch from a designer highlighting an{' '}
<em className="text-primary">aluminum bracket</em> they need for
this shelf:
<em>
<strong>aluminum bracket</strong>
</em>{' '}
they need for this shelf:
</p>
<figure className="my-4 w-2/3 mx-auto">
<img
@ -59,8 +62,8 @@ export default function ParametricModeling() {
<p className="my-4">
We are able to easily calculate the thickness of the material based
on the width of the bracket to meet a set safety factor on{' '}
<em className="text-primary">
line {bracketThicknessCalculationLine}
<em>
<strong>line {bracketThicknessCalculationLine}</strong>
</em>
.
</p>

View File

@ -1,6 +1,7 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore'
import { useEffect, useState } from 'react'
export default function UserMenu() {
const { buttonDownInStream } = useStore((s) => ({
@ -8,6 +9,20 @@ export default function UserMenu() {
}))
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false)
const buttonDescription = !avatarErrored ? 'your avatar' : 'the menu button'
// Set up error handling for the user's avatar image,
// so the onboarding text can be updated if it fails to load.
useEffect(() => {
const element = globalThis.document.querySelector(
'[data-testid="user-sidebar-toggle"] img'
)
if (element?.tagName === 'IMG') {
element.addEventListener('error', () => setAvatarErrored(true))
}
}, [])
return (
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
@ -20,8 +35,8 @@ export default function UserMenu() {
<section className="flex-1">
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click your avatar on the upper right to open the user menu. You can
change your settings, sign out, or request a feature.
Click {buttonDescription} in the upper right to open the user menu.
You can change your settings, sign out, or request a feature.
</p>
<p className="my-4">
We only support global settings at the moment, but we are working to

View File

@ -3,7 +3,7 @@ import { Outlet, useNavigate } from 'react-router-dom'
import Introduction from './Introduction'
import Camera from './Camera'
import Sketching from './Sketching'
import { useCallback } from 'react'
import { useCallback, useEffect } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import Streaming from './Streaming'
@ -19,9 +19,11 @@ import { paths } from 'lib/paths'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { ActionButton } from 'components/ActionButton'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { codeManager, editorManager } from 'lib/singletons'
import { bracket } from 'lib/exampleKcl'
export const kbdClasses =
'p-0.5 text-sm rounded-sm bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50'
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
export const onboardingRoutes = [
{
@ -75,6 +77,13 @@ export const onboardingRoutes = [
},
]
export function useDemoCode() {
useEffect(() => {
if (!editorManager.editorView) return
setTimeout(() => codeManager.updateCodeStateEditor(bracket))
}, [editorManager.editorView, codeManager.updateCodeStateEditor])
}
export function useNextClick(newStatus: string) {
const filePath = useAbsoluteFilePath()
const {
@ -94,17 +103,31 @@ export function useNextClick(newStatus: string) {
export function useDismiss() {
const filePath = useAbsoluteFilePath()
const {
settings: { send },
settings: { state, send },
} = useSettingsAuthContext()
const navigate = useNavigate()
return useCallback(() => {
const settingsCallback = useCallback(() => {
send({
type: 'set.app.onboardingStatus',
data: { level: 'user', value: 'dismissed' },
})
navigate(filePath)
}, [send, navigate, filePath])
}, [send])
/**
* A "listener" for the XState to return to "idle" state
* when the user dismisses the onboarding, using the callback above
*/
useEffect(() => {
if (
state.context.app.onboardingStatus.user === 'dismissed' &&
state.matches('idle')
) {
navigate(filePath)
}
}, [filePath, navigate, state])
return settingsCallback
}
// Get the 1-indexed step number of the current onboarding step

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

@ -297,9 +297,9 @@ dependencies = [
[[package]]
name = "bson"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
dependencies = [
"ahash",
"base64 0.13.1",
@ -406,9 +406,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.4"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [
"clap_builder",
"clap_derive",
@ -416,9 +416,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.2"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [
"anstream",
"anstyle",
@ -430,9 +430,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.4"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "databake"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
dependencies = [
"databake-derive",
"proc-macro2",
@ -681,9 +681,9 @@ dependencies = [
[[package]]
name = "databake-derive"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
@ -1152,9 +1152,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.28"
version = "0.14.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
dependencies = [
"bytes",
"futures-channel",
@ -1277,6 +1277,12 @@ dependencies = [
"hashbrown 0.14.3",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inflections"
version = "1.1.1"
@ -1369,7 +1375,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.58"
version = "0.1.60"
dependencies = [
"anyhow",
"approx",
@ -1399,6 +1405,7 @@ dependencies = [
"mime_guess",
"parse-display",
"pretty_assertions",
"pyo3",
"reqwest",
"ropey",
"schemars",
@ -1434,11 +1441,24 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "kcl-test-server"
version = "0.1.0"
dependencies = [
"anyhow",
"hyper",
"kcl-lib",
"pico-args",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "kittycad"
version = "0.3.3"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
dependencies = [
"anyhow",
"async-trait",
@ -1451,7 +1471,7 @@ dependencies = [
"format_serde_error",
"futures",
"http 0.2.12",
"itertools 0.12.1",
"itertools 0.13.0",
"log",
"mime_guess",
"parse-display",
@ -1556,6 +1576,15 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -1815,6 +1844,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project"
version = "1.1.5"
@ -1888,6 +1923,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -1943,6 +1984,69 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot 0.12.1",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.66",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.66",
]
[[package]]
name = "quick-xml"
version = "0.28.2"
@ -2037,9 +2141,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
@ -2378,9 +2482,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"bigdecimal",
"bytes",
@ -2395,9 +2499,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
@ -2492,9 +2596,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"indexmap 2.2.5",
"itoa",
@ -2524,9 +2628,9 @@ dependencies = [
[[package]]
name = "serde_tokenstream"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a00ffd23fd882d096f09fcaae2a9de8329a328628e86027e049ee051dc1621f"
checksum = "8790a7c3fe883e443eaa2af6f705952bc5d6e8671a220b9335c8cae92c037e74"
dependencies = [
"proc-macro2",
"quote",
@ -2776,6 +2880,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "task-local-extensions"
version = "0.1.4"
@ -2945,9 +3055,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.23.0"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
dependencies = [
"futures-util",
"log",
@ -2975,9 +3085,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.13"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
"serde",
"serde_spanned",
@ -2996,9 +3106,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.13"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap 2.2.5",
"serde",
@ -3157,10 +3267,12 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
dependencies = [
"chrono",
"serde_json",
"thiserror",
"ts-rs-macros",
"url",
@ -3169,8 +3281,9 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577"
dependencies = [
"proc-macro2",
"quote",
@ -3258,6 +3371,12 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unindent"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3266,9 +3385,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.0"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@ -3444,6 +3563,7 @@ dependencies = [
"console_error_panic_hook",
"futures",
"gloo-utils",
"hyper",
"image",
"js-sys",
"kcl-lib",

View File

@ -10,20 +10,21 @@ rust-version = "1.73"
crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
clap = "4.5.4"
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
clap = "4.5.7"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { workspace = true }
kittycad.workspace = true
serde_json = "1.0.116"
tokio = { version = "1.38.0", features = ["sync"] }
toml = "0.8.13"
toml = "0.8.14"
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42"
[dev-dependencies]
anyhow = "1"
hyper = { version = "0.14.29", features = ["server", "http1"] }
image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0"
@ -64,10 +65,11 @@ members = [
"derive-docs",
"kcl",
"kcl-macros",
"kcl-test-server",
]
[workspace.dependencies]
kittycad = { version = "0.3.3", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.3.5", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4"
[[test]]

View File

@ -1,24 +0,0 @@
[package]
name = "grackle"
version = "0.1.0"
edition = "2021"
description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
image = { version = "0.25.1", default-features = false, features = ["png"] }
kcl-lib = { path = "../kcl" }
kittycad = { workspace = true }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.61"
tokio = { version = "1.37.0", features = ["macros", "rt"] }
twenty-twenty = "0.8.0"
uuid = "1.8"
[dev-dependencies]
pretty_assertions = "1"
serde_json = "1.0.116"

View File

@ -11,7 +11,7 @@ repository = "https://github.com/KittyCAD/modeling-app"
proc-macro = true
[dependencies]
databake = "0.1.7"
databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"

View File

@ -0,0 +1,13 @@
[package]
name = "kcl-test-server"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
hyper = { version = "0.14.29", features = ["server"] }
kcl-lib = { path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }

View File

@ -0,0 +1,207 @@
//! Executes KCL programs.
//! The server reuses the same engine session for each KCL program it receives.
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use hyper::{
body::Bytes,
header::CONTENT_TYPE,
service::{make_service_fn, service_fn},
Body, Error, Response, Server,
};
use kcl_lib::{executor::ExecutorContext, settings::types::UnitLength, test_server::RequestBody};
use tokio::{
sync::{mpsc, oneshot},
task::JoinHandle,
time::sleep,
};
#[derive(Debug)]
pub struct ServerArgs {
/// What port this server should listen on.
pub listen_on: SocketAddr,
/// How many connections to establish with the engine.
pub num_engine_conns: u8,
}
impl ServerArgs {
pub fn parse(mut pargs: pico_args::Arguments) -> Result<Self, pico_args::Error> {
let args = ServerArgs {
listen_on: pargs
.opt_value_from_str("--listen-on")?
.unwrap_or("0.0.0.0:3333".parse().unwrap()),
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
};
println!("Config is {args:?}");
Ok(args)
}
}
/// Sent from the server to each worker.
struct WorkerReq {
/// A KCL program, in UTF-8.
body: Bytes,
/// A channel to send the HTTP response back.
resp: oneshot::Sender<Response<Body>>,
}
/// Each worker has a connection to the engine, and accepts
/// KCL programs. When it receives one (over the mpsc channel)
/// it executes it and returns the result via a oneshot channel.
fn start_worker(i: u8) -> mpsc::Sender<WorkerReq> {
println!("Starting worker {i}");
// Make a work queue for this worker.
let (tx, mut rx) = mpsc::channel(1);
tokio::task::spawn(async move {
let state = ExecutorContext::new_for_unit_test(UnitLength::Mm).await.unwrap();
println!("Worker {i} ready");
while let Some(req) = rx.recv().await {
let req: WorkerReq = req;
let resp = snapshot_endpoint(req.body, state.clone()).await;
if req.resp.send(resp).is_err() {
println!("\tWorker {i} exiting");
}
}
println!("\tWorker {i} exiting");
});
tx
}
struct ServerState {
workers: Vec<mpsc::Sender<WorkerReq>>,
req_num: AtomicUsize,
}
pub async fn start_server(args: ServerArgs) -> anyhow::Result<()> {
let ServerArgs {
listen_on,
num_engine_conns,
} = args;
let workers: Vec<_> = (0..num_engine_conns).map(start_worker).collect();
let state = Arc::new(ServerState {
workers,
req_num: 0.into(),
});
// In hyper, a `MakeService` is basically your server.
// It makes a `Service` for each connection, which manages the connection.
let make_service = make_service_fn(
// This closure is run for each connection.
move |_conn_info| {
// eprintln!("Connected to a client");
let state = state.clone();
async move {
// This is the `Service` which handles the connection.
// `service_fn` converts a function which returns a Response
// into a `Service`.
Ok::<_, Error>(service_fn(move |req| {
// eprintln!("Received a request");
let state = state.clone();
async move { handle_request(req, state).await }
}))
}
},
);
let server = Server::bind(&listen_on).serve(make_service);
println!("Listening on {listen_on}");
println!("PID is {}", std::process::id());
if let Err(e) = server.await {
eprintln!("Server error: {e}");
return Err(e.into());
}
Ok(())
}
async fn handle_request(req: hyper::Request<Body>, state3: Arc<ServerState>) -> Result<Response<Body>, Error> {
let body = hyper::body::to_bytes(req.into_body()).await?;
// Round robin requests between each available worker.
let req_num = state3.req_num.fetch_add(1, Ordering::Relaxed);
let worker_id = req_num % state3.workers.len();
// println!("Sending request {req_num} to worker {worker_id}");
let worker = state3.workers[worker_id].clone();
let (tx, rx) = oneshot::channel();
let req_sent = worker.send(WorkerReq { body, resp: tx }).await;
req_sent.unwrap();
let resp = rx.await.unwrap();
Ok(resp)
}
/// Execute a KCL program, then respond with a PNG snapshot.
/// KCL errors (from engine or the executor) respond with HTTP Bad Gateway.
/// Malformed requests are HTTP Bad Request.
/// Successful requests contain a PNG as the body.
async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body> {
let body = match serde_json::from_slice::<RequestBody>(body.as_ref()) {
Ok(bd) => bd,
Err(e) => return bad_request(format!("Invalid request JSON: {e}")),
};
let RequestBody { kcl_program, test_name } = body;
let parser = match kcl_lib::token::lexer(&kcl_program) {
Ok(ts) => kcl_lib::parser::Parser::new(ts),
Err(e) => return bad_request(format!("tokenization error: {e}")),
};
let program = match parser.ast() {
Ok(pr) => pr,
Err(e) => return bad_request(format!("Parse error: {e}")),
};
eprintln!("Executing {test_name}");
if let Err(e) = state.reset_scene().await {
return kcl_err(e);
}
// Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx);
let snapshot = match state.execute_and_prepare_snapshot(program).await {
Ok(sn) => sn,
Err(e) => return kcl_err(e),
};
let _ = done_tx.send(());
timer.abort();
eprintln!("\tServing response");
let png_bytes = snapshot.contents.0;
let mut resp = Response::new(Body::from(png_bytes));
resp.headers_mut().insert(CONTENT_TYPE, "image/png".parse().unwrap());
resp
}
fn bad_request(msg: String) -> Response<Body> {
eprintln!("\tBad request");
let mut resp = Response::new(Body::from(msg));
*resp.status_mut() = hyper::StatusCode::BAD_REQUEST;
resp
}
fn bad_gateway(msg: String) -> Response<Body> {
eprintln!("\tBad gateway");
let mut resp = Response::new(Body::from(msg));
*resp.status_mut() = hyper::StatusCode::BAD_GATEWAY;
resp
}
fn kcl_err(err: anyhow::Error) -> Response<Body> {
eprintln!("\tBad KCL");
bad_gateway(format!("{err}"))
}
fn time_until(done: oneshot::Receiver<()>) -> JoinHandle<()> {
tokio::task::spawn(async move {
let period = 10;
tokio::pin!(done);
for i in 1..=3 {
tokio::select! {
biased;
// If the test is done, no need for this timer anymore.
_ = &mut done => return,
_ = sleep(Duration::from_secs(period)) => {
eprintln!("\tTest has taken {}s", period * i);
},
};
}
})
}

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.58"
version = "0.1.60"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -16,9 +16,9 @@ async-recursion = "1.1.1"
async-trait = "0.1.80"
base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.4", default-features = false, optional = true }
clap = { version = "4.5.7", default-features = false, optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.18", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
@ -28,6 +28,7 @@ kittycad = { workspace = true, features = ["clap"] }
lazy_static = "1.4.0"
mime_guess = "2.0.4"
parse-display = "0.9.1"
pyo3 = {version = "0.21.2", optional = true}
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
@ -35,10 +36,9 @@ serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.116"
sha2 = "0.10.8"
thiserror = "1.0.61"
toml = "0.8.13"
# TODO: change this to a cargo release once 8.1.1 comes out
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
url = { version = "2.5.0", features = ["serde"] }
toml = "0.8.14"
ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"
@ -54,15 +54,19 @@ web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
tokio = { version = "1.38.0", features = ["full"] }
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] }
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features]
default = ["cli", "engine"]
cli = ["dep:clap"]
# For the lsp server, when run with stdout for rpc we want to disable println.
# This is used for editor extensions that use the lsp server.
disable-println = []
engine = []
pyo3 = ["dep:pyo3"]
[profile.release]
panic = "abort"

View File

@ -1,6 +1,11 @@
//! Data types for the AST.
use std::{collections::HashMap, fmt::Write, ops::RangeInclusive};
use std::{
collections::HashMap,
fmt::Write,
ops::RangeInclusive,
sync::{Arc, Mutex},
};
use anyhow::Result;
use databake::*;
@ -147,6 +152,21 @@ impl Program {
}
}
/// Check the provided Program for any lint findings.
pub fn lint<'a, RuleT>(&'a self, rule: RuleT) -> Result<Vec<crate::lint::Discovered>>
where
RuleT: crate::lint::rule::Rule<'a>,
{
let v = Arc::new(Mutex::new(vec![]));
crate::lint::walk(self, &|node: crate::lint::Node<'a>| {
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
findings.append(&mut rule.check(node)?);
Ok(true)
})?;
let x = v.lock().unwrap();
Ok(x.clone())
}
/// Returns the body item that includes the given character position.
pub fn get_body_item_for_position(&self, pos: usize) -> Option<&BodyItem> {
for item in &self.body {
@ -1076,7 +1096,12 @@ impl CallExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
format!(
"{}({})",
"{}{}({})",
if is_in_pipe {
"".to_string()
} else {
options.get_indentation(indentation_level)
},
self.callee.name,
self.arguments
.iter()
@ -1335,7 +1360,7 @@ impl VariableDeclaration {
indentation,
self.kind,
declaration.id.name,
declaration.init.recast(options, indentation_level, false)
declaration.init.recast(options, indentation_level, false).trim()
);
output
})
@ -1751,7 +1776,7 @@ impl ArrayExpression {
inner_indentation,
self.elements
.iter()
.map(|el| el.recast(options, indentation_level, false))
.map(|el| el.recast(options, indentation_level, is_in_pipe))
.collect::<Vec<String>>()
.join(format!(",\n{}", inner_indentation).as_str()),
if is_in_pipe {
@ -2678,7 +2703,8 @@ impl PipeExpression {
}
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
self.body
let pipe = self
.body
.iter()
.enumerate()
.map(|(index, statement)| {
@ -2710,7 +2736,8 @@ impl PipeExpression {
}
s
})
.collect::<String>()
.collect::<String>();
format!("{}{}", options.get_indentation(indentation_level), pipe)
}
/// Returns a hover value that includes the given character position.
@ -2997,6 +3024,7 @@ pub enum Hover {
/// Format options.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct FormatOptions {
@ -3265,6 +3293,132 @@ fn ghi = (x) => {
assert_eq!(symbols.len(), 7);
}
#[test]
fn test_recast_bug_fn_in_fn() {
let some_program_string = r#"// Start point (top left)
const zoo_x = -20
const zoo_y = 7
// Scale
const s = 1 // s = 1 -> height of Z is 13.4mm
// Depth
const d = 1
fn rect = (x, y, w, h) => {
startSketchOn('XY')
|> startProfileAt([x, y], %)
|> xLine(w, %)
|> yLine(h, %)
|> xLine(-w, %)
|> close(%)
|> extrude(d, %)
}
fn quad = (x1, y1, x2, y2, x3, y3, x4, y4) => {
startSketchOn('XY')
|> startProfileAt([x1, y1], %)
|> lineTo([x2, y2], %)
|> lineTo([x3, y3], %)
|> lineTo([x4, y4], %)
|> close(%)
|> extrude(d, %)
}
fn crosshair = (x, y) => {
startSketchOn('XY')
|> startProfileAt([x, y], %)
|> yLine(1, %)
|> yLine(-2, %)
|> yLine(1, %)
|> xLine(1, %)
|> xLine(-2, %)
}
fn z = (z_x, z_y) => {
const z_end_w = s * 8.4
const z_end_h = s * 3
const z_corner = s * 2
const z_w = z_end_w + 2 * z_corner
const z_h = z_w * 1.08130081300813
rect(z_x, z_y, z_end_w, -z_end_h)
rect(z_x + z_w, z_y, -z_corner, -z_corner)
rect(z_x + z_w, z_y - z_h, -z_end_w, z_end_h)
rect(z_x, z_y - z_h, z_corner, z_corner)
quad(z_x, z_y - z_h + z_corner, z_x + z_w - z_corner, z_y, z_x + z_w, z_y - z_corner, z_x + z_corner, z_y - z_h)
}
fn o = (c_x, c_y) => {
// Outer and inner radii
const o_r = s * 6.95
const i_r = 0.5652173913043478 * o_r
// Angle offset for diagonal break
const a = 7
// Start point for the top sketch
const o_x1 = c_x + o_r * cos((45 + a) / 360 * tau())
const o_y1 = c_y + o_r * sin((45 + a) / 360 * tau())
// Start point for the bottom sketch
const o_x2 = c_x + o_r * cos((225 + a) / 360 * tau())
const o_y2 = c_y + o_r * sin((225 + a) / 360 * tau())
// End point for the bottom startSketchAt
const o_x3 = c_x + o_r * cos((45 - a) / 360 * tau())
const o_y3 = c_y + o_r * sin((45 - a) / 360 * tau())
// Where is the center?
// crosshair(c_x, c_y)
startSketchOn('XY')
|> startProfileAt([o_x1, o_y1], %)
|> arc({
radius: o_r,
angle_start: 45 + a,
angle_end: 225 - a
}, %)
|> angledLine([45, o_r - i_r], %)
|> arc({
radius: i_r,
angle_start: 225 - a,
angle_end: 45 + a
}, %)
|> close(%)
|> extrude(d, %)
startSketchOn('XY')
|> startProfileAt([o_x2, o_y2], %)
|> arc({
radius: o_r,
angle_start: 225 + a,
angle_end: 360 + 45 - a
}, %)
|> angledLine([225, o_r - i_r], %)
|> arc({
radius: i_r,
angle_start: 45 - a,
angle_end: 225 + a - 360
}, %)
|> close(%)
|> extrude(d, %)
}
fn zoo = (x0, y0) => {
z(x0, y0)
o(x0 + s * 20, y0 - (s * 6.7))
o(x0 + s * 35, y0 - (s * 6.7))
}
zoo(zoo_x, zoo_y)
"#;
let tokens = crate::token::lexer(some_program_string).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_bug_extra_parens() {
let some_program_string = r#"// Ball Bearing
@ -3332,8 +3486,6 @@ const outsideRevolve = startSketchOn('XZ')
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
println!("{:#?}", program);
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
@ -3656,7 +3808,6 @@ const tabs_l = startSketchOn({
let tokens = crate::token::lexer(some_program_string).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
println!("{:#?}", program);
let recasted = program.recast(&Default::default(), 0);
// Its VERY important this comes back with zero new lines.

View File

@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use super::{Literal, Value};
use crate::ast::types::{Literal, Value};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]

View File

@ -4,8 +4,10 @@ use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::ConstraintLevel;
use crate::executor::{MemoryItem, SourceRange, UserVal};
use crate::{
ast::types::ConstraintLevel,
executor::{MemoryItem, SourceRange, UserVal},
};
/// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration,

View File

@ -3,6 +3,7 @@
use anyhow::Result;
use crate::coredump::CoreDump;
use serde_json::Value as JValue;
#[derive(Debug, Clone)]
pub struct CoreDumper {}
@ -55,6 +56,10 @@ impl CoreDump for CoreDumper {
Ok(crate::coredump::WebrtcStats::default())
}
async fn get_client_state(&self) -> Result<JValue> {
Ok(JValue::default())
}
async fn screenshot(&self) -> Result<String> {
// Take a screenshot of the engine.
todo!()

View File

@ -7,8 +7,13 @@ pub mod wasm;
use anyhow::Result;
use base64::Engine;
use kittycad::Client;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate.
use serde_json::Value as JValue;
use std::path::Path;
use uuid::Uuid;
#[async_trait::async_trait(?Send)]
pub trait CoreDump: Clone {
@ -27,25 +32,24 @@ pub trait CoreDump: Clone {
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
async fn get_client_state(&self) -> Result<JValue>;
/// Return a screenshot of the app.
async fn screenshot(&self) -> Result<String>;
/// Get a screenshot of the app and upload it to public cloud storage.
async fn upload_screenshot(&self) -> Result<String> {
async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
let screenshot = self.screenshot().await?;
let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
// Create the zoo client.
let mut zoo = kittycad::Client::new(self.token()?);
zoo.set_base_url(&self.base_api_url()?);
// Base64 decode the screenshot.
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
// Upload the screenshot.
let links = zoo
let links = zoo_client
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some("modeling-app/core-dump-screenshot.png".to_string()),
filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)),
content_type: Some("image/png".to_string()),
data,
}])
@ -60,12 +64,19 @@ pub trait CoreDump: Clone {
}
/// Dump the app info.
async fn dump(&self) -> Result<AppInfo> {
async fn dump(&self) -> Result<CoreDumpInfo> {
// Create the zoo client.
let mut zoo_client = kittycad::Client::new(self.token()?);
zoo_client.set_base_url(&self.base_api_url()?);
let coredump_id = uuid::Uuid::new_v4();
let client_state = self.get_client_state().await?;
let webrtc_stats = self.get_webrtc_stats().await?;
let os = self.os().await?;
let screenshot_url = self.upload_screenshot().await?;
let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
let mut app_info = AppInfo {
let mut core_dump_info = CoreDumpInfo {
id: coredump_id,
version: self.version()?,
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
timestamp: chrono::Utc::now(),
@ -74,18 +85,44 @@ pub trait CoreDump: Clone {
webrtc_stats,
github_issue_url: None,
pool: self.pool()?,
client_state,
};
app_info.set_github_issue_url(&screenshot_url)?;
Ok(app_info)
// pretty-printed JSON byte vector of the coredump.
let data = serde_json::to_vec_pretty(&core_dump_info)?;
// Upload the coredump.
let links = zoo_client
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)),
content_type: Some("application/json".to_string()),
data,
}])
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
if links.is_empty() {
anyhow::bail!("Failed to upload coredump");
}
let coredump_url = &links[0];
core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
Ok(core_dump_info)
}
}
/// The app info structure.
/// The Core Dump Info structure.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct AppInfo {
pub struct CoreDumpInfo {
/// The unique id for the core dump - this helps correlate uploaded files with coredump data.
pub id: Uuid,
/// The version of the app.
pub version: String,
/// The git revision of the app.
@ -95,45 +132,44 @@ pub struct AppInfo {
pub timestamp: chrono::DateTime<chrono::Utc>,
/// If the app is running in tauri or the browser.
pub tauri: bool,
/// The os info.
pub os: OsInfo,
/// The webrtc stats.
pub webrtc_stats: WebrtcStats,
/// A GitHub issue url to report the core dump.
/// This gets prepoulated with all the core dump info.
#[serde(skip_serializing_if = "Option::is_none")]
pub github_issue_url: Option<String>,
/// Engine pool the client is connected to.
pub pool: String,
/// The client state (singletons and xstate).
pub client_state: JValue,
}
impl AppInfo {
impl CoreDumpInfo {
/// Set the github issue url.
pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> {
pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
let labels = ["coredump", "bug", tauri_or_browser_label];
let body = format!(
r#"[Insert a description of the issue here]
r#"[Add a title above and insert a description of the issue here]
![Screenshot]({})
![Screenshot]({screenshot_url})
<details>
<summary><b>Core Dump</b></summary>
```json
{}
```
[{coredump_filename}]({coredump_url})
Reference ID: {coredump_id}
</details>
"#,
screenshot_url,
serde_json::to_string_pretty(&self)?
"#
);
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
// Note that `github_issue_url` is not included in the coredump file.
// It has already been encoded and uploaded at this point.
// The `github_issue_url` is used in openWindow in wasm.ts.
self.github_issue_url = Some(format!(
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
"KittyCAD",

View File

@ -4,6 +4,7 @@ use anyhow::Result;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{coredump::CoreDump, wasm::JsFuture};
use serde_json::Value as JValue;
#[wasm_bindgen(module = "/../../lib/coredump.ts")]
extern "C" {
@ -31,6 +32,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = getClientState, catch)]
fn get_client_state(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = screenshot, catch)]
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
}
@ -123,6 +127,27 @@ impl CoreDump for CoreDumper {
Ok(stats)
}
async fn get_client_state(&self) -> Result<JValue> {
let promise = self
.manager
.get_client_state()
.map_err(|e| anyhow::anyhow!("Failed to get promise from get client state: {:?}", e))?;
let value = JsFuture::from(promise)
.await
.map_err(|e| anyhow::anyhow!("Failed to get response from client state: {:?}", e))?;
// Parse the value as a string.
let s = value
.as_string()
.ok_or_else(|| anyhow::anyhow!("Failed to get string from response from client stat: `{:?}`", value))?;
let client_state: JValue =
serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse client state: {:?}", e))?;
Ok(client_state)
}
async fn screenshot(&self) -> Result<String> {
let promise = self
.manager

View File

@ -1,15 +1,17 @@
//! Functions for generating docs for our stdlib functions.
use crate::std::Primitive;
use std::path::Path;
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent,
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
};
use crate::std::Primitive;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@ -63,9 +65,10 @@ impl StdLibFnArg {
pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> {
if self.type_ == "SketchGroup"
|| self.type_ == "ExtrudeGroup"
|| self.type_ == "SketchSurface"
|| self.type_ == "SketchGroupSet"
|| self.type_ == "ExtrudeGroup"
|| self.type_ == "ExtrudeGroupSet"
|| self.type_ == "SketchSurface"
{
return Ok(Some((index, format!("${{{}:{}}}", index, "%"))));
}

View File

@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Result};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use kittycad::types::{WebSocketRequest, WebSocketResponse};
use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg;
@ -40,23 +40,54 @@ pub struct TcpRead {
stream: futures::stream::SplitStream<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>>,
}
/// Occurs when client couldn't read from the WebSocket to the engine.
// #[derive(Debug)]
pub enum WebSocketReadError {
/// Could not read a message due to WebSocket errors.
Read(tokio_tungstenite::tungstenite::Error),
/// WebSocket message didn't contain a valid message that the KCL Executor could parse.
Deser(anyhow::Error),
}
impl From<anyhow::Error> for WebSocketReadError {
fn from(e: anyhow::Error) -> Self {
Self::Deser(e)
}
}
impl TcpRead {
pub async fn read(&mut self) -> Result<WebSocketResponse> {
pub async fn read(&mut self) -> std::result::Result<WebSocketResponse, WebSocketReadError> {
let Some(msg) = self.stream.next().await else {
anyhow::bail!("Failed to read from websocket");
return Err(anyhow::anyhow!("Failed to read from WebSocket").into());
};
let msg: WebSocketResponse = match msg? {
WsMsg::Text(text) => serde_json::from_str(&text)?,
WsMsg::Binary(bin) => bson::from_slice(&bin)?,
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
let msg = match msg {
Ok(msg) => msg,
Err(e) if matches!(e, tokio_tungstenite::tungstenite::Error::Protocol(_)) => {
return Err(WebSocketReadError::Read(e))
}
Err(e) => return Err(anyhow::anyhow!("Error reading from engine's WebSocket: {e}").into()),
};
let msg: WebSocketResponse = match msg {
WsMsg::Text(text) => serde_json::from_str(&text)
.map_err(anyhow::Error::from)
.map_err(WebSocketReadError::from)?,
WsMsg::Binary(bin) => bson::from_slice(&bin)
.map_err(anyhow::Error::from)
.map_err(WebSocketReadError::from)?,
other => return Err(anyhow::anyhow!("Unexpected WebSocket message from engine API: {other}").into()),
};
Ok(msg)
}
}
#[derive(Debug)]
pub struct TcpReadHandle {
handle: Arc<tokio::task::JoinHandle<Result<()>>>,
handle: Arc<tokio::task::JoinHandle<Result<(), WebSocketReadError>>>,
}
impl std::fmt::Debug for TcpReadHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TcpReadHandle")
}
}
impl Drop for TcpReadHandle {
@ -149,15 +180,47 @@ impl EngineConnection {
loop {
match tcp_read.read().await {
Ok(ws_resp) => {
for e in ws_resp.errors.iter().flatten() {
println!("got error message: {e}");
// If we got a batch response, add all the inner responses.
if let Some(kittycad::types::OkWebSocketResponseData::ModelingBatch { responses }) =
&ws_resp.resp
{
for (resp_id, batch_response) in responses {
let id: uuid::Uuid = resp_id.parse().unwrap();
if let Some(response) = &batch_response.response {
responses_clone.insert(
id,
kittycad::types::WebSocketResponse {
request_id: Some(id),
resp: Some(kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
}),
errors: None,
success: Some(true),
},
);
} else {
responses_clone.insert(
id,
kittycad::types::WebSocketResponse {
request_id: Some(id),
resp: None,
errors: batch_response.errors.clone(),
success: Some(false),
},
);
}
}
}
if let Some(id) = ws_resp.request_id {
responses_clone.insert(id, ws_resp.clone());
}
}
Err(e) => {
println!("got ws error: {:?}", e);
match &e {
WebSocketReadError::Read(e) => eprintln!("could not read from WS: {:?}", e),
WebSocketReadError::Deser(e) => eprintln!("could not deserialize msg from WS: {:?}", e),
}
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
return Err(e);
}
@ -212,7 +275,7 @@ impl EngineManager for EngineConnection {
source_range: crate::executor::SourceRange,
cmd: kittycad::types::WebSocketRequest,
_id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
) -> Result<OkWebSocketResponseData, KclError> {
) -> Result<WebSocketResponse, KclError> {
let (tx, rx) = oneshot::channel();
// Send the request to the engine, via the actor.
@ -257,14 +320,7 @@ impl EngineManager for EngineConnection {
}
// We pop off the responses to cleanup our mappings.
if let Some((_, resp)) = self.responses.remove(&id) {
return if let Some(data) = &resp.resp {
Ok(data.clone())
} else {
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", resp.errors),
source_ranges: vec![source_range],
}))
};
return Ok(resp);
}
}

View File

@ -1,10 +1,13 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use std::sync::{Arc, Mutex};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result;
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use crate::{errors::KclError, executor::DefaultPlanes};
@ -37,13 +40,43 @@ impl crate::engine::EngineManager for EngineConnection {
async fn inner_send_modeling_cmd(
&self,
_id: uuid::Uuid,
id: uuid::Uuid,
_source_range: crate::executor::SourceRange,
_cmd: kittycad::types::WebSocketRequest,
cmd: kittycad::types::WebSocketRequest,
_id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
) -> Result<OkWebSocketResponseData, KclError> {
Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
})
) -> Result<WebSocketResponse, KclError> {
match cmd {
WebSocketRequest::ModelingCmdBatchReq {
ref requests,
batch_id: _,
responses: _,
} => {
// Create the empty responses.
let mut responses = HashMap::new();
for request in requests {
responses.insert(
request.cmd_id.to_string(),
kittycad::types::BatchResponse {
response: Some(kittycad::types::OkModelingCmdResponse::Empty {}),
errors: None,
},
);
}
Ok(WebSocketResponse {
request_id: Some(id),
resp: Some(OkWebSocketResponseData::ModelingBatch { responses }),
success: Some(true),
errors: None,
})
}
_ => Ok(WebSocketResponse {
request_id: Some(id),
resp: Some(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
}),
success: Some(true),
errors: None,
}),
}
}
}

View File

@ -130,7 +130,7 @@ impl crate::engine::EngineManager for EngineConnection {
source_range: crate::executor::SourceRange,
cmd: kittycad::types::WebSocketRequest,
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
) -> Result<kittycad::types::OkWebSocketResponseData, KclError> {
) -> Result<kittycad::types::WebSocketResponse, KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
@ -182,18 +182,6 @@ impl crate::engine::EngineManager for EngineConnection {
})
})?;
if let Some(data) = &ws_result.resp {
Ok(data.clone())
} else if let Some(errors) = &ws_result.errors {
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", errors),
source_ranges: vec![source_range],
}))
} else {
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", ws_result),
source_ranges: vec![source_range],
}))
}
Ok(ws_result)
}
}

View File

@ -47,13 +47,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: crate::executor::SourceRange,
cmd: kittycad::types::WebSocketRequest,
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError>;
) -> Result<kittycad::types::WebSocketResponse, crate::errors::KclError>;
async fn clear_scene(&self, source_range: crate::executor::SourceRange) -> Result<(), crate::errors::KclError> {
self.send_modeling_cmd(
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
kittycad::types::ModelingCmd::SceneClearAll {},
&kittycad::types::ModelingCmd::SceneClearAll {},
)
.await?;
@ -67,12 +67,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(())
}
async fn send_modeling_cmd(
// Add a modeling command to the batch but don't fire it right away.
async fn batch_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
cmd: &kittycad::types::ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let req = WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id,
@ -81,16 +82,17 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch.
self.batch().lock().unwrap().push((req, source_range));
// If the batch only has this one command that expects a return value,
// fire it right away, or if we want to flush batch queue.
let is_sending = is_cmd_with_return_values(&cmd);
Ok(())
}
// Return a fake modeling_request empty response.
if !is_sending {
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
});
}
/// Send the modeling cmd and wait for the response.
async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
self.batch_modeling_cmd(id, source_range, &cmd).await?;
// Flush the batch queue.
self.flush_batch(source_range).await
@ -124,7 +126,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
let batched_requests = WebSocketRequest::ModelingCmdBatchReq {
requests,
batch_id: uuid::Uuid::new_v4(),
responses: false,
responses: true,
};
let final_req = if self.batch().lock().unwrap().len() == 1 {
@ -155,23 +157,41 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.batch().lock().unwrap().clear();
// We pop off the responses to cleanup our mappings.
let id_final = match final_req {
match final_req {
WebSocketRequest::ModelingCmdBatchReq {
requests: _,
ref requests,
batch_id,
responses: _,
} => batch_id,
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => cmd_id,
_ => {
return Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
}));
}
};
} => {
// Get the last command ID.
let last_id = requests.last().unwrap().cmd_id;
let ws_resp = self
.inner_send_modeling_cmd(batch_id, source_range, final_req, id_to_source_range.clone())
.await?;
let response = self.parse_websocket_response(ws_resp, source_range)?;
self.inner_send_modeling_cmd(id_final, source_range, final_req, id_to_source_range)
.await
// If we have a batch response, we want to return the specific id we care about.
if let kittycad::types::OkWebSocketResponseData::ModelingBatch { responses } = &response {
self.parse_batch_responses(last_id, id_to_source_range, responses.clone())
} else {
// We should never get here.
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to get batch response: {:?}", response),
source_ranges: vec![source_range],
}))
}
}
WebSocketRequest::ModelingCmdReq { cmd: _, cmd_id } => {
let ws_resp = self
.inner_send_modeling_cmd(cmd_id, source_range, final_req, id_to_source_range)
.await?;
self.parse_websocket_response(ws_resp, source_range)
}
_ => Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
})),
}
}
async fn make_default_plane(
@ -186,10 +206,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into();
let plane_id = uuid::Uuid::new_v4();
self.send_modeling_cmd(
self.batch_modeling_cmd(
plane_id,
source_range,
ModelingCmd::MakePlane {
&ModelingCmd::MakePlane {
clobber: false,
origin: default_origin,
size: default_size,
@ -202,10 +222,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
if let Some(color) = color {
// Set the color.
self.send_modeling_cmd(
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
ModelingCmd::PlaneSetColor { color, plane_id },
&ModelingCmd::PlaneSetColor { color, plane_id },
)
.await?;
}
@ -312,62 +332,79 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
neg_yz: planes[&PlaneName::NegYz],
})
}
}
pub fn is_cmd_with_return_values(cmd: &kittycad::types::ModelingCmd) -> bool {
let (kittycad::types::ModelingCmd::Export { .. }
| kittycad::types::ModelingCmd::Extrude { .. }
| kittycad::types::ModelingCmd::DefaultCameraLookAt { .. }
| kittycad::types::ModelingCmd::DefaultCameraFocusOn { .. }
| kittycad::types::ModelingCmd::DefaultCameraGetSettings { .. }
| kittycad::types::ModelingCmd::DefaultCameraPerspectiveSettings { .. }
| kittycad::types::ModelingCmd::DefaultCameraZoom { .. }
| kittycad::types::ModelingCmd::SketchModeDisable { .. }
| kittycad::types::ModelingCmd::ObjectBringToFront { .. }
| kittycad::types::ModelingCmd::SelectWithPoint { .. }
| kittycad::types::ModelingCmd::HighlightSetEntity { .. }
| kittycad::types::ModelingCmd::EntityGetChildUuid { .. }
| kittycad::types::ModelingCmd::EntityGetNumChildren { .. }
| kittycad::types::ModelingCmd::EntityGetParentId { .. }
| kittycad::types::ModelingCmd::EntityGetAllChildUuids { .. }
| kittycad::types::ModelingCmd::CameraDragMove { .. }
| kittycad::types::ModelingCmd::CameraDragEnd { .. }
| kittycad::types::ModelingCmd::SelectGet { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllEdgeFaces { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllOppositeEdges { .. }
| kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetNextAdjacentEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { .. }
| kittycad::types::ModelingCmd::GetEntityType { .. }
| kittycad::types::ModelingCmd::CurveGetControlPoints { .. }
| kittycad::types::ModelingCmd::CurveGetType { .. }
| kittycad::types::ModelingCmd::MouseClick { .. }
| kittycad::types::ModelingCmd::TakeSnapshot { .. }
| kittycad::types::ModelingCmd::PathGetInfo { .. }
| kittycad::types::ModelingCmd::PathGetCurveUuidsForVertices { .. }
| kittycad::types::ModelingCmd::PathGetVertexUuids { .. }
| kittycad::types::ModelingCmd::CurveGetEndPoints { .. }
| kittycad::types::ModelingCmd::FaceIsPlanar { .. }
| kittycad::types::ModelingCmd::FaceGetPosition { .. }
| kittycad::types::ModelingCmd::FaceGetGradient { .. }
| kittycad::types::ModelingCmd::PlaneIntersectAndProject { .. }
| kittycad::types::ModelingCmd::ImportFiles { .. }
| kittycad::types::ModelingCmd::Mass { .. }
| kittycad::types::ModelingCmd::Volume { .. }
| kittycad::types::ModelingCmd::Density { .. }
| kittycad::types::ModelingCmd::SurfaceArea { .. }
| kittycad::types::ModelingCmd::CenterOfMass { .. }
| kittycad::types::ModelingCmd::GetSketchModePlane { .. }
| kittycad::types::ModelingCmd::EntityGetDistance { .. }
| kittycad::types::ModelingCmd::EntityLinearPattern { .. }
| kittycad::types::ModelingCmd::EntityCircularPattern { .. }
| kittycad::types::ModelingCmd::ZoomToFit { .. }
| kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { .. }) = cmd
else {
return false;
};
fn parse_websocket_response(
&self,
response: kittycad::types::WebSocketResponse,
source_range: crate::executor::SourceRange,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
if let Some(data) = &response.resp {
Ok(data.clone())
} else if let Some(errors) = &response.errors {
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", errors),
source_ranges: vec![source_range],
}))
} else {
// We should never get here.
Err(KclError::Engine(KclErrorDetails {
message: "Modeling command failed: no response or errors".to_string(),
source_ranges: vec![source_range],
}))
}
}
true
fn parse_batch_responses(
&self,
// The last response we are looking for.
id: uuid::Uuid,
// The mapping of source ranges to command IDs.
id_to_source_range: std::collections::HashMap<uuid::Uuid, crate::executor::SourceRange>,
// The response from the engine.
responses: HashMap<String, kittycad::types::BatchResponse>,
) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
// Iterate over the responses and check for errors.
for (cmd_id, resp) in responses.iter() {
let cmd_id = uuid::Uuid::parse_str(cmd_id).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to parse command ID: {:?}", e),
source_ranges: vec![id_to_source_range[&id]],
})
})?;
if let Some(errors) = resp.errors.as_ref() {
// Get the source range for the command.
let source_range = id_to_source_range.get(&cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
})?;
return Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command failed: {:?}", errors),
source_ranges: vec![source_range],
}));
}
if let Some(response) = resp.response.as_ref() {
if cmd_id == id {
// This is the response we care about.
return Ok(kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: response.clone(),
});
} else {
// Continue the loop if this is not the response we care about.
continue;
}
}
}
// Return an error that we did not get an error or the response we wanted.
// This should never happen but who knows.
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to find response for command ID: {:?}", id),
source_ranges: vec![],
}))
}
}
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]

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