Compare commits

...

75 Commits

Author SHA1 Message Date
66ba60dc8e Bump to v0.7.0 (#524) 2023-09-14 07:35:08 -04:00
8fcc8cdd17 Change updater from .nsis to .msi, add EV Code Sign to it (#435)
* Change updater to msi over nsis, more common to sign

* Add sign-windows-msi from docs

* Add secrets and enable step

* Update ci.yml

* Add todo and fix download.json
2023-09-14 07:04:11 -04:00
bba9bdc563 Add logomark, update README (#412) 2023-09-14 05:16:24 +00:00
760a180f56 Convert to radians/degrees using Rust methods (#516)
use to_radians and to_degrees
2023-09-13 22:25:41 -06:00
0eeff8cb45 Add @replit/codemirror-interact (#500)
* Add @replit/codemirror-interact

Signed off by Frank Noirot <frank@kittycad.io>

* Add support for Alt + Meta + Shift for 0.01 increment

* Remove pixelsPerIncrement
This doesn't work because we have to rely on movementX, which means
only mouse movements of 3px or greater register while dragging. Although
I would eventually like to control the screen speed of incrementing,
I'd rather have consistent behavior for now.

Signed off by Frank Noirot <frank@kittycad.io>

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-14 04:03:51 +00:00
3c76721159 Fix id source range mapping with path_get_info (#517) 2023-09-14 13:49:59 +10:00
6ac79ae645 Move app.tsx effects (#513)
* move useSetupEngineManager

* move code eval

* add comment
2023-09-14 10:47:55 +10:00
90d7c33c92 add roadmap md file (#514) 2023-09-13 17:35:31 -07:00
e02bc76bdb add ast explorer (#460)
* add crude ast explorer

* tsc
2023-09-14 08:51:23 +10:00
0466f04d82 add sin cos tan to stdlib and make sure you cant redeclare a stdlib fn (#497)
more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 15:09:07 -07:00
f8ed830b60 Bump tauri from 1.3.0 to 1.4.1 in /src-tauri (#490)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 1.3.0 to 1.4.1.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v1.3...tauri-v1.4.1)

---
updated-dependencies:
- dependency-name: tauri
  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>
2023-09-13 14:15:46 -07:00
b7ca91bf6d Bump oauth2 from 4.4.1 to 4.4.2 in /src-tauri (#489)
Bumps [oauth2](https://github.com/ramosbugs/oauth2-rs) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/ramosbugs/oauth2-rs/releases)
- [Commits](https://github.com/ramosbugs/oauth2-rs/compare/4.4.1...4.4.2)

---
updated-dependencies:
- dependency-name: oauth2
  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>
2023-09-13 13:46:17 -07:00
2261f92b0b Bump anyhow from 1.0.71 to 1.0.75 in /src-tauri (#488)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.71 to 1.0.75.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.71...1.0.75)

---
updated-dependencies:
- dependency-name: anyhow
  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>
2023-09-13 13:46:07 -07:00
bbe9e621b1 Bump toml from 0.7.3 to 0.8.0 in /src-tauri (#493)
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.3 to 0.8.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.3...toml-v0.8.0)

---
updated-dependencies:
- dependency-name: toml
  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>
2023-09-13 13:45:59 -07:00
bf087d760b Bump thiserror from 1.0.47 to 1.0.48 in /src/wasm-lib (#375)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.47 to 1.0.48.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.47...1.0.48)

---
updated-dependencies:
- dependency-name: thiserror
  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>
2023-09-13 13:45:49 -07:00
a4353c63fd Bump tauri-plugin-fs-extra from 7e58dc8 to 5b814f5 in /src-tauri (#491)
Bumps [tauri-plugin-fs-extra](https://github.com/tauri-apps/plugins-workspace) from `7e58dc8` to `5b814f5`.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](7e58dc8502...5b814f56e6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 13:45:36 -07:00
c438d11c3d make .4 parse as number (#492)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 13:10:55 -07:00
43284e33c8 Bump toml from 0.6.0 to 0.7.3 in /src-tauri (#277)
Bumps [toml](https://github.com/toml-rs/toml) from 0.6.0 to 0.7.3.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.6.0...toml-v0.7.3)

---
updated-dependencies:
- dependency-name: toml
  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>
2023-09-13 12:50:52 -07:00
77dce7f0dd Bump syn from 2.0.32 to 2.0.33 in /src/wasm-lib (#485)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.32 to 2.0.33.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.32...2.0.33)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:50:39 -07:00
d559862051 Bump proc-macro2 from 1.0.66 to 1.0.67 in /src/wasm-lib (#486)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.66...1.0.67)

---
updated-dependencies:
- dependency-name: proc-macro2
  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>
2023-09-13 12:50:22 -07:00
7382ed87ba Bump tokio from 1.29.1 to 1.32.0 in /src-tauri (#280)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.32.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:23:56 -07:00
3324ed31de Bump serde_json from 1.0.96 to 1.0.106 in /src-tauri (#429)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.96 to 1.0.106.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.96...v1.0.106)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:23:43 -07:00
ba9dbc2205 Bump rustls-webpki from 0.101.1 to 0.101.5 in /src-tauri (#449)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.101.1 to 0.101.5.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.101.1...v/0.101.5)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 12:23:32 -07:00
b0028d4874 add basic docs for lang things (#484)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:59:21 -07:00
9e6be9651c Bump webpki from 0.22.0 to 0.22.1 in /src-tauri (#399)
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1.
- [Commits](https://github.com/briansmith/webpki/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 11:54:34 -07:00
b145ab0106 Bump clap from 4.4.2 to 4.4.3 in /src/wasm-lib (#459)
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.2 to 4.4.3.
- [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/v4.4.2...v4.4.3)

---
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>
2023-09-13 11:54:13 -07:00
84e0fbb70f Bump serde from 1.0.186 to 1.0.188 in /src/wasm-lib (#330)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.186 to 1.0.188.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.186...v1.0.188)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 11:54:02 -07:00
990605bbea fn required and can only be used for fn not var (#483)
* fixups

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

* fix javascript tests

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

* fix clippy

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:42:09 -07:00
d075c4ad13 Bump syn from 2.0.29 to 2.0.32 in /src/wasm-lib (#427)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.29 to 2.0.32.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.29...2.0.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-13 11:12:18 -07:00
a3f41f5519 Bump serde_json from 1.0.105 to 1.0.106 in /src/wasm-lib (#428)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.105 to 1.0.106.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.105...v1.0.106)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2023-09-13 11:00:50 -07:00
cb173e2850 Error on keywords used in wrong way (#481)
* error if someone is using keyword where they should not

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

* updates

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

* cleanup

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 10:51:03 -07:00
87cd3b67f4 allow ranges in arrays to work (#476)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 10:03:28 -07:00
fe3ee3806e Fix array expressions (#473)
* fix array expressions

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

* fix-tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 09:23:24 -07:00
c9ed6c724c Fix path for starting second sketch (#474) 2023-09-13 11:49:19 -04:00
a5fa259d55 fixes obj.thing stuff and member expressions and bugs (#461)
* add failing tests for loops

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

* fix obj["thing"] and obj.thing complex cases

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

* fix clippy

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

* remove println

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

* fix test

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

* fixes more tests

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

* fixups

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

* fixups

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

* fix tests

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

* add more tests

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

* more test fixes

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

* fixes

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

* last test fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 07:23:14 -07:00
33822b5a19 Bump to v0.6.1 (#465) 2023-09-13 06:33:11 -04:00
a2a4daebe3 fix move cmd order (#462) 2023-09-13 07:42:42 +00:00
a17ede50bd Build endpoint for download page on website (#451) 2023-09-13 03:03:13 +00:00
2d452f80d1 ts-rs changes (#450)
* initial changes

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>

* fixes

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

* updates

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

* bust cache

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

* add dumb shit

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

* Revert "add dumb shit"

This reverts commit 638e9cf72f75e1ad08fb6b22d2a7b143ab7e06e5.

* Revert "bust cache"

This reverts commit fd6f53ba0757d635190aa82d4b055a83755f3027.

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-13 11:10:27 +10:00
cf39c08428 Bump to v0.6.0 (#442) 2023-09-12 18:48:12 -04:00
2f25564fcc Fix PathError on Linux builds (#441)
* Path Error on Linux builds
Fixes #438

* Add fallback to homeDir
2023-09-12 18:46:35 -04:00
fd2ed8acbd Sketch on plane WIP (#309)
create default planes wip
2023-09-12 22:36:47 +00:00
5f3e1cfb6c ability to test stuff against a real executor (#448)
* ability to test stuff against a real executor

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

* add another regression test

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

* fix clippy

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

* updates

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

* figure out why hanging

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

* ficxes

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-12 14:59:40 -07:00
ee767afc3f Franknoirot/online indicator (#443)
* Add network health indicator to AppHeader

* Add tests for network detection responsiveness

* Format test file
2023-09-12 14:58:59 -04:00
8071eb6f8a Fix: allow the error from getUser to really be thrown (#437)
We have been catching the error thrown by the getUser fetch,
but since our state machine needs to fire onError we should
actually *not* wrap it in a try/catch statement.

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-12 00:37:54 +00:00
11f789e980 fixes angledLine (#436)
* fixes

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

* tests

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

* fix other tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-11 17:14:41 -07:00
3f82522fe9 dont set globally that we are not in a pipe (#432)
* dont set globally that we are not in a pipe

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

* updates

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

* fix 0-l case as well

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

* updates

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

* cleanup

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

* fix unary expression as well, add test

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

* fix array expressions in functions

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

* more options

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

* updates

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

* u[dates

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>

* add a test

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

* updates

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

* Revert "updates"

This reverts commit 3cf06388db.

* fix tets

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-11 15:15:37 -07:00
c5cb0e2fd4 Add "Trackpad Friendly" camera control setting inspired by Blender (#431)
* Refactor: rename CADProgram to CameraSystem

* Fix buttonDownInStream always set to 0
This is problematic because the left mouse
button ID is actually 0. If no button is
pressed we should set back to undefined.

* Fix: middle mouse button ID is 1, not 3

* Add "Trackpad Friendly" camera system setting

Signed off by Frank Noirot <frank@kittycad.io>

* Allow camera configs to be lenient on first click
2023-09-11 16:21:23 -04:00
9e2a94fcd9 Bump to v0.5.0 (#430) 2023-09-11 05:16:53 -04:00
8a3e8d331d Change WebRTC metrics to be request/response from the Engine (#410)
* Add in a Metrics request/response handler

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

* Update @kittycad/lib to 0.0.37

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

* Fix up type issues

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

* yarn fmt

* Remove VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS

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

---------

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-11 09:04:46 +10:00
1be9b2612c Add menu to code editor, put "Format code" and "Convert to variable" buttons in it (#426)
* Move format code button to menu item
by extending CollapsiblePanel to take an optional
menu React element.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Style tweaks

* Add shortcuts for format and cmd bar to codemirror

* Move convert to variable into code menu

Signed off by Frank Noirot <frank@kittycad.io>

* Add keyboard shortcut to convert to variable

* Remove convert to variable from toolbar

* Refactor: move TextEditor to its own component

* Set a better convertToVar shortcut

* Style and ergonomic polish for convertToVar modal

* Use named constants for shortcuts 😇

* Try yet another keyboard shortcut

* Fix formatting

* remove isShiftDown from app.tsx

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-09-09 05:38:36 +00:00
7c9aaeafa2 Guard Promise resolution with a shouldTrace() (#424)
The Promises are created behind a shouldTrace, so they'll be undefined
if you shouldn't be tracing. As a result, we need to guard the resoluton
of the promises.

Thanks @mlfarrell for spotting this in local dev!

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-09-08 16:40:08 -04:00
46c0078885 try window.location.origin (#423)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-08 13:02:02 -07:00
87ebf3b1d6 bump kittycad lib (#421)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-08 11:20:59 -07:00
45238f8196 Bump KCL lib to 0.1.25 (#420) 2023-09-08 12:24:25 -05:00
44f3a12fbe Remove .vscode dir (#419) 2023-09-08 11:47:34 -05:00
61acada2a0 Bump kittycad from 0.2.23 to 0.2.25 in /src/wasm-lib (#418)
* Bump kittycad from 0.2.23 to 0.2.25 in /src/wasm-lib

Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.23 to 0.2.25.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.23...v0.2.25)

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

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

* Handle metricsrequest

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.s.chalmers@gmail.com>
2023-09-08 11:45:50 -05:00
c68fbbd89d Make camera mouse controls configurable (#411)
* Add camera handler config object
Using definitions of camera controls of various
CAD incumbents from Onshape's onboarding.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Refactor: alphabetize settingsMachine

* Refactor: add descriptions to MouseGuards

* Refactor: don't destructure mousemove event

* Refactor: button down in stream as int, not bool

* Honor current camera control settings

* Add cameraControls to settings

* Refactor: alphabetize settings imports

* Refactor: break out cameraControls to own file

* Fix camera control setting in command bar

* Fix formatting on generated type file

* dont use "as" in App.tsx guards

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Don't use "as" in Stream.tsx

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Don't use "as" in settingsMachine.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Add type to cadPrograms

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Kurt review

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-08 10:13:35 -04:00
97a0b6a543 fix persist (#416) 2023-09-08 17:52:50 +10:00
3bccae492d clear old engine ids (#415)
* clear old engine ids

* animate re-execute and deffer execution for user typing
2023-09-08 17:50:37 +10:00
0120a89d9c Make empty defaultProjectName value impossible (#409)
* Set named const as default project name

* Refactor: move base units into settings machine

Signed off by Frank Noirot <frank@kittycad.io>

* Reset default when creating with blank name

Signed off by Frank Noirot <frank@kittycad.io>

* Make it impossible to set empty defaultProjectName

Signed off by Frank Noirot <frank@kittycad.io>

* Make it impossible to assign empty strings
to defaultProjectName

Signed off by Frank Noirot <frank@kittycad.io>
2023-09-07 21:48:51 -04:00
3da6fc3b7e Bump to v0.4.0 (#413) 2023-09-07 20:04:04 -04:00
34dd15ead7 Add macOS universal release builds (#408)
* Add macOS universal release builds
Fixes #397

* Change macos to universal-apple-darwin

* Upload universal-apple-darwin/release

* Clean up

* Clean up
2023-09-07 19:19:58 -04:00
b3d441e9d6 start of fuzzing (#405)
* fuzzing

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

* more tests

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

* unicode

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

* more fixes

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

* fix clippy

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>

* one more

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

* one more

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

* last one

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-06 21:56:10 -07:00
4b3dc3756c remove noisy log (#407) 2023-09-07 04:11:46 +00:00
10027b98b5 implement rename (#396)
* updates

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

* rename function

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

* start of rename

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>

* updates

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

* cache rust

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

* fix gnarly bug

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>

* fucking tabs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-06 19:34:47 -07:00
da17dad63b remove rust tests in ci, already covered in build (#403) 2023-09-07 01:49:11 +00:00
fba6c422a8 Fix LSP tooltip cutoff, style hover/autocomplete tooltips, add text wrapping setting (#404)
* Fix: allow tooltips to overflow code pane
while keeping the same vertical and horizontal
scroll behavior that we've had.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Style tooltips in light and dark mode

* Fix: properly display autocomplete info as HTML
We were parsing it from md to html, but displaying
the parsed html as a string in the info box.

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Fix z-index of command bar to show over code panel

* Let user set text wrapping in editor

* Style hover tooltips

* Fix failing tests
by not including line wrapping plugin in test mode

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-06 21:27:30 -04:00
0b4b93932d tweak prettierignore (#401) 2023-09-07 00:31:36 +00:00
f42900ec46 break up ci (#400) 2023-09-07 10:15:38 +10:00
eeca624ba6 bump kitty lib (#398) 2023-09-06 22:45:43 +00:00
84d08bad16 Allow people to set format options (#389)
* better naming

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

* fixes

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

* up[dates

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

* bump version

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

* fix tests

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

* updates

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

* whitespace

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-09-06 10:36:03 -07:00
1181f33e9d Bump to v0.3.2 (#392) 2023-09-06 09:04:06 -04:00
797e200d08 Make sure extra artifacts don't get uploaded on release (#390)
* Make sure extra artifacts don't get uploaded on release
Fixes #388

* Clean up
2023-09-06 07:52:58 -04:00
d2f231066b Franknoirot/debug rerendering (#387)
* Refactor: let Stream handle control drag status

* Fix: prevent app rerender on mouse move
By not setting the highlight range unless things
actually need to change. Setting the highlight range
still causes an app rerender, though.

Signed-off-by: Frank Noirot <frank@kittycad.io>

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-06 15:32:53 +10:00
94 changed files with 8966 additions and 2113 deletions

View File

@ -1,7 +1,6 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=

View File

@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -40,6 +40,17 @@ jobs:
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: Run clippy
run: |
cd "${{ matrix.dir }}"

View File

@ -41,6 +41,16 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Install ffmpeg
run: |
sudo apt update
sudo apt install \
ffmpeg \
libavformat-dev \
libavutil-dev \
libclang-dev \
libswscale-dev \
--no-install-recommends
- name: cargo test
shell: bash
run: |-

View File

@ -1,4 +1,4 @@
name: CI
name: CI
on:
pull_request:
@ -13,17 +13,31 @@ jobs:
check-format:
runs-on: 'ubuntu-20.04'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- run: yarn build:wasm
- run: yarn tsc
build-test-web:
runs-on: ubuntu-20.04
@ -36,12 +50,15 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn build:wasm
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- run: yarn tsc
- run: yarn build:wasm
- run: yarn simpleserver:ci
@ -49,14 +66,12 @@ jobs:
- run: yarn test:cov
- run: yarn test:rust
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-apps:
needs: [check-format, build-test-web]
needs: [check-format, build-test-web, check-types]
runs-on: ${{ matrix.os }}
strategy:
matrix:
@ -87,6 +102,10 @@ jobs:
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./src/wasm-lib"
- name: wasm prep
shell: bash
run: |
@ -110,19 +129,26 @@ jobs:
- name: Fix format
run: yarn fmt
- name: install apple silicon target mac
if: matrix.os == 'macos-latest'
run: |
rustup target add aarch64-apple-darwin
- name: Build the app for the current platform (no upload)
uses: tauri-apps/tauri-action@v0
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- uses: actions/upload-artifact@v3
with:
path: src-tauri/target/release/bundle/*/*
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
publish-apps-release:
runs-on: ubuntu-20.04
sign-windows-msi:
runs-on: windows-latest
if: github.event_name == 'release'
needs: [build-test-web, build-apps]
env:
@ -131,29 +157,89 @@ jobs:
- uses: actions/download-artifact@v3
- name: Setup Certificate
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
shell: bash
- name: Set variables
id: variables
run: |
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup SSM KSP on windows latest
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Signing using Signtool
run: |
signtool.exe sign /sha1 ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "artifact\msi\*.msi"
signtool.exe verify /v /pa "artifact\msi\*.msi"
# TODO: for the updater, investigate if we need to also replace what's in the .zip, and what to do about the .sig file
- uses: actions/upload-artifact@v3
with:
path: artifact/*
publish-apps-release:
runs-on: ubuntu-20.04
if: github.event_name == 'release'
needs: [build-test-web, build-apps, sign-windows-msi]
env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
PUB_DATE: ${{ github.event.release.created_at }}
NOTES: ${{ github.event.release.body }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact
ls -l artifact/*
ls -l artifact/*/*itty*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
@ -166,6 +252,34 @@ jobs:
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "v${VERSION_NO_V}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v1.1.1'
with:
@ -175,22 +289,28 @@ jobs:
uses: google-github-actions/setup-gcloud@v1.1.1
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: artifact
glob: '*/*'
glob: '*/*itty*'
parent: false
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_update.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_download.json
destination: dl.kittycad.io/releases/modeling-app
- name: Upload release files to Github
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*
files: artifact/*/*itty*

View File

@ -5,3 +5,5 @@ coverage
# Ignore Rust projects:
*.rs
target
src/wasm-lib/pkg
src/wasm-lib/kcl/bindings

121
README.md
View File

@ -1,48 +1,72 @@
## Kurt demo project
![KittyCAD Modeling App](/public/kcma-logomark.png)
## KittyCAD Modeling App
live at [app.kittycad.io](https://app.kittycad.io/)
Not sure what to call this, it's both a language/interpreter and a UI that uses the language as the source of truth model the user build with direct-manipulation with the UI.
A CAD application from the future, brought to you by the [KittyCAD team](https://kittycad.io).
It might make sense to split this repo up at some point, but not the lang and the UI are all togther in a react app
The KittyCAD modeling app is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
Originally Presented on 10/01/2023
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
- This makes version control—which is a solved problem in software engineering—trivial for CAD
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in KittyCAD Modeling App
- Everything graphics _has_ to be built for the GPU
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
- Make the resource-intensive pieces of an application auto-scaling
- One of the bottlenecks of today's hardware design tools is that they all rely on the local machine's resources to do the hardest parts, which include geometry rendering and analysis. Our geometry engine parallelizes rendering and just sends video frames back to the app (seriously, inspect source, it's just a `<video>` element), and our API will offload analysis as we build it in
[Video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
[demo-slides.pdf](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
KittyCAD Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
## To run, there are a couple steps since we're compiling rust to WASM, you'll need to have rust stuff installed, then
The 3D view in KittyCAD Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools
- UI
- [React](https://react.dev/)
- [Headless UI](https://headlessui.com/)
- [TailwindCSS](https://tailwindcss.com/)
- Networking
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
- Code Editor
- [CodeMirror](https://codemirror.net/)
- Custom WASM LSP Server
- Modeling
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
[Original demo slides](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
## Get started
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
## Running a development build
First, [install Rust via `rustup`](https://www.rust-lang.org/tools/install). This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. Then, run:
```
yarn install
```
then
followed by:
```
yarn build:wasm
```
That will build the WASM binary and put in the `public` dir (though gitignored)
finally
finally, to run the web app only, run:
```
yarn start
```
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
```
yarn simpleserver
```
in one terminal
and
```
yarn test
```
in another.
If you want to edit the rust files, you can cd into `src/wasm-lib` and then use the usual rust commands, `cargo build`, `cargo test`, when you want to bring the changes back to the web-app, a fresh `yarn build:wasm` in the root will be needed.
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
## Developing in Chrome
Chrome is in the process of rolling out a new default which
@ -52,12 +76,26 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Running tests
First, start the dev server following "Running a development build" above.
Then in another terminal tab, run:
```
yarn test
```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
```
yarn tauri dev
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writting they can conflict.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
@ -67,11 +105,22 @@ To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access t
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `yarn fmt`
- `yarn tsc`
- `yarn test`
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
## Release a new version
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
@ -79,6 +128,7 @@ Note that these became separate apps on Macos, so make sure you open the right o
```bash
VERSION=x.y.z yarn run bump-jsons
```
The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR
@ -86,3 +136,24 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd src/wasm-lib/kcl
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).

View File

@ -9322,6 +9322,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "cos",
"summary": "Computes the sine of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "extrude",
"summary": "Extrudes by a given amount.",
@ -11173,22 +11201,13 @@
},
"to": {
"description": "The to point.",
"anyOf": [
{
"description": "A point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
}
}
},
@ -11201,10 +11220,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
},
@ -13031,6 +13046,24 @@
"unpublished": false,
"deprecated": false
},
{
"name": "pi",
"summary": "Return the value of `pi`.",
"description": "",
"tags": [],
"args": [],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "segAng",
"summary": "Returns the angle of the segment.",
@ -15315,6 +15348,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "sin",
"summary": "Computes the sine of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "startSketchAt",
"summary": "Start a sketch at a given point.",
@ -15341,22 +15402,13 @@
},
"to": {
"description": "The to point.",
"anyOf": [
{
"description": "A point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
}
}
},
@ -15369,10 +15421,6 @@
},
"maxItems": 2,
"minItems": 2
},
{
"description": "A string like `default`.",
"type": "string"
}
]
},
@ -15815,6 +15863,34 @@
"unpublished": false,
"deprecated": false
},
{
"name": "tan",
"summary": "Computes the tangent of a number (in radians).",
"description": "",
"tags": [],
"args": [
{
"name": "num",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "xLine",
"summary": "Draw a line on the x-axis.",

View File

@ -16,6 +16,7 @@
* [`arc`](#arc)
* [`bezierCurve`](#bezierCurve)
* [`close`](#close)
* [`cos`](#cos)
* [`extrude`](#extrude)
* [`getExtrudeWallTransform`](#getExtrudeWallTransform)
* [`lastSegX`](#lastSegX)
@ -26,12 +27,15 @@
* [`line`](#line)
* [`lineTo`](#lineTo)
* [`min`](#min)
* [`pi`](#pi)
* [`segAng`](#segAng)
* [`segEndX`](#segEndX)
* [`segEndY`](#segEndY)
* [`segLen`](#segLen)
* [`show`](#show)
* [`sin`](#sin)
* [`startSketchAt`](#startSketchAt)
* [`tan`](#tan)
* [`xLine`](#xLine)
* [`xLineTo`](#xLineTo)
* [`yLine`](#yLine)
@ -1637,6 +1641,26 @@ close(sketch_group: SketchGroup) -> SketchGroup
### cos
Computes the sine of a number (in radians).
```
cos(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### extrude
Extrudes by a given amount.
@ -2044,11 +2068,9 @@ line(data: LineData, sketch_group: SketchGroup) -> SketchGroup
// The tag.
tag: string,
// The to point.
to: [number] |
string,
to: [number],
} |
[number] |
string
[number]
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
@ -2356,6 +2378,25 @@ min(args: [number]) -> number
### pi
Return the value of `pi`.
```
pi() -> number
```
#### Arguments
#### Returns
* `number`
### segAng
Returns the angle of the segment.
@ -2766,6 +2807,26 @@ show(sketch: SketchGroup)
### sin
Computes the sine of a number (in radians).
```
sin(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### startSketchAt
Start a sketch at a given point.
@ -2784,11 +2845,9 @@ startSketchAt(data: LineData) -> SketchGroup
// The tag.
tag: string,
// The to point.
to: [number] |
string,
to: [number],
} |
[number] |
string
[number]
```
#### Returns
@ -2859,6 +2918,26 @@ string
### tan
Computes the tangent of a number (in radians).
```
tan(num: number) -> number
```
#### Arguments
* `num`: `number`
#### Returns
* `number`
### xLine
Draw a line on the x-axis.

75
docs/kcl/types.md Normal file
View File

@ -0,0 +1,75 @@
# Types
`KCL` defines the following types and keywords the language.
All these types can be nested in various forms where nesting applies. Like
arrays can hold objects and vice versa.
## Boolean
`true` or `false` work when defining values.
## Variable declaration
Variables are defined with the `let` keyword like so:
```
let myBool = false
```
## Array
An array is defined with `[]` braces. What is inside the brackets can
be of any type. For example, the following is completely valid:
```
let myArray = ["thing", 2, false]
```
If you want to get a value from an array you can use the index like so:
`myArray[0]`.
## Object
An object is defined with `{}` braces. Here is an example object:
```
let myObj = {a: 0, b: "thing"}
```
We support two different ways of getting properties from objects, you can call
`myObj.a` or `myObj["a"]` both work.
## Functions
We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax:
```
fn myFn = (x) => {
return x
}
```
As you can see above `myFn` just returns whatever it is given.
## Binary expressions
You can also do math! Let's show an example below:
```
let myMathExpression = 3 + 1 * 2 / 3 - 7
```
You can nest expressions in parenthesis as well:
```
let myMathExpression = 3 + (1 * 2 / (3 - 7))
```
Please if you find any issues using any of the above expressions or syntax
please file an issue with the `ast` label on the [modeling-app
repo](https://github.com/KittyCAD/modeling-app/issues/new).

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.3.1",
"version": "0.7.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.9.0",
@ -10,10 +10,11 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.35",
"@kittycad/lib": "^0.0.37",
"@lezer/javascript": "^1.4.7",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
@ -70,7 +71,7 @@
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"

BIN
public/kcma-logomark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

26
public/roadmap.md Normal file
View File

@ -0,0 +1,26 @@
## KittyCAD Modeling App Roadmap
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.
### Current Priority List
1. [Sketch on Face](https://github.com/KittyCAD/modeling-app/discussions/477)
2. [Revolve](https://github.com/KittyCAD/modeling-app/discussions/496)
3. [Fillet](https://github.com/KittyCAD/modeling-app/discussions/501)
4. [Linear Pattern](https://github.com/KittyCAD/modeling-app/discussions/256)
5. [Circular Pattern](https://github.com/KittyCAD/modeling-app/discussions/257)
6. [Mirror-Sketch](https://github.com/KittyCAD/modeling-app/discussions/507)
7. [Chamfer](https://github.com/KittyCAD/modeling-app/discussions/502)
8. [Sweep](https://github.com/KittyCAD/modeling-app/discussions/498)
9. [Draft](https://github.com/KittyCAD/modeling-app/discussions/495)
10. [Shell](https://github.com/KittyCAD/modeling-app/discussions/503)
11. [Union](https://github.com/KittyCAD/modeling-app/discussions/509)
12. [Mirror-Model](https://github.com/KittyCAD/modeling-app/discussions/508)
13. [Subtract](https://github.com/KittyCAD/modeling-app/discussions/510)
14. [Intersect](https://github.com/KittyCAD/modeling-app/discussions/511)
15. [Offset](https://github.com/KittyCAD/modeling-app/discussions/512)
16. [Thicken](https://github.com/KittyCAD/modeling-app/discussions/499)
17. [Import](https://github.com/KittyCAD/modeling-app/discussions/478)
18. [Assemblies](https://github.com/KittyCAD/modeling-app/discussions/494)
19. [External Thread](https://github.com/KittyCAD/modeling-app/discussions/505)

247
src-tauri/Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.71"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "app"
@ -83,7 +83,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-fs-extra",
"tokio",
"toml 0.6.0",
"toml 0.8.0",
]
[[package]]
@ -110,22 +110,6 @@ dependencies = [
"system-deps 6.1.0",
]
[[package]]
name = "attohttpc"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
dependencies = [
"flate2",
"http",
"log",
"native-tls",
"serde",
"serde_json",
"serde_urlencoded",
"url",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -234,6 +218,9 @@ name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
dependencies = [
"serde",
]
[[package]]
name = "cairo-rs"
@ -266,7 +253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838"
dependencies = [
"serde",
"toml 0.7.3",
"toml 0.7.8",
]
[[package]]
@ -325,8 +312,10 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"winapi",
]
@ -495,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -535,7 +524,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -546,7 +535,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -628,7 +617,7 @@ checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a"
dependencies = [
"cc",
"rustc_version",
"toml 0.7.3",
"toml 0.7.8",
"vswhom",
"winreg 0.11.0",
]
@ -805,7 +794,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -829,6 +818,7 @@ dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
@ -1282,7 +1272,7 @@ dependencies = [
"httpdate",
"itoa 1.0.6",
"pin-project-lite",
"socket2",
"socket2 0.4.9",
"tokio",
"tower-service",
"tracing",
@ -1303,6 +1293,19 @@ dependencies = [
"tokio-rustls",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.56"
@ -1745,15 +1748,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1836,9 +1830,9 @@ dependencies = [
[[package]]
name = "oauth2"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a6e2a2b13a56ebeabba9142f911745be6456163fd6c3d361274ebcd891a80c"
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
dependencies = [
"base64 0.13.1",
"chrono",
@ -1941,7 +1935,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -2128,9 +2122,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@ -2190,7 +2184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.8",
"toml_edit 0.19.15",
]
[[package]]
@ -2225,9 +2219,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.59"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
@ -2417,10 +2411,12 @@ dependencies = [
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -2430,11 +2426,14 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
"winreg 0.10.1",
@ -2531,9 +2530,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.101.1"
version = "0.101.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
dependencies = [
"ring",
"untrusted",
@ -2651,29 +2650,29 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.163"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.163"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
name = "serde_json"
version = "1.0.96"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
dependencies = [
"itoa 1.0.6",
"ryu",
@ -2698,14 +2697,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
name = "serde_spanned"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@ -2748,7 +2747,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -2840,6 +2839,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "socket2"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "soup2"
version = "0.2.1"
@ -2934,9 +2943,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.18"
version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [
"proc-macro2",
"quote",
@ -2965,7 +2974,7 @@ dependencies = [
"cfg-expr 0.15.2",
"heck 0.4.1",
"pkg-config",
"toml 0.7.3",
"toml 0.7.8",
"version-compare 0.1.1",
]
@ -3046,13 +3055,13 @@ checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
[[package]]
name = "tauri"
version = "1.3.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
dependencies = [
"anyhow",
"attohttpc",
"base64 0.21.2",
"bytes",
"cocoa",
"dirs-next",
"embed_plist",
@ -3073,6 +3082,7 @@ dependencies = [
"rand 0.8.5",
"raw-window-handle",
"regex",
"reqwest",
"rfd",
"semver",
"serde",
@ -3116,9 +3126,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde"
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
dependencies = [
"base64 0.21.2",
"brotli",
@ -3142,9 +3152,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46"
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@ -3157,7 +3167,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs-extra"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7e58dc8502f654b99d51c087421f84ccc0e03119"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
dependencies = [
"log",
"serde",
@ -3168,9 +3178,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422"
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
dependencies = [
"gtk",
"http",
@ -3189,9 +3199,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1"
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
dependencies = [
"cocoa",
"gtk",
@ -3243,7 +3253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
dependencies = [
"embed-resource",
"toml 0.7.3",
"toml 0.7.8",
]
[[package]]
@ -3293,7 +3303,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -3335,21 +3345,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.29.1"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
"autocfg",
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"pin-project-lite",
"socket2",
"socket2 0.5.4",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@ -3385,69 +3404,60 @@ dependencies = [
[[package]]
name = "toml"
version = "0.6.0"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime 0.5.1",
"toml_edit 0.18.1",
"toml_datetime",
"toml_edit 0.19.15",
]
[[package]]
name = "toml"
version = "0.7.3"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime 0.6.2",
"toml_edit 0.19.8",
"toml_datetime",
"toml_edit 0.20.0",
]
[[package]]
name = "toml_datetime"
version = "0.5.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.18.1"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 1.9.3",
"nom8",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime 0.5.1",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.19.8"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95"
dependencies = [
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime 0.6.2",
"toml_datetime",
"winnow",
]
@ -3477,7 +3487,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
]
[[package]]
@ -3702,7 +3712,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
"wasm-bindgen-shared",
]
@ -3736,7 +3746,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.33",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3747,6 +3757,19 @@ version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
[[package]]
name = "wasm-streams"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.63"
@ -3806,9 +3829,9 @@ dependencies = [
[[package]]
name = "webpki"
version = "0.22.0"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
dependencies = [
"ring",
"untrusted",
@ -4169,9 +4192,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.1"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
dependencies = [
"memchr",
]

View File

@ -16,12 +16,12 @@ tauri-build = { version = "1.4.0", features = [] }
[dependencies]
anyhow = "1"
oauth2 = "4.4.1"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater"] }
tokio = { version = "1.32.0", features = ["time"] }
toml = "0.8.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features]

View File

@ -8,7 +8,7 @@
},
"package": {
"productName": "kittycad-modeling",
"version": "0.3.1"
"version": "0.7.0"
},
"tauri": {
"allowlist": {

View File

@ -1,39 +1,17 @@
import {
useRef,
useEffect,
useLayoutEffect,
useMemo,
useCallback,
MouseEventHandler,
} from 'react'
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { asyncParser } from './lang/abstractSyntaxTree'
import { _executor } from './lang/executor'
import CodeMirror from '@uiw/react-codemirror'
import { linter, lintGutter } from '@codemirror/lint'
import { ViewUpdate, EditorView } from '@codemirror/view'
import {
lineHighlightField,
addLineHighlight,
} from './editor/highlightextension'
import { PaneType, Selections, useStore } from './useStore'
import Server from './editor/lsp/server'
import Client from './editor/lsp/client'
import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise'
import { FromServer, IntoServer } from './editor/lsp/codec'
import {
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { isOverlap, throttle } from './lib/utils'
import { EngineCommand } from './lang/std/engineConnection'
import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader'
import { KCLError, kclErrToDiagnostic } from './lang/errors'
import { Resizable } from 're-resizable'
import {
faCode,
@ -41,95 +19,42 @@ import {
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils'
import { Themes, getSystemTheme } from './lib/theme'
import { isTauri } from './lib/isTauri'
import { useLoaderData, useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { useLoaderData } from 'react-router-dom'
import { IndexLoaderData } from './Router'
import { toast } from 'react-hot-toast'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useCodeEval } from 'hooks/useCodeEval'
export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
const pathParams = useParams()
const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener()
const {
editorView,
setEditorView,
setSelectionRanges,
selectionRanges,
addLog,
addKCLError,
code,
setCode,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
selectionRangeTypeMap,
setArtifactMap,
engineCommandManager,
setEngineCommandManager,
setHighlightRange,
setCursor2,
sourceRangeMap,
setMediaStream,
setIsStreamReady,
isStreamReady,
isLSPServerReady,
setIsLSPServerReady,
isMouseDownInStream,
formatCode,
buttonDownInStream,
openPanes,
setOpenPanes,
didDragInStream,
setDidDragInStream,
setStreamDimensions,
streamDimensions,
guiMode,
} = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
setGuiMode: s.setGuiMode,
addLog: s.addLog,
code: s.code,
guiMode: s.guiMode,
setCode: s.setCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
setEngineCommandManager: s.setEngineCommandManager,
setHighlightRange: s.setHighlightRange,
isShiftDown: s.isShiftDown,
setCursor: s.setCursor,
setCursor2: s.setCursor2,
sourceRangeMap: s.sourceRangeMap,
setMediaStream: s.setMediaStream,
isStreamReady: s.isStreamReady,
setIsStreamReady: s.setIsStreamReady,
isLSPServerReady: s.isLSPServerReady,
setIsLSPServerReady: s.setIsLSPServerReady,
isMouseDownInStream: s.isMouseDownInStream,
formatCode: s.formatCode,
addKCLError: s.addKCLError,
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
}))
@ -138,7 +63,7 @@ export function App() {
context: { token },
},
settings: {
context: { showDebugPanel, theme, onboardingStatus },
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
},
} = useGlobalStateContext()
@ -179,228 +104,74 @@ export function App() {
}
}, [loadedCode, setCode])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
setCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
}
)
}
if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
}
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
useEffect(() => {
if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!code) {
setAst(null)
return
}
const _ast = await asyncParser(code)
setAst(_ast)
resetLogs()
resetKCLErrors()
engineCommandManager.endSession()
engineCommandManager.startNewSession()
const programMemory = await _executor(
_ast,
{
root: {
_0: {
type: 'userVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
value: 270,
__meta: [],
},
},
},
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
setArtifactMap({ artifactMap, sourceRangeMap })
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (!data?.entity_id) {
setHighlightRange([0, 0])
} else {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
unsubFn.push(unSubHover, unSubClick)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setError()
} catch (e: any) {
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
return () => {
unsubFn.forEach((fn) => fn())
}
}, [code, isStreamReady, engineCommandManager])
useSetupEngineManager(streamRef, token)
useCodeEval()
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)
}, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
clientX,
clientY,
ctrlKey,
shiftKey,
currentTarget,
nativeEvent,
}) => {
nativeEvent.preventDefault()
if (isMouseDownInStream) {
setDidDragInStream(true)
}
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault()
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: currentTarget,
clientX: e.clientX,
clientY: e.clientY,
el: e.currentTarget,
...streamDimensions,
})
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
const newCmdId = uuidv4()
if (buttonDownInStream === undefined) {
if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any)
) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'mouse_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else {
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type
const eWithButton = { ...e, button: buttonDownInStream }
if (interactionGuards.pan.callback(eWithButton)) {
interaction = 'pan'
} else if (interactionGuards.rotate.callback(eWithButton)) {
interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
interaction = 'zoom'
} else {
console.log('none')
return
}
if (isMouseDownInStream) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
@ -410,68 +181,9 @@ export function App() {
},
cmd_id: newCmdId,
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
}
const extraExtensions = useMemo(() => {
if (TEST) return []
return [
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
EditorView.lineWrapping,
]
}, [])
// So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts.
// Below is the client and server promise.
const { lspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
}
const lspClient = new LanguageServerClient({ client })
return { lspClient }
}, [setIsLSPServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup becuase it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => {
let plugin = null
if (isLSPServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = kclLanguage({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
client: lspClient,
})
plugin = lsp
}
return plugin
}, [lspClient, isLSPServerReady])
return (
<div
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
@ -482,7 +194,7 @@ export function App() {
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
(buttonDownInStream ? ' pointer-events-none' : '')
}
project={project}
enableMenu={true}
@ -491,17 +203,17 @@ export function App() {
<Resizable
className={
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
(isMouseDownInStream || onboardingStatus === 'camera'
(buttonDownInStream || onboardingStatus === 'camera'
? ' pointer-events-none '
: ' ') +
paneOpacity
}
defaultSize={{
width: '400px',
width: '550px',
height: 'auto',
}}
minWidth={200}
maxWidth={600}
maxWidth={800}
minHeight={'auto'}
maxHeight={'auto'}
handleClasses={{
@ -513,36 +225,11 @@ export function App() {
<CollapsiblePanel
title="Code"
icon={faCode}
className="open:!mb-2 overflow-x-hidden"
className="open:!mb-2"
open={openPanes.includes('code')}
menu={<CodeMenu />}
>
<div className="px-2 py-1">
<button
// disabled={!shouldFormat}
onClick={formatCode}
// className={`${!shouldFormat && 'text-gray-300'}`}
>
format
</button>
</div>
<div
id="code-mirror-override"
className="overflow-x-hidden h-full"
>
<CodeMirror
className="h-full overflow-hidden-x"
value={code}
extensions={
kclLSP
? [kclLSP, lineHighlightField, ...extraExtensions]
: [lineHighlightField, ...extraExtensions]
}
onChange={onChange}
onUpdate={onUpdate}
theme={editorTheme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
<TextEditor theme={editorTheme} />
</CollapsiblePanel>
<section className="flex flex-col">
<MemoryPanel
@ -573,7 +260,7 @@ export function App() {
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
(buttonDownInStream ? ' pointer-events-none' : '')
}
open={openPanes.includes('debug')}
/>

View File

@ -1,4 +1,4 @@
import { useStore, toolTips } from './useStore'
import { useStore, toolTips, Selections } from './useStore'
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/queryAst'
import { HorzVert } from './components/Toolbar/HorzVert'
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
import { Intersect } from './components/Toolbar/Intersect'
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
import { SetAngleLength } from './components/Toolbar/setAngleLength'
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
import { Fragment, useEffect } from 'react'
@ -16,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
import { Popover, Transition } from '@headlessui/react'
import styles from './Toolbar.module.css'
import { v4 as uuidv4 } from 'uuid'
import { useAppMode } from 'hooks/useAppMode'
export const Toolbar = () => {
const {
@ -25,6 +26,7 @@ export const Toolbar = () => {
ast,
updateAst,
programMemory,
engineCommandManager,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
@ -32,7 +34,9 @@ export const Toolbar = () => {
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
engineCommandManager: s.engineCommandManager,
}))
useAppMode()
useEffect(() => {
console.log('guiMode', guiMode)
@ -40,7 +44,7 @@ export const Toolbar = () => {
function ToolbarButtons() {
return (
<>
<span className="overflow-x-auto">
{guiMode.mode === 'default' && (
<button
onClick={() => {
@ -72,9 +76,18 @@ export const Toolbar = () => {
SketchOnFace
</button>
)}
{(guiMode.mode === 'canEditSketch' || false) && (
{guiMode.mode === 'canEditSketch' && (
<button
onClick={() => {
console.log('guiMode.pathId', guiMode.pathId)
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: guiMode.pathId,
},
})
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
@ -126,14 +139,23 @@ export const Toolbar = () => {
)}
{guiMode.mode === 'sketch' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>
<button
onClick={() => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
setGuiMode({ mode: 'default' })
}}
>
Exit sketch
</button>
)}
{toolTips
.filter(
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
(sketchFnName) => ['line'].includes(sketchFnName)
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
)
.map((sketchFnName) => {
if (
@ -144,7 +166,18 @@ export const Toolbar = () => {
return (
<button
key={sketchFnName}
onClick={() =>
onClick={() => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_tool',
tool:
guiMode.sketchMode === sketchFnName
? 'select'
: (sketchFnName as any),
},
})
setGuiMode({
...guiMode,
...(guiMode.sketchMode === sketchFnName
@ -154,17 +187,17 @@ export const Toolbar = () => {
}
: {
sketchMode: sketchFnName,
waitingFirstClick: true,
isTooltip: true,
}),
})
}
}}
>
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
</button>
)
})}
<ConvertToVariable />
<HorzVert horOrVert="horizontal" />
<HorzVert horOrVert="vertical" />
<EqualLength />
@ -182,7 +215,7 @@ export const Toolbar = () => {
<Intersect />
<RemoveConstrainingValues />
<SetAngleBetween />
</>
</span>
)
}

View File

@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css'
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
@ -43,7 +44,8 @@ export const AppHeader = ({
)}
{/* If there are children, show them, otherwise show User menu */}
{children || (
<div className="ml-auto">
<div className="ml-auto flex items-center gap-1">
<NetworkHealthIndicator />
<UserSidebarMenu user={user} />
</div>
)}

View File

@ -0,0 +1,182 @@
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() {
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
ast: s.ast,
setHighlightRange: s.setHighlightRange,
selectionRanges: s.selectionRanges,
}))
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections?.[0]?.range
)
const node = getNodeFromPath(ast, pathToNode).node
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
return (
<div className="relative" style={{ width: '300px' }}>
<div className="">
filter out keys:<div className="w-2 inline-block"></div>
{['start', 'end', 'type'].map((key) => {
return (
<label key={key} className="inline-flex items-center">
<input
type="checkbox"
className="form-checkbox"
checked={filterKeys.includes(key)}
onChange={(e) => {
if (filterKeys.includes(key)) {
setFilterKeys(filterKeys.filter((k) => k !== key))
} else {
setFilterKeys([...filterKeys, key])
}
}}
/>
<span className="mr-2">{key}</span>
</label>
)
})}
</div>
<div
className="h-full relative"
onMouseLeave={(e) => {
setHighlightRange([0, 0])
}}
>
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
</pre>
</div>
</div>
)
}
function DisplayBody({
body,
filterKeys,
node,
}: {
body: { start: number; end: number; [key: string]: any }[]
filterKeys: string[]
node: any
}) {
return (
<>
{body.map((b, index) => {
return (
<div className="my-2" key={index}>
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
</div>
)
})}
</>
)
}
function DisplayObj({
obj,
filterKeys,
node,
}: {
obj: { start: number; end: number; [key: string]: any }
filterKeys: string[]
node: any
}) {
const { setHighlightRange, setCursor2 } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
setCursor2: s.setCursor2,
}))
const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(false)
useEffect(() => {
if (
node?.start === obj?.start &&
node?.end === obj?.end &&
node.type === obj?.type
) {
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
setHasCursor(true)
} else {
setHasCursor(false)
}
}, [node.start, node.end, node.type])
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
setHighlightRange([obj?.start || 0, obj.end])
}}
onClick={(e) => {
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
e.stopPropagation()
}}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (Array.isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DisplayBody
body={value}
filterKeys={filterKeys}
node={node}
/>
{']'}
</li>
)
} else if (
typeof value === 'object' &&
value !== null &&
value?.end
) {
return (
<li key={key}>
{key}:
<DisplayObj
obj={value}
filterKeys={filterKeys}
node={node}
/>
</li>
)
} else if (
typeof value === 'string' ||
typeof value === 'number'
) {
return (
<li key={key}>
{key}: {value}
</li>
)
}
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -144,7 +144,7 @@ export function useCalc({
try {
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parser_wasm(code)
const _programMem: any = { root: {} }
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
isNewVariableNameUnique,
setNewVariableName,
shouldCreateVariable,
setShouldCreateVariable,
setShouldCreateVariable = () => {},
showCheckbox = true,
}: {
isNewVariableNameUnique: boolean
newVariableName: string
setNewVariableName: (a: string) => void
shouldCreateVariable: boolean
setShouldCreateVariable: (a: boolean) => void
shouldCreateVariable?: boolean
setShouldCreateVariable?: (a: boolean) => void
showCheckbox?: boolean
}) => {
return (
<>
<label
htmlFor="create-new-variable"
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
>
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
Create new variable
</label>
<div className="mt-1 flex flex-1">
<div className="mt-1 flex gap-2 items-center">
{showCheckbox && (
<input
type="checkbox"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
checked={shouldCreateVariable}
onChange={(e) => {
setShouldCreateVariable(e.target.checked)
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
disabled={!shouldCreateVariable}
name="create-new-variable"
id="create-new-variable"
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
autoFocus={true}
autoCapitalize="off"
autoCorrect="off"
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
!shouldCreateVariable ? 'opacity-50' : ''
}`}
value={newVariableName}

View File

@ -0,0 +1,19 @@
.button {
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
@apply transition-colors ease-out;
}
:global(.dark) .button {
@apply text-chalkboard-30;
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
}
.button small {
@apply text-chalkboard-60;
}
:global(.dark) .button small {
@apply text-chalkboard-40;
}

View File

@ -0,0 +1,59 @@
import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react'
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import { useStore } from 'useStore'
import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor'
export const CodeMenu = ({ children }: PropsWithChildren) => {
const { formatCode } = useStore((s) => ({
formatCode: s.formatCode,
}))
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
return (
<Menu>
<div
className="relative"
onClick={(e) => {
if (e.eventPhase === 3) {
e.stopPropagation()
e.preventDefault()
}
}}
>
<Menu.Button className="p-0 border-none relative">
<ActionIcon
icon={faEllipsis}
bgClassName={
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
}
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
/>
</Menu.Button>
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item>
<button onClick={() => formatCode()} className={styles.button}>
<span>Format code</span>
<small>{editorShortcutMeta.formatCode.display}</small>
</button>
</Menu.Item>
{convertToVarEnabled && (
<Menu.Item>
<button
onClick={handleConvertToVarClick}
className={styles.button}
>
<span>Convert to Variable</span>
<small>{editorShortcutMeta.convertToVariable.display}</small>
</button>
</Menu.Item>
)}
</Menu.Items>
</div>
</Menu>
)
}

View File

@ -1,5 +1,5 @@
.panel {
@apply relative overflow-auto z-0;
@apply relative z-0;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
@ -9,7 +9,7 @@
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center gap-2 w-full p-2;
@apply flex items-center justify-between gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-20;
}

View File

@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
title: string
icon?: IconDefinition
open?: boolean
menu?: React.ReactNode
iconClassNames?: {
bg?: string
icon?: string
@ -18,21 +19,27 @@ export const PanelHeader = ({
title,
icon,
iconClassNames,
menu,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
<div className="flex gap-2 align-center flex-1">
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
</div>
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
{menu}
</div>
</summary>
)
}
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
children,
className,
iconClassNames,
menu,
...props
}: CollapsiblePanelProps) => {
return (
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
{...props}
className={styles.panel + ' group ' + (className || '')}
>
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
<PanelHeader
title={title}
icon={icon}
iconClassNames={iconClassNames}
menu={menu}
/>
{children}
</details>
)

View File

@ -196,7 +196,7 @@ const CommandBar = () => {
setCommandBarOpen(false)
clearState()
}}
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
>
<Transition.Child
enter="duration-100 ease-out"
@ -207,7 +207,7 @@ const CommandBar = () => {
leaveTo="opacity-0"
as={Fragment}
>
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<Transition.Child
enter="duration-100 ease-out"
@ -221,7 +221,7 @@ const CommandBar = () => {
<Combobox
value={selectedCommand}
onChange={handleCommandSelection}
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
className="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
as="div"
>
<div className="flex gap-2 items-center">

View File

@ -6,6 +6,7 @@ import { useState } from 'react'
import { ActionButton } from '../components/ActionButton'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { isReducedMotion } from 'lang/util'
import { AstExplorer } from './AstExplorer'
type SketchModeCmd = Extract<
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
@ -94,6 +95,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
>
Send sketch mode command
</ActionButton>
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</div>
</section>
</CollapsiblePanel>
)

View File

@ -10,7 +10,7 @@ describe('processMemory', () => {
// Enable rotations #152
const code = `
const myVar = 5
const myFn = (a) => {
fn myFn = (a) => {
return a - 2
}
const otherVar = myFn(5)
@ -29,6 +29,7 @@ describe('processMemory', () => {
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast, {
root: {},
return: null,
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)

View File

@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps {
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
Object.keys(programMemory.root).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {

View File

@ -0,0 +1,51 @@
import { fireEvent, render, screen } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
import {
NETWORK_CONTENT,
NetworkHealthIndicator,
} from './NetworkHealthIndicator'
function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter>
)
}
describe('NetworkHealthIndicator tests', () => {
test('Renders the network indicator', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-good')).toHaveTextContent(
NETWORK_CONTENT.good
)
})
test('Responds to network changes', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-bad')).toHaveTextContent(
NETWORK_CONTENT.bad
)
})
})

View File

@ -0,0 +1,112 @@
import {
faCheck,
faExclamation,
faWifi,
} from '@fortawesome/free-solid-svg-icons'
import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { ActionIcon } from './ActionIcon'
export const NETWORK_CONTENT = {
good: 'Network health is good',
bad: 'Network issue',
}
const NETWORK_MESSAGES = {
offline: 'You are offline',
}
export const NetworkHealthIndicator = () => {
const [networkIssues, setNetworkIssues] = useState<string[]>([])
const hasIssues = [...networkIssues.values()].length > 0
useEffect(() => {
const offlineListener = () =>
setNetworkIssues((issues) => {
return [
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
NETWORK_MESSAGES.offline,
]
})
window.addEventListener('offline', offlineListener)
const onlineListener = () =>
setNetworkIssues((issues) => {
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
})
window.addEventListener('online', onlineListener)
return () => {
window.removeEventListener('offline', offlineListener)
window.removeEventListener('online', onlineListener)
}
}, [])
return (
<Popover className="relative">
<Popover.Button
className={
'p-0 border-none relative ' +
(hasIssues
? 'focus-visible:outline-destroy-80'
: 'focus-visible:outline-succeed-80')
}
data-testid="network-toggle"
>
<span className="sr-only">Network Health</span>
<ActionIcon
icon={faWifi}
iconClassName={
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
}
bgClassName={
hasIssues
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
}
/>
</Popover.Button>
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
{!hasIssues ? (
<span
className="flex items-center justify-center gap-1 px-4"
data-testid="network-good"
>
<ActionIcon
icon={faCheck}
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
iconClassName={'text-succeed-80 dark:text-succeed-30'}
/>
{NETWORK_CONTENT.good}
</span>
) : (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
<span
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
data-testid="network-bad"
>
{NETWORK_CONTENT.bad}
{networkIssues.length > 1 ? 's' : ''}
</span>
{networkIssues.map((issue) => (
<li
key={issue}
className="flex items-center gap-1 py-2 my-2 last:mb-0"
>
<ActionIcon
icon={faExclamation}
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
iconClassName={'text-destroy-80 dark:text-destroy-30'}
className="ml-4"
/>
<p className="flex-1 mr-4">{issue}</p>
</li>
))}
</ul>
)}
</Popover.Panel>
</Popover>
)
}

View File

@ -1,6 +1,9 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
import { ActionButton } from './ActionButton'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast'
export const SetVarNameModal = ({
isOpen,
@ -19,67 +22,65 @@ export const SetVarNameModal = ({
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onReject}>
<Dialog
as="div"
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
onClose={onReject}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-75"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
<form
onSubmit={(e) => {
e.preventDefault()
onResolve({
variableName: newVariableName,
})
toast.success(`Added variable ${newVariableName}`)
}}
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 capitalize"
<CreateNewVariable
setNewVariableName={setNewVariableName}
newVariableName={newVariableName}
isNewVariableNameUnique={isNewVariableNameUnique}
shouldCreateVariable={true}
showCheckbox={false}
/>
<div className="mt-8 flex justify-between">
<ActionButton
Element="button"
type="submit"
disabled={!isNewVariableNameUnique}
icon={{ icon: faPlus }}
>
Set {valueName}
</Dialog.Title>
<CreateNewVariable
setNewVariableName={setNewVariableName}
newVariableName={newVariableName}
isNewVariableNameUnique={isNewVariableNameUnique}
shouldCreateVariable={true}
setShouldCreateVariable={() => {}}
/>
<div className="mt-4">
<button
type="button"
disabled={!isNewVariableNameUnique}
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
!isNewVariableNameUnique
? 'opacity-50 cursor-not-allowed'
: ''
}`}
onClick={() =>
onResolve({
variableName: newVariableName,
})
}
>
Add variable
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
Add variable
</ActionButton>
<ActionButton Element="button" onClick={() => onReject(false)}>
Cancel
</ActionButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
)

View File

@ -7,29 +7,52 @@ import {
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { getNormalisedCoordinates } from '../lib/utils'
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib'
import { addStartSketch } from 'lang/modifyAst'
import { addNewSketchLn } from 'lang/std/sketch'
export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null)
const {
mediaStream,
engineCommandManager,
setIsMouseDownInStream,
setButtonDownInStream,
didDragInStream,
setDidDragInStream,
streamDimensions,
isExecuting,
guiMode,
ast,
updateAst,
setGuiMode,
programMemory,
} = useStore((s) => ({
mediaStream: s.mediaStream,
engineCommandManager: s.engineCommandManager,
isMouseDownInStream: s.isMouseDownInStream,
setIsMouseDownInStream: s.setIsMouseDownInStream,
setButtonDownInStream: s.setButtonDownInStream,
fileId: s.fileId,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions,
isExecuting: s.isExecuting,
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
setGuiMode: s.setGuiMode,
programMemory: s.programMemory,
}))
const {
settings: {
context: { cameraControls },
},
} = useGlobalStateContext()
useEffect(() => {
if (
@ -42,39 +65,70 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream
}, [mediaStream, engineCommandManager])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
clientX,
clientY,
ctrlKey,
}) => {
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!videoRef.current) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
clientX: e.clientX,
clientY: e.clientY,
el: videoRef.current,
...streamDimensions,
})
console.log('click', x, y)
const newId = uuidv4()
const interaction = ctrlKey ? 'pan' : 'rotate'
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type = 'rotate'
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
if (
interactionGuards.pan.callback(e) ||
interactionGuards.pan.lenientDragStartButton === e.button
) {
interaction = 'pan'
} else if (
interactionGuards.rotate.callback(e) ||
interactionGuards.rotate.lenientDragStartButton === e.button
) {
interaction = 'rotate'
} else if (
interactionGuards.zoom.dragCallback(e) ||
interactionGuards.zoom.lenientDragStartButton === e.button
) {
interaction = 'zoom'
}
setIsMouseDownInStream(true)
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'handle_mouse_drag_start',
window: { x, y },
},
cmd_id: newId,
})
} else if (
!(
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any)
)
) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
}
setButtonDownInStream(e.button)
setClickCoords({ x, y })
}
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
e.preventDefault()
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
@ -91,6 +145,7 @@ export const Stream = ({ className = '' }) => {
ctrlKey,
}) => {
if (!videoRef.current) return
setButtonDownInStream(undefined)
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
@ -101,7 +156,7 @@ export const Stream = ({ className = '' }) => {
const newCmdId = uuidv4()
const interaction = ctrlKey ? 'pan' : 'rotate'
engineCommandManager?.sendSceneCommand({
const command: Models['WebSocketRequest_type'] = {
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
@ -109,9 +164,8 @@ export const Stream = ({ className = '' }) => {
window: { x, y },
},
cmd_id: newCmdId,
})
}
setIsMouseDownInStream(false)
if (!didDragInStream) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
@ -123,7 +177,109 @@ export const Stream = ({ className = '' }) => {
cmd_id: uuidv4(),
})
}
if (!didDragInStream && guiMode.mode === 'default') {
command.cmd = {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
}
} else if (
(!didDragInStream &&
guiMode.mode === 'sketch' &&
['move', 'select'].includes(guiMode.sketchMode)) ||
(guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any))
) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
} else if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('move' as any)
) {
command.cmd = {
type: 'handle_mouse_drag_end',
window: { x, y },
}
}
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => {
if (command.cmd.type !== 'mouse_click' || !ast) return
if (
!(
guiMode.mode === 'sketch' &&
guiMode.sketchMode === ('sketch_line' as any as 'line')
)
)
return
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
const _addStartSketch = addStartSketch(
ast,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
setGuiMode({
...guiMode,
pathToNode: _pathToNode,
waitingFirstClick: false,
})
updateAst(_modifiedAst)
} else if (
data?.data?.entities_modified?.length &&
!guiMode.waitingFirstClick
) {
const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
const _modifiedAst = addNewSketchLn({
node: ast,
programMemory,
to: [coords[1].x, coords[1].y],
fnName: 'line',
pathToNode: guiMode.pathToNode,
}).modifiedAst
updateAst(_modifiedAst)
}
})
setDidDragInStream(false)
setClickCoords(undefined)
}
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!clickCoords) return
const delta =
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
0.5
if (delta > 5 && !didDragInStream) {
setDidDragInStream(true)
}
}
return (
@ -139,7 +295,9 @@ export const Stream = ({ className = '' }) => {
onContextMenuCapture={(e) => e.preventDefault()}
onWheel={handleScroll}
onPlay={() => setIsLoading(false)}
className="w-full h-full"
onMouseMoveCapture={handleMouseMove}
className={`w-full h-full ${isExecuting && 'blur-md'}`}
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/>
{isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">

View File

@ -0,0 +1,298 @@
import ReactCodeMirror, {
Extension,
ViewUpdate,
keymap,
} from '@uiw/react-codemirror'
import { FromServer, IntoServer } from 'editor/lsp/codec'
import Server from '../editor/lsp/server'
import Client from '../editor/lsp/client'
import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme'
import { useMemo } from 'react'
import { linter, lintGutter } from '@codemirror/lint'
import { Selections, useStore } from 'useStore'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { isTauri } from 'lib/isTauri'
import { useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
import { toast } from 'react-hot-toast'
import {
EditorView,
addLineHighlight,
lineHighlightField,
} from 'editor/highlightextension'
import { isOverlap, roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config'
import interact from '@replit/codemirror-interact'
export const editorShortcutMeta = {
formatCode: {
codeMirror: 'Alt-Shift-f',
display: 'Alt + Shift + F',
},
convertToVariable: {
codeMirror: 'Ctrl-Shift-c',
display: 'Ctrl + Shift + C',
},
}
export const TextEditor = ({
theme,
}: {
theme: Themes.Light | Themes.Dark
}) => {
const pathParams = useParams()
const {
code,
defferedSetCode,
editorView,
engineCommandManager,
formatCode,
isLSPServerReady,
selectionRanges,
selectionRangeTypeMap,
setEditorView,
setIsLSPServerReady,
setSelectionRanges,
sourceRangeMap,
} = useStore((s) => ({
code: s.code,
defferedSetCode: s.defferedSetCode,
editorView: s.editorView,
engineCommandManager: s.engineCommandManager,
formatCode: s.formatCode,
isLSPServerReady: s.isLSPServerReady,
selectionRanges: s.selectionRanges,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady,
setSelectionRanges: s.setSelectionRanges,
sourceRangeMap: s.sourceRangeMap,
}))
const {
settings: {
context: { textWrapping },
},
} = useGlobalStateContext()
const { setCommandBarOpen } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
// So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts.
// Below is the client and server promise.
const { lspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
}
const lspClient = new LanguageServerClient({ client })
return { lspClient }
}, [setIsLSPServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup becuase it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => {
let plugin = null
if (isLSPServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = kclLanguage({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null,
client: lspClient,
})
plugin = lsp
}
return plugin
}, [lspClient, isLSPServerReady])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
defferedSetCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
}
)
}
if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
}
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const editorExtensions = useMemo(() => {
const extensions = [
lineHighlightField,
keymap.of([
{
key: 'Meta-k',
run: () => {
setCommandBarOpen(true)
return false
},
},
{
key: editorShortcutMeta.formatCode.codeMirror,
run: () => {
formatCode()
return true
},
},
{
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
if (convertEnabled) {
convertCallback()
return true
}
return false
},
},
]),
] as Extension[]
if (kclLSP) extensions.push(kclLSP)
// These extensions have proven to mess with vitest
if (!TEST) {
extensions.push(
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
interact({
rules: [
// a rule for a number dragger
{
// the regexp matching the value
regexp: /-?\b\d+\.?\d*\b/g,
// set cursor to "ew-resize" on hover
cursor: 'ew-resize',
// change number value based on mouse X movement on drag
onDrag: (text, setText, e) => {
const multiplier =
e.shiftKey && e.metaKey
? 0.01
: e.metaKey
? 0.1
: e.shiftKey
? 10
: 1
const delta = e.movementX * multiplier
const newVal = roundOff(
Number(text) + delta,
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
)
if (isNaN(newVal)) return
setText(newVal.toString())
},
},
],
})
)
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
}
return extensions
}, [kclLSP, textWrapping])
return (
<div
id="code-mirror-override"
className="full-height-subtract"
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
>
<ReactCodeMirror
className="h-full"
value={code}
extensions={editorExtensions}
onChange={onChange}
onUpdate={onUpdate}
theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
)
}

View File

@ -1,61 +0,0 @@
import { useState, useEffect } from 'react'
import { create } from 'react-modal-promise'
import { useStore } from '../../useStore'
import { isNodeSafeToReplace } from '../../lang/queryAst'
import { SetVarNameModal } from '../SetVarNameModal'
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
const getModalInfo = create(SetVarNameModal as any)
export const ConvertToVariable = () => {
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
(s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
selectionRanges: s.selectionRanges,
programMemory: s.programMemory,
})
)
const [enableAngLen, setEnableAngLen] = useState(false)
useEffect(() => {
if (!ast) return
const { isSafe, value } = isNodeSafeToReplace(
ast,
selectionRanges.codeBasedSelections?.[0]?.range || []
)
const canReplace = isSafe && value.type !== 'Identifier'
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
const _enableHorz = canReplace && isOnlyOneSelection
setEnableAngLen(_enableHorz)
}, [guiMode, selectionRanges])
return (
<button
onClick={async () => {
if (!ast) return
try {
const { variableName } = await getModalInfo({
valueName: 'var',
} as any)
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
ast,
programMemory,
selectionRanges.codeBasedSelections[0].range,
variableName
)
updateAst(_modifiedAst)
} catch (e) {
console.log('e', e)
}
}}
disabled={!enableAngLen}
>
ConvertToVariable
</button>
)
}

View File

@ -208,7 +208,13 @@ export class LanguageServerPlugin implements PluginValue {
filterText: filterText ?? label,
}
if (documentation) {
completion.info = formatContents(documentation)
completion.info = () => {
const htmlString = formatContents(documentation)
const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString
return { dom: htmlNode }
}
}
return completion

View File

@ -8,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN

243
src/hooks/useAppMode.ts Normal file
View File

@ -0,0 +1,243 @@
// needed somewhere to dump this logic,
// Once we have xState this should be removed
import { useStore, Selections } from 'useStore'
import { useEffect, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
import { Models } from '@kittycad/lib/dist/types/src'
import { isReducedMotion } from 'lang/util'
import { isOverlap } from 'lib/utils'
interface DefaultPlanes {
xy: string
yz: string
xz: string
}
export function useAppMode() {
const {
guiMode,
setGuiMode,
selectionRanges,
engineCommandManager,
selectionRangeTypeMap,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRanges: s.selectionRanges,
engineCommandManager: s.engineCommandManager,
selectionRangeTypeMap: s.selectionRangeTypeMap,
}))
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
useEffect(() => {
if (
guiMode.mode === 'sketch' &&
guiMode.sketchMode === 'selectFace' &&
engineCommandManager
) {
if (!defaultPlanes) {
const xy = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
})
const yz = createPlane(engineCommandManager, {
x_axis: { x: 0, y: 1, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
})
const xz = createPlane(engineCommandManager, {
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
})
setDefaultPlanes({ xy, yz, xz })
} else {
hideDefaultPlanes(engineCommandManager, defaultPlanes)
}
}
if (guiMode.mode !== 'sketch' && defaultPlanes) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
} else if (guiMode.mode === 'default') {
const pathId =
engineCommandManager &&
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
if (pathId) {
setGuiMode({
mode: 'canEditSketch',
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [],
pathId,
})
}
} else if (guiMode.mode === 'canEditSketch') {
if (
!engineCommandManager ||
!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
) {
setGuiMode({
mode: 'default',
})
}
}
}, [
guiMode,
guiMode.mode,
engineCommandManager,
selectionRanges,
selectionRangeTypeMap,
])
useEffect(() => {
const unSub = engineCommandManager?.subscribeTo({
event: 'select_with_point',
callback: async ({ data }) => {
if (!data.entity_id) return
if (!defaultPlanes) return
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
// user clicked something else in the scene
return
}
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
{
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_enable',
plane_id: data.entity_id,
ortho: true,
animated: !isReducedMotion(),
},
}
)
hideDefaultPlanes(engineCommandManager, defaultPlanes)
const sketchUuid = uuidv4()
const proms: any[] = []
proms.push(
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
)
proms.push(
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
)
const res = await Promise.all(proms)
console.log('res', res)
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: [],
})
console.log('sketchModeResponse', sketchModeResponse)
},
})
return unSub
}, [engineCommandManager, defaultPlanes])
}
function createPlane(
engineCommandManager: EngineCommandManager,
{
x_axis,
y_axis,
color,
}: {
x_axis: Models['Point3d_type']
y_axis: Models['Point3d_type']
color: Models['Color_type']
}
) {
const planeId = uuidv4()
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'make_plane',
size: 60,
origin: { x: 0, y: 0, z: 0 },
x_axis,
y_axis,
clobber: false,
},
cmd_id: planeId,
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'plane_set_color',
plane_id: planeId,
color,
},
cmd_id: uuidv4(),
})
return planeId
}
function hideDefaultPlanes(
engineCommandManager: EngineCommandManager,
defaultPlanes: DefaultPlanes
) {
Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
}
function isCursorInSketchCommandRange(
artifactMap: ArtifactMap,
selectionRanges: Selections
): string | false {
const overlapingEntries = Object.entries(artifactMap || {}).filter(
([id, artifact]) =>
selectionRanges.codeBasedSelections.some(
(selection) =>
Array.isArray(selection.range) &&
Array.isArray(artifact.range) &&
isOverlap(selection.range, artifact.range) &&
(artifact.commandType === 'start_path' ||
artifact.commandType === 'extend_path' ||
'close_path')
)
)
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
? overlapingEntries[0][1].parentId
: false
}

156
src/hooks/useCodeEval.ts Normal file
View File

@ -0,0 +1,156 @@
import { useEffect } from 'react'
import { asyncParser } from '../lang/abstractSyntaxTree'
import { _executor } from '../lang/executor'
import { useStore } from '../useStore'
import { KCLError } from '../lang/errors'
// This recently moved out of app.tsx
// and is our old way of thinking that whenever the code changes we need to re-execute, instead of
// being more decisive about when and where we execute, its likey this custom hook will be
// refactored away entirely at some point
export function useCodeEval() {
const {
addLog,
addKCLError,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
setArtifactMap,
engineCommandManager,
highlightRange,
setHighlightRange,
setCursor2,
isStreamReady,
setIsExecuting,
defferedCode,
} = useStore((s) => ({
addLog: s.addLog,
defferedCode: s.defferedCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
highlightRange: s.highlightRange,
setHighlightRange: s.setHighlightRange,
setCursor2: s.setCursor2,
isStreamReady: s.isStreamReady,
addKCLError: s.addKCLError,
setIsExecuting: s.setIsExecuting,
}))
useEffect(() => {
if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!defferedCode) {
setAst({
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
})
setProgramMemory({ root: {}, return: null })
engineCommandManager.endSession()
engineCommandManager.startNewSession()
return
}
const _ast = await asyncParser(defferedCode)
setAst(_ast)
resetLogs()
resetKCLErrors()
engineCommandManager.endSession()
engineCommandManager.startNewSession()
setIsExecuting(true)
const programMemory = await _executor(
_ast,
{
root: {
_0: {
type: 'UserVal',
value: 0,
__meta: [],
},
_90: {
type: 'UserVal',
value: 90,
__meta: [],
},
_180: {
type: 'UserVal',
value: 180,
__meta: [],
},
_270: {
type: 'UserVal',
value: 270,
__meta: [],
},
},
return: null,
},
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands(_ast, programMemory)
setIsExecuting(false)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setArtifactMap({ artifactMap, sourceRangeMap })
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (data?.entity_id) {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
unsubFn.push(unSubHover, unSubClick)
setError()
} catch (e: any) {
setIsExecuting(false)
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
return () => {
unsubFn.forEach((fn) => fn())
}
}, [defferedCode, isStreamReady, engineCommandManager])
}

View File

@ -0,0 +1,48 @@
import { useLayoutEffect } from 'react'
import { _executor } from '../lang/executor'
import { useStore } from '../useStore'
import { EngineCommandManager } from '../lang/std/engineConnection'
export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
token?: string
) {
const {
setEngineCommandManager,
setMediaStream,
setIsStreamReady,
setStreamDimensions,
} = useStore((s) => ({
setEngineCommandManager: s.setEngineCommandManager,
setMediaStream: s.setMediaStream,
setIsStreamReady: s.setIsStreamReady,
setStreamDimensions: s.setStreamDimensions,
}))
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
}

View File

@ -0,0 +1,56 @@
import { SetVarNameModal } from 'components/SetVarNameModal'
import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react'
import { create } from 'react-modal-promise'
import { useStore } from 'useStore'
const getModalInfo = create(SetVarNameModal as any)
export function useConvertToVariable() {
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
(s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
selectionRanges: s.selectionRanges,
programMemory: s.programMemory,
})
)
const [enable, setEnabled] = useState(false)
useEffect(() => {
if (!ast) return
const { isSafe, value } = isNodeSafeToReplace(
ast,
selectionRanges.codeBasedSelections?.[0]?.range || []
)
const canReplace = isSafe && value.type !== 'Identifier'
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
const _enableHorz = canReplace && isOnlyOneSelection
setEnabled(_enableHorz)
}, [guiMode, selectionRanges])
const handleClick = async () => {
if (!ast) return
try {
const { variableName } = await getModalInfo({
valueName: 'var',
} as any)
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
ast,
programMemory,
selectionRanges.codeBasedSelections[0].range,
variableName
)
updateAst(_modifiedAst)
} catch (e) {
console.log('e', e)
}
}
return { enable, handleClick }
}

View File

@ -82,11 +82,22 @@ code {
monospace;
}
.full-height-subtract {
--height-subtract: 2.25rem;
height: 100%;
max-height: calc(100% - var(--height-subtract));
}
#code-mirror-override .cm-editor {
@apply bg-transparent;
@apply h-full bg-transparent;
}
#code-mirror-override .cm-scroller {
@apply h-full;
}
#code-mirror-override .cm-scroller::-webkit-scrollbar {
@apply h-0;
}
#code-mirror-override .cm-activeLine,
@ -137,14 +148,39 @@ code {
}
#code-mirror-override .cm-tooltip {
font-size: 80%;
@apply text-xs shadow-md;
@apply bg-chalkboard-10 text-chalkboard-80;
@apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10;
}
.dark #code-mirror-override .cm-tooltip {
@apply bg-chalkboard-110 text-chalkboard-40;
@apply border-chalkboard-70/20 border-l-liquid-70;
}
#code-mirror-override .cm-tooltip-hover {
@apply py-1 px-2 w-max max-w-md;
}
#code-mirror-override .cm-tooltip-hover .documentation {
padding: 5;
#code-mirror-override .cm-completionInfo {
@apply px-4 rounded-l-none;
@apply bg-chalkboard-10 text-liquid-90;
@apply border-liquid-40/30;
}
.dark #code-mirror-override .cm-completionInfo {
@apply bg-liquid-120 text-liquid-50;
@apply border-liquid-90/60;
}
#code-mirror-override .cm-tooltip-autocomplete li {
@apply px-2 py-1;
}
#code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
@apply bg-liquid-10 text-liquid-110;
}
.dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
@apply bg-liquid-100 text-liquid-20;
}
#code-mirror-override .cm-content {

View File

@ -1,5 +1,5 @@
import { parser_wasm } from './abstractSyntaxTree'
import { KCLUnexpectedError } from './errors'
import { KCLError } from './errors'
import { initPromise } from './rust'
beforeAll(() => initPromise)
@ -1564,7 +1564,7 @@ const key = 'c'`
start: code.indexOf('\n// this is a comment'),
end: code.indexOf('const key'),
value: {
type: 'block',
type: 'blockComment',
value: 'this is a comment',
},
}
@ -1602,7 +1602,7 @@ const key = 'c'`
start: 106,
end: 166,
value: {
type: 'block',
type: 'blockComment',
value: 'this is\n a comment\n spanning a few lines',
},
})
@ -1625,7 +1625,7 @@ const key = 'c'`
start: 125,
end: 141,
value: {
type: 'block',
type: 'blockComment',
value: 'a comment',
},
})
@ -1744,6 +1744,12 @@ describe('parsing errors', () => {
_theError = e
}
const theError = _theError as any
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
expect(theError).toEqual(
new KCLError(
'unexpected',
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
[[29, 30]]
)
)
})
})

View File

@ -21,7 +21,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -77,7 +77,7 @@ show(mySketch001)`
)
expect(shown).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
)
expect(geos).toEqual([
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
__meta: [{ sourceRange: [13, 34] }],
},
{
type: 'extrudeGroup',
type: 'ExtrudeGroup',
id: expect.any(String),
value: [],
height: 2,

View File

@ -1,7 +1,7 @@
import fs from 'node:fs'
import { parser_wasm } from './abstractSyntaxTree'
import { ProgramMemory } from './executor'
import { ProgramMemory, SketchGroup } from './executor'
import { initPromise } from './rust'
import { enginelessExecutor } from '../lib/testHelpers'
import { vi } from 'vitest'
@ -117,10 +117,10 @@ show(mySketch)
// ].join('\n')
// const { root } = await exe(code)
// expect(root.mySk1.value).toHaveLength(3)
// expect(root?.rotated?.type).toBe('sketchGroup')
// expect(root?.rotated?.type).toBe('SketchGroup')
// if (
// root?.mySk1?.type !== 'sketchGroup' ||
// root?.rotated?.type !== 'sketchGroup'
// root?.mySk1?.type !== 'SketchGroup' ||
// root?.rotated?.type !== 'SketchGroup'
// )
// throw new Error('not a sketch group')
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
@ -143,7 +143,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.mySk1).toEqual({
type: 'sketchGroup',
type: 'SketchGroup',
start: {
to: [0, 0],
from: [0, 0],
@ -199,7 +199,7 @@ show(mySketch)
// TODO path to node is probably wrong here, zero indexes are not correct
expect(root).toEqual({
three: {
type: 'userVal',
type: 'UserVal',
value: 3,
__meta: [
{
@ -208,7 +208,7 @@ show(mySketch)
],
},
yo: {
type: 'userVal',
type: 'UserVal',
value: [1, '2', 3, 9],
__meta: [
{
@ -225,7 +225,7 @@ show(mySketch)
].join('\n')
const { root } = await exe(code)
expect(root.yo).toEqual({
type: 'userVal',
type: 'UserVal',
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
__meta: [
{
@ -240,7 +240,7 @@ show(mySketch)
)
const { root } = await exe(code)
expect(root.myVar).toEqual({
type: 'userVal',
type: 'UserVal',
value: '123',
__meta: [
{
@ -338,7 +338,7 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = sketch.value?.[0]?.to?.[1]
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -356,8 +356,8 @@ describe('testing math operators', () => {
const { root } = await exe(code)
const sketch = root.part001
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
expect(sketch.value?.[1]?.from).toEqual([3, 4])
expect(sketch.value?.[1]?.to).toEqual([6, 0])
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen('seg01', %), myVar)`,
`legLen(segLen('seg01', %), myVar)`
@ -366,7 +366,9 @@ describe('testing math operators', () => {
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
// without the minus sign, the y value should be 8
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
6, 8,
])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
@ -397,7 +399,10 @@ show(theExtrude)`
// helpers
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
async function exe(
code: string,
programMemory: ProgramMemory = { root: {}, return: null }
) {
const ast = parser_wasm(code)
const result = await enginelessExecutor(ast, programMemory)

View File

@ -5,96 +5,21 @@ import {
SourceRangeMap,
} from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number]
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
export type Metadata = {
sourceRange: SourceRange
}
export type Position = [number, number, number]
export type Rotation = [number, number, number, number]
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
export type { Position } from '../wasm-lib/kcl/bindings/Position'
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
interface BasePath {
from: [number, number]
to: [number, number]
name?: string
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export interface ToPoint extends BasePath {
type: 'toPoint'
}
export interface Base extends BasePath {
type: 'base'
}
export interface HorizontalLineTo extends BasePath {
type: 'horizontalLineTo'
x: number
}
export interface AngledLineTo extends BasePath {
type: 'angledLineTo'
angle: number
x?: number
y?: number
}
interface GeoMeta {
__geoMeta: {
id: string
sourceRange: SourceRange
}
}
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
export interface SketchGroup {
type: 'sketchGroup'
id: string
value: Path[]
start?: Base
position: Position
rotation: Rotation
__meta: Metadata[]
}
interface ExtrudePlane {
type: 'extrudePlane'
position: Position
rotation: Rotation
name?: string
}
export type ExtrudeSurface = GeoMeta &
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
export interface ExtrudeGroup {
type: 'extrudeGroup'
id: string
value: ExtrudeSurface[]
height: number
position: Position
rotation: Rotation
__meta: Metadata[]
}
/** UserVal not produced by one of our internal functions */
export interface UserVal {
type: 'userVal'
value: any
__meta: Metadata[]
}
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
export type PathToNode = [string | number, string][]
interface Memory {
[key: string]: MemoryItem
@ -102,12 +27,12 @@ interface Memory {
export interface ProgramMemory {
root: Memory
return?: ProgramReturn
return: ProgramReturn | null
}
export const executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager,
// work around while the gemotry is still be stored on the frontend
// will be removed when the stream UI is added.
@ -123,7 +48,7 @@ export const executor = async (
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
await engineCommandManager.waitForAllCommands(node, _programMemory)
tempMapCallback({ artifactMap, sourceRangeMap })
engineCommandManager.endSession()
@ -132,7 +57,7 @@ export const executor = async (
export const _executor = async (
node: Program,
programMemory: ProgramMemory = { root: {} },
programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager
): Promise<ProgramMemory> => {
try {

View File

@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
expect(str).toBe(`const part001 = startSketchAt('default')
|> ry(90, %)
|> line('default', %)
show(part001)`)
show(part001)
`)
})
})
@ -175,11 +176,14 @@ show(part001)`
})
describe('Testing moveValueIntoNewVariable', () => {
const fn = (fnName: string) => `const ${fnName} = (x) => {
const fn = (fnName: string) => `fn ${fnName} = (x) => {
return x
}
`
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
fn ghi = (x) => {
return 2
}
const abc = 3
const identifierGuy = 5
const yo = 5 + 6

View File

@ -27,6 +27,48 @@ import {
getFirstArg,
createFirstArg,
} from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos'
export function addStartSketch(
node: Program,
start: [number, number],
end: [number, number]
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
const _node = { ...node }
const _name = findUniqueName(node, 'part')
const startSketchAt = createCallExpression('startSketchAt', [
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
])
const initialLineTo = createCallExpression('line', [
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
createPipeSubstitution(),
])
const pipeBody = [startSketchAt, initialLineTo]
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
const newIndex = node.body.length
_node.body = [...node.body, variableDeclaration]
let pathToNode: PathToNode = [
['body', ''],
[newIndex.toString(10), 'index'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'],
]
return {
modifiedAst: _node,
id: _name,
pathToNode,
}
}
export function addSketchTo(
node: Program,
@ -151,7 +193,7 @@ export function mutateArrExp(
): boolean {
if (node.type === 'ArrayExpression') {
node.elements.forEach((element, i) => {
if (element.type === 'Literal') {
if (isLiteralArrayOrStatic(element)) {
node.elements[i] = updateWith.elements[i]
}
})
@ -169,8 +211,8 @@ export function mutateObjExpProp(
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
if (keyIndex !== -1) {
if (
updateWith.type === 'Literal' &&
node.properties[keyIndex].value.type === 'Literal'
isLiteralArrayOrStatic(updateWith) &&
isLiteralArrayOrStatic(node.properties[keyIndex].value)
) {
node.properties[keyIndex].value = updateWith
return true
@ -180,7 +222,7 @@ export function mutateObjExpProp(
) {
const arrExp = node.properties[keyIndex].value as ArrayExpression
arrExp.elements.forEach((element, i) => {
if (element.type === 'Literal') {
if (isLiteralArrayOrStatic(element)) {
arrExp.elements[i] = updateWith.elements[i]
}
})

View File

@ -11,26 +11,27 @@ describe('recast', () => {
const code = '1 + 2'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('variable declaration', () => {
const code = 'const myVar = 5'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it("variable declaration that's binary with string", () => {
const code = "const myVar = 5 + 'yo'"
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
const { ast: ast2 } = code2ast(codeWithOtherQuotes)
expect(recast(ast2)).toBe(codeWithOtherQuotes)
expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
})
it('test assigning two variables, the second summing with the first', () => {
const code = `const myVar = 5
const newVar = myVar + 1`
const newVar = myVar + 1
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -42,11 +43,12 @@ const newVar = myVar + 1`
)
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('test with function call', () => {
const code = `const myVar = "hello"
log(5, myVar)`
log(5, myVar)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -61,7 +63,7 @@ log(5, myVar)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('recast sketch declaration', () => {
let code = `const mySketch = startSketchAt([0, 0])
@ -70,10 +72,11 @@ log(5, myVar)`
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|> close(%)
show(mySketch)`
show(mySketch)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('sketch piped into callExpression', () => {
const code = [
@ -85,7 +88,7 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast BinaryExpression piped into CallExpression', () => {
const code = [
@ -97,37 +100,37 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('recast nested binary expression', () => {
const code = ['const myVar = 1 + 2 * 5'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast nested binary expression with parans', () => {
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('unnecessary paran wrap will be remove', () => {
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.replace('(', '').replace(')', ''))
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
})
it('complex nested binary expression', () => {
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('multiplied paren expressions', () => {
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast array declaration', () => {
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
@ -135,7 +138,7 @@ show(mySketch)`
)
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast long array declaration', () => {
const code = [
@ -150,7 +153,7 @@ show(mySketch)`
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted.trim()).toBe(code.trim())
})
it('recast long object exectution', () => {
const code = `const three = 3
@ -159,26 +162,29 @@ const yo = {
anum: 2,
identifier: three,
binExp: 4 + 5
}`
}
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('recast short object exectution', () => {
const code = `const yo = { key: 'val' }`
const code = `const yo = { key: 'val' }
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
it('recast object execution with member expression', () => {
const code = `const yo = { a: { b: { c: '123' } } }
const key = 'c'
const myVar = yo.a['b'][key]
const key2 = 'b'
const myVar2 = yo['a'][key2].c`
const myVar2 = yo['a'][key2].c
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code.trim())
expect(recasted).toBe(code)
})
})
@ -186,7 +192,8 @@ describe('testing recasting with comments and whitespace', () => {
it('code with comments', () => {
const code = `const yo = { a: { b: { c: '123' } } }
// this is a comment
const key = 'c'`
const key = 'c'
`
const { ast } = code2ast(code)
const recasted = recast(ast)
@ -199,7 +206,8 @@ const key = 'c'`
/* this is
a
comment */
const yo = 'bing'`
const yo = 'bing'
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -209,13 +217,14 @@ const yo = 'bing'`
const yo = { a: { b: { c: '123' } } }
const key = 'c'
// this is also a comment`
// this is also a comment
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
})
it('comments in a fn block', () => {
const code = `const myFn = () => {
const code = `fn myFn = () => {
// this is a comment
const yo = { a: { b: { c: '123' } } }
@ -223,7 +232,8 @@ const key = 'c'
comment */
const key = 'c'
// this is also a comment
}`
}
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -239,7 +249,7 @@ const key = 'c'
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('comments sprinkled in all over the place', () => {
const code = `
@ -261,7 +271,8 @@ const mySk1 = startSketchAt([0, 0])
|> rx(45, %)
/*
one more for good measure
*/`
*/
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(`// comment at start
@ -278,7 +289,8 @@ a comment between pipe expression statements */
// and another with just white space between others below
|> ry(45, %)
|> rx(45, %)
// one more for good measure`)
// one more for good measure
`)
})
})
@ -287,19 +299,19 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('nested callExpression in unaryExpression', () => {
const code = 'const myVar = -min(100, legLen(5, 3))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('with unaryExpression in callExpression', () => {
const code = 'const myVar = min(5, -legLen(5, 4))'
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
it('with unaryExpression in sketch situation', () => {
const code = [
@ -308,7 +320,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
].join('\n')
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
expect(recasted.trim()).toBe(code)
})
})
@ -318,12 +330,13 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|> line({ to: [0.62, 4.15], tag: 'seg01' }, %)
|> line([2.77, -1.24], %)
|> angledLineThatIntersects({
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
}, %)
|> line([-0.42, -1.72], %)
show(part001)`
show(part001)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -333,7 +346,8 @@ show(part001)`
angle: 201,
offset: -1.35,
intersectTag: 'seg01'
}, %)`
}, %)
`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
@ -342,7 +356,8 @@ show(part001)`
describe('it recasts binary expression using brackets where needed', () => {
it('when there are two minus in a row', () => {
const code = `const part001 = 1 - (def - abc)`
const code = `const part001 = 1 - (def - abc)
`
const recasted = recast(code2ast(code).ast)
expect(recasted).toBe(code)
})

View File

@ -1,20 +1,25 @@
import { SourceRange } from 'lang/executor'
import { ProgramMemory, SourceRange } from 'lang/executor'
import { Selections } from 'useStore'
import {
VITE_KC_API_WS_MODELING_URL,
VITE_KC_CONNECTION_TIMEOUT_MS,
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
} from 'env'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
interface ResultCommand {
let lastMessage = ''
interface CommandInfo {
commandType: CommandTypes
range: SourceRange
parentId?: string
}
interface ResultCommand extends CommandInfo {
type: 'result'
data: any
}
interface PendingCommand {
interface PendingCommand extends CommandInfo {
type: 'pending'
promise: Promise<any>
resolve: (val: any) => void
@ -34,6 +39,8 @@ interface NewTrackArgs {
type WebSocketResponse = Models['OkWebSocketResponseData_type']
type ClientMetrics = Models['ClientMetrics_type']
// EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket
// and WebRTC connections.
@ -53,6 +60,9 @@ export class EngineConnection {
private onClose: (engineConnection: EngineConnection) => void
private onNewTrack: (track: NewTrackArgs) => void
// TODO: actual type is ClientMetrics
private webrtcStatsCollector?: () => Promise<ClientMetrics>
constructor({
url,
token,
@ -188,15 +198,17 @@ export class EngineConnection {
)
}
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
if (this.shouldTrace()) {
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
}
this.onWebsocketOpen(this)
})
@ -297,7 +309,9 @@ export class EngineConnection {
this.pc.addEventListener('connectionstatechange', (event) => {
if (this.pc?.iceConnectionState === 'connected') {
iceSpan.resolve?.()
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
}
})
@ -330,6 +344,17 @@ export class EngineConnection {
})
})
.catch(console.log)
} else if (resp.type === 'metrics_request') {
if (this.webrtcStatsCollector === undefined) {
// TODO: Error message here?
return
}
this.webrtcStatsCollector().then((client_metrics) => {
this.send({
type: 'metrics_response',
metrics: client_metrics,
})
})
}
// TODO(paultag): This ought to be both controllable, as well as something
@ -361,127 +386,58 @@ export class EngineConnection {
})
}
// Set up the background thread to keep an eye on statistical
// information about the WebRTC media stream from the server to
// us. We'll also eventually want more global statistical information,
// but this will give us a baseline.
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
setInterval(() => {
if (this.pc === undefined) {
return
}
if (!this.shouldTrace()) {
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
return new Promise((resolve, reject) => {
if (mediaStream.getVideoTracks().length !== 1) {
reject(new Error('too many video tracks to report'))
return
}
// Use the WebRTC Statistics API to collect statistical information
// about the WebRTC connection we're using to report to Sentry.
mediaStream.getVideoTracks().forEach((videoTrack) => {
let trackStats = new Map<string, any>()
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// Sentry only allows 10 metrics per transaction. We're going
// to have to pick carefully here, eventually send like a prom
// file or something to the peer.
let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// TODO(paultag): this needs type information from the KittyCAD typescript
// library once it's updated
let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0,
rtc_frames_dropped: 0,
rtc_frames_received: 0,
rtc_frames_per_second: 0,
rtc_freeze_count: 0,
rtc_jitter_sec: 0.0,
rtc_keyframes_decoded: 0,
rtc_total_freezes_duration_sec: 0.0,
}
const transaction = Sentry.startTransaction({
name: 'webrtc-stats',
})
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
// RTC Stream Info
// transaction.setMeasurement(
// 'mediaStreamTrack.framesDecoded',
// videoTrackReport.framesDecoded,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesDropped',
videoTrackReport.framesDropped,
''
)
// transaction.setMeasurement(
// 'mediaStreamTrack.framesReceived',
// videoTrackReport.framesReceived,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesPerSecond',
videoTrackReport.framesPerSecond,
'fps'
)
transaction.setMeasurement(
'rtcFreezeCount',
videoTrackReport.freezeCount,
''
)
transaction.setMeasurement(
'rtcJitter',
videoTrackReport.jitter,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferDelay',
// videoTrackReport.jitterBufferDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferEmittedCount',
// videoTrackReport.jitterBufferEmittedCount,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferMinimumDelay',
// videoTrackReport.jitterBufferMinimumDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferTargetDelay',
// videoTrackReport.jitterBufferTargetDelay,
// ''
// )
transaction.setMeasurement(
'rtcKeyFramesDecoded',
videoTrackReport.keyFramesDecoded,
''
)
transaction.setMeasurement(
'rtcTotalFreezesDuration',
videoTrackReport.totalFreezesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalInterFrameDelay',
// videoTrackReport.totalInterFrameDelay,
// ''
// )
transaction.setMeasurement(
'rtcTotalPausesDuration',
videoTrackReport.totalPausesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalProcessingDelay',
// videoTrackReport.totalProcessingDelay,
// 'second'
// )
} else if (videoTrackReport.type === 'transport') {
// // Bytes i/o
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesReceived',
// videoTrackReport.bytesReceived,
// 'byte'
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesSent',
// videoTrackReport.bytesSent,
// 'byte'
// )
}
})
transaction?.finish()
// TODO(paultag): Since we can technically have multiple WebRTC
// video tracks (even if the Server doesn't at the moment), we
// ought to send stats for every video track(?), and add the stream
// ID into it. This raises the cardinality of collected metrics
// when/if we do, but for now, just report the one stream.
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
client_metrics.rtc_frames_decoded =
videoTrackReport.framesDecoded
client_metrics.rtc_frames_dropped =
videoTrackReport.framesDropped
client_metrics.rtc_frames_received =
videoTrackReport.framesReceived
client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded
client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration
} else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent,
}
})
resolve(client_metrics)
})
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
})
}
this.onNewTrack({
@ -490,10 +446,6 @@ export class EngineConnection {
})
})
// During startup, we'll track the time from `connect` being called
// until the 'done' event fires.
let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => {
this.unreliableDataChannel = event.channel
@ -537,6 +489,7 @@ export class EngineConnection {
this.websocket = undefined
this.pc = undefined
this.unreliableDataChannel = undefined
this.webrtcStatsCollector = undefined
this.onClose(this)
this.ready = false
@ -546,6 +499,8 @@ export class EngineConnection {
export type EngineCommand = Models['WebSocketRequest_type']
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
type CommandTypes = Models['ModelingCmd_type']['type']
type UnreliableResponses = Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'highlight_set_entity' }
@ -687,15 +642,22 @@ export class EngineCommandManager {
const resolve = command.resolve
this.artifactMap[id] = {
type: 'result',
range: command.range,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse,
}
resolve({
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
})
} else {
this.artifactMap[id] = {
type: 'result',
commandType: command?.commandType,
range: command?.range,
data: modelingResponse,
}
}
@ -747,8 +709,29 @@ export class EngineCommandManager {
delete this.unreliableSubscriptions[event][id]
}
endSession() {
// this.websocket?.close()
// socket.off('command')
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
// we need to loop over them each individualy because if the engine doesn't recognise a single
// id the whole command fails.
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
// however all of the segments in the path are its children so there don't need to be deleted.
// this fact is very opaque in the api and docs (as to what should can be deleted).
// Using an array is the list is likely to grow.
'start_path',
]
if (!artifactTypesToDelete.includes(artifact.commandType)) return
const deletCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'remove_scene_objects',
object_ids: [id],
},
}
this.engineConnection?.send(deletCmd)
})
}
cusorsSelected(selections: {
otherSelections: Selections['otherSelections']
@ -775,6 +758,13 @@ export class EngineCommandManager {
})
}
sendSceneCommand(command: EngineCommand): Promise<any> {
if (
command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage
) {
console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type
}
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return Promise.resolve()
@ -782,7 +772,8 @@ export class EngineCommandManager {
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd
if (
cmd.type === 'camera_drag_move' &&
(cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') &&
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
@ -801,11 +792,20 @@ export class EngineCommandManager {
JSON.stringify(command)
)
return Promise.resolve()
} else if (
cmd.type === 'mouse_move' &&
this.engineConnection.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
}
console.log('sending command', command)
// since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id)
return this.handlePendingCommand(command.cmd_id, command.cmd)
}
sendModelingCommand({
id,
@ -823,15 +823,35 @@ export class EngineCommandManager {
return Promise.resolve()
}
this.engineConnection?.send(command)
return this.handlePendingCommand(id)
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, command?.cmd, range)
} else if (typeof command === 'string') {
const parseCommand: EngineCommand = JSON.parse(command)
if (parseCommand.type === 'modeling_cmd_req')
return this.handlePendingCommand(id, parseCommand?.cmd, range)
}
throw 'shouldnt reach here'
}
handlePendingCommand(id: string) {
handlePendingCommand(
id: string,
command: Models['ModelingCmd_type'],
range?: SourceRange
) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') {
return command.path
}
// TODO handle other commands that have a parent
}
this.artifactMap[id] = {
range: range || [0, 0],
type: 'pending',
commandType: command.type,
parentId: getParentId(),
promise,
resolve,
}
@ -865,7 +885,10 @@ export class EngineCommandManager {
}
return command.promise
}
async waitForAllCommands(): Promise<{
async waitForAllCommands(
ast?: Program,
programMemory?: ProgramMemory
): Promise<{
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}> {
@ -874,9 +897,94 @@ export class EngineCommandManager {
) as PendingCommand[]
const proms = pendingCommands.map(({ promise }) => promise)
await Promise.all(proms)
if (ast && programMemory) {
await this.fixIdMappings(ast, programMemory)
}
return {
artifactMap: this.artifactMap,
sourceRangeMap: this.sourceRangeMap,
}
}
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
/* This is a temporary solution since the cmd_ids that are sent through when
sending 'extend_path' ids are not used as the segment ids.
We have a way to back fill them with 'path_get_info', however this relies on one
the sketchGroup array and the segements array returned from the server to be in
the same length and order. plus it's super hacky, we first use the path_id to get
the source range of the pipe expression then use the name of the variable to get
the sketchGroup from programMemory.
I feel queezy about relying on all these steps to always line up.
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
We should get the cmd_ids to match with the segment ids and delete this method.
*/
const pathInfoProms = []
for (const [id, artifact] of Object.entries(this.artifactMap)) {
if (artifact.commandType === 'start_path') {
pathInfoProms.push(
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: id,
},
}).then(({ data }) => ({
originalId: id,
segments: data?.data?.segments,
}))
)
}
}
const pathInfos = await Promise.all(pathInfoProms)
pathInfos.forEach(({ originalId, segments }) => {
const originalArtifact = this.artifactMap[originalId]
if (!originalArtifact || originalArtifact.type === 'pending') {
console.log('problem')
return
}
const pipeExpPath = getNodePathFromSourceRange(
ast,
originalArtifact.range
)
const pipeExp = getNodeFromPath<VariableDeclarator>(
ast,
pipeExpPath,
'VariableDeclarator'
).node
if (pipeExp.type !== 'VariableDeclarator') {
console.log('problem', pipeExp, pipeExpPath, ast)
return
}
const variableName = pipeExp.id.name
const memoryItem = programMemory.root[variableName]
if (!memoryItem) {
console.log('problem', variableName, programMemory)
return
} else if (memoryItem.type !== 'SketchGroup') {
console.log('problem', memoryItem, programMemory)
return
}
const relevantSegments = segments.filter(
({ command_id }: { command_id: string | null }) => command_id
)
if (memoryItem.value.length !== relevantSegments.length) {
console.log('problem', memoryItem.value, relevantSegments)
return
}
for (let i = 0; i < relevantSegments.length; i++) {
const engineSegment = relevantSegments[i]
const memorySegment = memoryItem.value[i]
const oldId = memorySegment.__geoMeta.id
const artifact = this.artifactMap[oldId]
delete this.artifactMap[oldId]
delete this.sourceRangeMap[oldId]
this.artifactMap[engineSegment.command_id] = artifact
this.sourceRangeMap[engineSegment.command_id] = artifact.range
}
})
}
}

View File

@ -101,7 +101,8 @@ describe('testing changeSketchArguments', () => {
|> ${line}
|> lineTo([0.46, -5.82], %)
// |> rx(45, %)
show(mySketch001)`
show(mySketch001)
`
const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange)
const ast = parser_wasm(code)
@ -164,7 +165,8 @@ show(mySketch001)`
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %)
show(mySketch001)`
show(mySketch001)
`
expect(recast(modifiedAst)).toBe(expectedCode)
})
})
@ -177,7 +179,8 @@ describe('testing addTagForSketchOnFace', () => {
// |> rx(45, %)
|> ${line}
|> lineTo([0.46, -5.82], %)
show(mySketch001)`
show(mySketch001)
`
const code = genCode(originalLine)
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast)

View File

@ -4,6 +4,7 @@ import {
SketchGroup,
SourceRange,
PathToNode,
MemoryItem,
} from '../executor'
import {
Program,
@ -19,8 +20,9 @@ import {
getNodeFromPathCurry,
getNodePathFromSourceRange,
} from '../queryAst'
import { isLiteralArrayOrStatic } from './sketchcombos'
import { GuiModes, toolTips, TooTip } from '../../useStore'
import { splitPathAtPipeExpression } from '../modifyAst'
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { generateUuidFromHashSeed } from '../../lib/uuid'
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
@ -185,7 +187,7 @@ export const line: SketchLineHelper = {
createCallback,
}) => {
const _node = { ...node }
const { node: pipe } = getNodeFromPath<PipeExpression>(
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
_node,
pathToNode,
'PipeExpression'
@ -197,12 +199,12 @@ export const line: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
if (replaceExisting && createCallback) {
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[newXVal, newYVal],
@ -220,7 +222,11 @@ export const line: SketchLineHelper = {
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
])
pipe.body = [...pipe.body, callExp]
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, callExp]
} else {
varDec.init = createPipeExpression([varDec.init, callExp])
}
return {
modifiedAst: _node,
pathToNode,
@ -238,22 +244,10 @@ export const line: SketchLineHelper = {
createLiteral(roundOff(to[1] - from[1], 2)),
])
if (
callExpression.arguments?.[0].type === 'Literal' &&
callExpression.arguments?.[0].value === 'default'
) {
callExpression.arguments[0] = toArrExp
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
const toProp = callExpression.arguments?.[0].properties?.find(
({ key }) => key.name === 'to'
)
if (
toProp &&
toProp.value.type === 'Literal' &&
toProp.value.value === 'default'
) {
toProp.value = toArrExp
}
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
} else {
mutateArrExp(callExpression.arguments?.[0], toArrExp)
@ -301,7 +295,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode
)
const newX = createLiteral(roundOff(to[0], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
@ -349,7 +343,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode
)
const newY = createLiteral(roundOff(to[1], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
@ -399,7 +393,7 @@ export const xLine: SketchLineHelper = {
pathToNode
)
const newX = createLiteral(roundOff(to[0] - from[0], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
@ -443,7 +437,7 @@ export const yLine: SketchLineHelper = {
pathToNode
)
const newY = createLiteral(roundOff(to[1] - from[1], 2))
if (callExpression.arguments?.[0]?.type === 'Literal') {
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
@ -546,7 +540,7 @@ export const angledLineOfXLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
const newLine = createCallback
@ -619,7 +613,7 @@ export const angledLineOfYLength: SketchLineHelper = {
)
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
@ -876,7 +870,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
const intersectPath = sketchGroup.value.find(
({ name }) => name === intersectTagName
({ name }: Path) => name === intersectTagName
)
let offset = 0
if (intersectPath) {
@ -968,60 +962,14 @@ export function addNewSketchLn({
pathToNode,
'VariableDeclarator'
)
const { node: pipeExp, shallowPath: pipePath } =
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
const maybeStartSketchAt = pipeExp.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startSketchAt' &&
exp.arguments[0].type === 'Literal' &&
exp.arguments[0].value === 'default'
)
const maybeDefaultLine = pipeExp.body.findIndex(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'line' &&
exp.arguments[0].type === 'Literal' &&
exp.arguments[0].value === 'default'
)
const defaultLinePath: PathToNode = [
...pipePath,
['body', ''],
[maybeDefaultLine, ''],
]
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
PipeExpression | CallExpression
>(node, pathToNode, 'PipeExpression')
const variableName = varDec.id.name
const sketch = previousProgramMemory?.root?.[variableName]
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
if (maybeStartSketchAt) {
const startSketchAt = maybeStartSketchAt as any
startSketchAt.arguments[0] = createArrayExpression([
createLiteral(to[0]),
createLiteral(to[1]),
])
return {
modifiedAst: node,
}
}
if (maybeDefaultLine !== -1) {
const defaultLine = getNodeFromPath<CallExpression>(
node,
defaultLinePath
).node
const { from } = getSketchSegmentFromSourceRange(sketch, [
defaultLine.start,
defaultLine.end,
]).segment
return updateArgs({
node,
previousProgramMemory,
pathToNode: defaultLinePath,
to,
from,
})
}
const last = sketch.value[sketch.value.length - 1]
const last = sketch.value[sketch.value.length - 1] || sketch.start
const from = last.to
return add({
@ -1089,10 +1037,11 @@ export function addTagForSketchOnFace(
function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression'
? lineArugement.elements[0].type === 'Literal'
? isLiteralArrayOrStatic(lineArugement.elements[0])
: lineArugement?.type === 'ObjectExpression'
? lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
.type === 'Literal'
? isLiteralArrayOrStatic(
lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
)
: false
}
@ -1198,14 +1147,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
} {
// used for lineTo, line
const firstArg = callExpression.arguments[0]
if (firstArg.type === 'Literal' && firstArg.value === 'default') {
return {
val:
callExpression.callee.name === 'startSketchAt'
? [createLiteral(0), createLiteral(0)]
: [createLiteral(1), createLiteral(1)],
}
}
if (firstArg.type === 'ArrayExpression') {
return { val: [firstArg.elements[0], firstArg.elements[1]] }
}
@ -1215,8 +1156,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
if (to?.type === 'ArrayExpression') {
const [x, y] = to.elements
return { val: [x, y], tag }
} else if (to?.type === 'Literal' && to.value === 'default') {
return { val: [createLiteral(0), createLiteral(0)], tag }
}
}
throw new Error('expected ArrayExpression or ObjectExpression')

View File

@ -59,19 +59,19 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
` |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
` |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
` |> angledLine({`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` }, %)`,
` |> angledLineOfXLength({`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` }, %)`,
` |> angledLineOfYLength({`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` }, %)`,
` |> angledLineToX({ angle: 55, to: -2.89, tag: 'abc6' }, %)`,
` |> angledLineToY({ angle: 330, to: 2.53, tag: 'abc7' }, %)`,
@ -144,9 +144,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLine({`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` angle: 157,`,
` length: 1.69,`,
` tag: 'abc3'`,
` }, %)`,
].join('\n'),
constraintType: 'horizontal',
@ -172,9 +172,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLineOfXLength({`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` angle: 217,`,
` length: 0.86,`,
` tag: 'abc4'`,
` }, %)`,
].join('\n'),
constraintType: 'horizontal',
@ -201,9 +201,9 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
inputCode: bigExample,
callToSwap: [
`angledLineOfYLength({`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` angle: 104,`,
` length: 1.58,`,
` tag: 'abc5'`,
` }, %)`,
].join('\n'),
constraintType: 'vertical',
@ -401,6 +401,11 @@ show(part001)`
programMemory.root['part001'] as SketchGroup,
[index, index]
).segment
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
expect(segment).toEqual({
to: [0, 0.04],
from: [0, 0.04],
name: '',
type: 'base',
})
})
})

View File

@ -4,7 +4,7 @@ import {
VariableDeclarator,
CallExpression,
} from '../abstractSyntaxTreeTypes'
import { SketchGroup, SourceRange } from '../executor'
import { SketchGroup, SourceRange, Path } from '../executor'
export function getSketchSegmentFromSourceRange(
sketchGroup: SketchGroup,
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
startSourceRange[1] >= rangeEnd &&
sketchGroup.start
)
return { segment: sketchGroup.start, index: -1 }
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
const lineIndex = sketchGroup.value.findIndex(
({ __geoMeta: { sourceRange } }) =>
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketchGroup.value[lineIndex]

View File

@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
show(part001)`
show(part001)
`
const expectModifiedScript = `const myVar = 3
const myVar2 = 5
const myVar3 = 6
@ -133,69 +134,70 @@ const myAng2 = 134
const part001 = startSketchAt([0, 0])
|> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
|> angledLineToX([
-angleToMatchLengthX('seg01', myVar, %),
myVar
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
-angleToMatchLengthX('seg01', myVar, %),
myVar
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLineToY([
-angleToMatchLengthY('seg01', myVar, %),
myVar
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
-angleToMatchLengthY('seg01', myVar, %),
myVar
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|> angledLine([45, segLen('seg01', %)], %) // ln-lineTo-free should become angledLine
|> angledLine([45, segLen('seg01', %)], %) // ln-angledLineToX-free should become angledLine
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineToX-angle should become angledLine
|> angledLineToX([
angleToMatchLengthX('seg01', myVar2, %),
myVar2
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
angleToMatchLengthX('seg01', myVar2, %),
myVar2
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
|> angledLine([-45, segLen('seg01', %)], %) // ln-angledLineToY-free should become angledLine
|> angledLine([myAng2, segLen('seg01', %)], %) // ln-angledLineToY-angle should become angledLine
|> angledLineToY([
angleToMatchLengthY('seg01', myVar3, %),
myVar3
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
angleToMatchLengthY('seg01', myVar3, %),
myVar3
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
|> line([
min(segLen('seg01', %), myVar),
legLen(segLen('seg01', %), myVar)
], %) // ln-should use legLen for y
min(segLen('seg01', %), myVar),
legLen(segLen('seg01', %), myVar)
], %) // ln-should use legLen for y
|> line([
min(segLen('seg01', %), myVar),
-legLen(segLen('seg01', %), myVar)
], %) // ln-legLen but negative
min(segLen('seg01', %), myVar),
-legLen(segLen('seg01', %), myVar)
], %) // ln-legLen but negative
|> angledLine([-112, segLen('seg01', %)], %) // ln-should become angledLine
|> angledLine([myVar, segLen('seg01', %)], %) // ln-use segLen for secound arg
|> angledLine([45, segLen('seg01', %)], %) // ln-segLen again
|> angledLine([54, segLen('seg01', %)], %) // ln-should be transformed to angledLine
|> angledLineOfXLength([
legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-should use legAngX to calculate angle
legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-should use legAngX to calculate angle
|> angledLineOfXLength([
180 + legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-same as above but should have + 180 to match original quadrant
180 + legAngX(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-same as above but should have + 180 to match original quadrant
|> line([
legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-legLen again but yRelative
legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-legLen again but yRelative
|> line([
-legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-negative legLen yRelative
-legLen(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-negative legLen yRelative
|> angledLine([58, segLen('seg01', %)], %) // ln-angledLineOfYLength-free should become angledLine
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineOfYLength-angle should become angledLine
|> angledLineOfXLength([
legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative use legAngY
legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative use legAngY
|> angledLineOfXLength([
270 + legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
270 + legAngY(segLen('seg01', %), myVar),
min(segLen('seg01', %), myVar)
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
|> xLine(segLen('seg01', %), %) // ln-xLine-free should sub in segLen
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
show(part001)`
show(part001)
`
it('should transform the ast', async () => {
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)`
show(part001)
`
it('should transform horizontal lines the ast', async () => {
const expectModifiedScript = `const myVar = 2
const myVar2 = 12
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
show(part001)`
show(part001)
`
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n')
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
|> yLineTo(7.68, %) // select for vertical constraint 9
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|> yLineTo(myVar, %) // select for vertical constraint 10
show(part001)`
show(part001)
`
const ast = parser_wasm(inputScript)
const selectionRanges: Selections['codeBasedSelections'] = inputScript
.split('\n')
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
|> line([0.45, 1.46], %) // free
|> line([myVar, 0.01], %) // xRelative
|> line([0.7, myVar], %) // yRelative
show(part001)`
show(part001)
`
it('testing for free to horizontal and vertical distance', async () => {
const expectedHorizontalCode = await helperThing(
inputScript,
@ -406,9 +412,9 @@ show(part001)`
'setVertDistance'
)
expect(expectedCode).toContain(`|> lineTo([
lastSegX(%) + myVar,
segEndY('seg01', %) + 2.93
], %) // xRelative`)
lastSegX(%) + myVar,
segEndY('seg01', %) + 2.93
], %) // xRelative`)
})
it('testing for yRelative to horizontal distance', async () => {
const expectedCode = await helperThing(
@ -417,9 +423,9 @@ show(part001)`
'setHorzDistance'
)
expect(expectedCode).toContain(`|> lineTo([
segEndX('seg01', %) + 2.6,
lastSegY(%) + myVar
], %) // yRelative`)
segEndX('seg01', %) + 2.6,
lastSegY(%) + myVar
], %) // yRelative`)
})
})
})

View File

@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
import { PathToNode, ProgramMemory } from '../executor'
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
type LineInputsType =
| 'xAbsolute'
@ -1136,27 +1137,18 @@ export function getRemoveConstraintsTransform(
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' || b?.type !== 'Literal') {
return transformInfo
}
} else {
if (firstArg.val?.type !== 'Literal') {
return transformInfo
}
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return transformInfo
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) {
return false
}
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isOneValFree) {
return transformInfo
}
@ -1187,25 +1179,12 @@ function getTransformMapPath(
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' && b?.type !== 'Literal') {
return false
}
} else {
if (firstArg.val?.type !== 'Literal') {
return false
}
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return false
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
if (isTwoValFree || isOneValFree) {
if (isLiteralArrayOrStatic(firstArg.val)) {
const info = transformMap?.[name]?.free?.[constraintType]
if (info)
return {
@ -1259,7 +1238,7 @@ export function getConstraintType(
if (fnName === 'xLineTo') return 'yAbsolute'
if (fnName === 'yLineTo') return 'xAbsolute'
} else {
const isFirstArgLockedDown = val?.[0]?.type !== 'Literal'
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
if (fnName === 'line')
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
if (fnName === 'lineTo')
@ -1426,7 +1405,6 @@ export function transformAstSketchLines({
selectionRanges.codeBasedSelections.forEach(({ range }, index) => {
const callBack = transformInfos?.[index].createNode
const transformTo = transformInfos?.[index].tooltip
console.log('transformTo', transformInfos)
if (!callBack || !transformTo) throw new Error('no callback helper')
const getNode = getNodeFromPathCurry(
@ -1453,7 +1431,7 @@ export function transformAstSketchLines({
const varName = varDec.id.name
const sketchGroup = programMemory.root?.[varName]
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
throw new Error('not a sketch group')
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
const referencedSegment = referencedSegmentRange
@ -1539,23 +1517,46 @@ export function getConstraintLevelFromSourceRange(
const firstArg = getFirstArg(sketchFnExp)
// check if the function is fully constrained
if (Array.isArray(firstArg.val)) {
const [a, b] = firstArg.val
if (a?.type !== 'Literal' && b?.type !== 'Literal') return 'full'
} else {
if (firstArg.val?.type !== 'Literal') return 'full'
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return 'full'
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) &&
firstArg.val?.[0]?.type === 'Literal' &&
firstArg.val?.[1]?.type === 'Literal'
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree =
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return 'free'
if (isOneValFree) return 'partial'
return 'partial'
}
export function isLiteralArrayOrStatic(
val: Value | [Value, Value] | [Value, Value, Value] | undefined
): boolean {
if (!val) return false
if (Array.isArray(val)) {
const [a, b] = val
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
}
return (
val.type === 'Literal' ||
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
)
}
export function isNotLiteralArrayOrStatic(
val: Value | [Value, Value] | [Value, Value, Value]
): boolean {
if (Array.isArray(val)) {
const [a, b] = val
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
}
return (
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
)
}

View File

@ -131,10 +131,12 @@ const yi=45`
})
it('test negative and decimal numbers', () => {
expect(stringSummaryLexer('-1')).toEqual([
"number '-1' from 0 to 2",
"operator '-' from 0 to 1",
"number '1' from 1 to 2",
])
expect(stringSummaryLexer('-1.5')).toEqual([
"number '-1.5' from 0 to 4",
"operator '-' from 0 to 1",
"number '1.5' from 1 to 4",
])
expect(stringSummaryLexer('1.5')).toEqual([
"number '1.5' from 0 to 3",
@ -158,10 +160,12 @@ const yi=45`
"whitespace ' ' from 3 to 4",
"operator '+' from 4 to 5",
"whitespace ' ' from 5 to 6",
"number '-2.5' from 6 to 10",
"operator '-' from 6 to 7",
"number '2.5' from 7 to 10",
])
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
"number '-1.5' from 0 to 4",
"operator '-' from 0 to 1",
"number '1.5' from 1 to 4",
"whitespace ' ' from 4 to 5",
"operator '+' from 5 to 6",
"whitespace ' ' from 6 to 7",

156
src/lib/cameraControls.ts Normal file
View File

@ -0,0 +1,156 @@
const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem =
| 'KittyCAD'
| 'OnShape'
| 'Trackpad Friendly'
| 'Solidworks'
| 'NX'
| 'Creo'
| 'AutoCAD'
export const cameraSystems: CameraSystem[] = [
'KittyCAD',
'OnShape',
'Trackpad Friendly',
'Solidworks',
'NX',
'Creo',
'AutoCAD',
]
interface MouseGuardHandler {
description: string
callback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
}
interface MouseGuardZoomHandler {
description: string
dragCallback: (e: React.MouseEvent) => boolean
scrollCallback: (e: React.MouseEvent) => boolean
lenientDragStartButton?: number
}
interface MouseGuard {
pan: MouseGuardHandler
zoom: MouseGuardZoomHandler
rotate: MouseGuardHandler
}
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 1 && noModifiersPressed(e)) ||
(e.button === 2 && e.shiftKey),
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
},
},
OnShape: {
pan: {
description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) =>
(e.button === 2 && e.ctrlKey) ||
(e.button === 1 && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
},
},
'Trackpad Friendly': {
pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
(e.button === 1 && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
scrollCallback: () => true,
},
rotate: {
description: 'Left click + Alt + drag',
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0,
},
},
Solidworks: {
pan: {
description: 'Right click + Ctrl + drag',
callback: (e) => e.button === 2 && e.ctrlKey,
lenientDragStartButton: 2,
},
zoom: {
description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => e.button === 1 && e.shiftKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
NX: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
Creo: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
},
AutoCAD: {
pan: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
},
zoom: {
description: 'Scroll wheel',
dragCallback: () => false,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
},
},
}

View File

@ -5,7 +5,7 @@ import {
readDir,
writeTextFile,
} from '@tauri-apps/api/fs'
import { documentDir } from '@tauri-apps/api/path'
import { documentDir, homeDir } from '@tauri-apps/api/path'
import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
return directory
}
const docDirectory = await documentDir()
let docDirectory: string
try {
docDirectory = await documentDir()
} catch (e) {
console.log(e)
docDirectory = await homeDir() // seems to work better on Linux
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER

View File

@ -49,7 +49,7 @@ class MockEngineCommandManager {
export async function enginelessExecutor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const mockEngineCommandManager = new MockEngineCommandManager({
setIsStreamReady: () => {},
@ -64,7 +64,7 @@ export async function enginelessExecutor(
export async function executor(
ast: Program,
pm: ProgramMemory = { root: {} }
pm: ProgramMemory = { root: {}, return: null }
): Promise<ProgramMemory> {
const engineCommandManager = new EngineCommandManager({
setIsStreamReady: () => {},
@ -75,6 +75,6 @@ export async function executor(
await engineCommandManager.waitForReady
engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager)
await engineCommandManager.waitForAllCommands()
await engineCommandManager.waitForAllCommands(ast, programMemory)
return programMemory
}

View File

@ -56,6 +56,27 @@ export function throttle<T>(
return throttled
}
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function defferExecution<T>(func: (args: T) => any, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T
function later() {
timeout = null
func(latestArgs)
}
function deffered(args: T) {
latestArgs = args
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
return deffered
}
export function getNormalisedCoordinates({
clientX,
clientY,

View File

@ -118,16 +118,14 @@ async function getUser(context: UserContext) {
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
if (SKIP_AUTH) return LOCAL_USER
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
}

View File

@ -1,29 +1,54 @@
import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type Toggle = 'On' | 'Off'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsCommandBarMeta: CommandBarMeta = {
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'theme',
name: 'baseUnit',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v) => ({ name: v })) as {
name: string
}[],
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Camera Controls': {
displayValue: (args: string[]) => 'Set your camera controls',
args: [
{
name: 'cameraControls',
type: 'select',
defaultValue: 'cameraControls',
options: Object.values(cameraSystems).map((v) => ({ name: v })),
},
],
},
'Set Default Directory': {
hide: 'both',
},
'Set Default Project Name': {
displayValue: (args: string[]) => 'Set a new default project name',
hide: 'web',
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
},
],
},
'Set Default Directory': {
'Set Onboarding Status': {
hide: 'both',
},
'Set Text Wrapping': {
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
args: [
{
name: 'textWrapping',
type: 'select',
defaultValue: 'textWrapping',
options: [{ name: 'On' }, { name: 'Off' }],
},
],
},
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
args: [
{
name: 'theme',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v): { name: string } => ({
name: v,
})),
},
],
},
'Set Unit System': {
displayValue: (args: string[]) => 'Set your default unit system',
args: [
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
},
],
},
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'baseUnit',
type: 'select',
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Onboarding Status': {
hide: 'both',
},
}
export const settingsMachine = createMachine(
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
id: 'Settings',
predictableActionArguments: true,
context: {
theme: Themes.System,
defaultProjectName: '',
unitSystem: UnitSystem.Imperial,
baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '',
showDebugPanel: false,
defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
},
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass'],
on: {
'Set Theme': {
'Set Base Unit': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
assign({ baseUnit: (_, event) => event.data.baseUnit }),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
'Set Camera Controls': {
actions: [
assign({
defaultProjectName: (_, event) => event.data.defaultProjectName,
cameraControls: (_, event) => event.data.cameraControls,
}),
'persistSettings',
'toastSuccess',
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Unit System': {
'Set Default Project Name': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
defaultProjectName: (_, event) =>
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
}),
'persistSettings',
'toastSuccess',
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Base Unit': {
'Set Onboarding Status': {
actions: [
assign({ baseUnit: (_, event) => event.data.baseUnit }),
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
'Set Text Wrapping': {
actions: [
assign({
textWrapping: (_, event) => event.data.textWrapping,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
],
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set Theme'; data: { theme: Themes } }
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| {
type: 'Set Camera Controls'
data: { cameraControls: CameraSystem }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Unit System'
data: { unitSystem: UnitSystem }
}
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Toggle Debug Panel' },
},
},

View File

@ -15,25 +15,31 @@ export interface Typegen0 {
eventsCausingActions: {
persistSettings:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
setThemeClass:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
| 'xstate.init'
toastSuccess:
| 'Set Base Unit'
| 'Set Camera Controls'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Text Wrapping'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'

View File

@ -28,6 +28,7 @@ import {
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -38,6 +39,7 @@ const Home = () => {
const {
settings: {
context: { defaultDirectory, defaultProjectName },
send: sendToSettings,
},
} = useGlobalStateContext()
@ -71,16 +73,33 @@ const Home = () => {
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
) => {
let name =
let name = (
event.data && 'name' in event.data
? event.data.name
: defaultProjectName
).trim()
let shouldUpdateDefaultProjectName = false
// If there is no default project name, flag it to be set to the default
if (!name) {
name = DEFAULT_PROJECT_NAME
shouldUpdateDefaultProjectName = true
}
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProject(context.defaultDirectory + '/' + name)
if (shouldUpdateDefaultProjectName) {
sendToSettings({
type: 'Set Default Project Name',
data: { defaultProjectName: DEFAULT_PROJECT_NAME },
})
}
return `Successfully created "${name}"`
},
renameProject: async (

View File

@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useStore } from '../../useStore'
export default function Units() {
const { isMouseDownInStream } = useStore((s) => ({
isMouseDownInStream: s.isMouseDownInStream,
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING)
@ -15,7 +15,7 @@ export default function Units() {
<div
className={
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
(isMouseDownInStream ? '' : ' pointer-events-auto')
(buttonDownInStream ? '' : ' pointer-events-auto')
}
>
<h1 className="text-2xl font-bold">Camera</h1>

View File

@ -1,5 +1,5 @@
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
import { BaseUnit, baseUnits } from '../../useStore'
import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle'

View File

@ -6,13 +6,22 @@ import {
import { ActionButton } from '../components/ActionButton'
import { AppHeader } from '../components/AppHeader'
import { open } from '@tauri-apps/api/dialog'
import { BaseUnit, baseUnits } from '../useStore'
import {
BaseUnit,
DEFAULT_PROJECT_NAME,
baseUnits,
} from '../machines/settingsMachine'
import { Toggle } from '../components/Toggle/Toggle'
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import {
CameraSystem,
cameraSystems,
cameraMouseDragGuards,
} from 'lib/cameraControls'
import { UnitSystem } from 'machines/settingsMachine'
export const Settings = () => {
@ -25,12 +34,13 @@ export const Settings = () => {
send,
state: {
context: {
baseUnit,
cameraControls,
defaultDirectory,
defaultProjectName,
showDebugPanel,
defaultDirectory,
unitSystem,
baseUnit,
theme,
unitSystem,
},
},
},
@ -82,6 +92,42 @@ export const Settings = () => {
, and start a discussion if you don't see it! Your feedback will help
us prioritize what to build next.
</p>
<SettingsSection
title="Camera Controls"
description="How you want to control the camera in the 3D view"
>
<select
id="camera-controls"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={cameraControls}
onChange={(e) => {
send({
type: 'Set Camera Controls',
data: { cameraControls: e.target.value as CameraSystem },
})
}}
>
{cameraSystems.map((program) => (
<option key={program} value={program}>
{program}
</option>
))}
</select>
<ul className="text-sm my-2 mx-4 leading-relaxed">
<li>
<strong>Pan:</strong>{' '}
{cameraMouseDragGuards[cameraControls].pan.description}
</li>
<li>
<strong>Zoom:</strong>{' '}
{cameraMouseDragGuards[cameraControls].zoom.description}
</li>
<li>
<strong>Rotate:</strong>{' '}
{cameraMouseDragGuards[cameraControls].rotate.description}
</li>
</ul>
</SettingsSection>
{(window as any).__TAURI__ && (
<>
<SettingsSection
@ -118,10 +164,14 @@ export const Settings = () => {
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
defaultValue={defaultProjectName}
onBlur={(e) => {
const newValue = e.target.value.trim() || DEFAULT_PROJECT_NAME
send({
type: 'Set Default Project Name',
data: { defaultProjectName: e.target.value },
data: {
defaultProjectName: newValue,
},
})
e.target.value = newValue
}}
autoCapitalize="off"
autoComplete="off"

View File

@ -19,6 +19,7 @@ import {
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
import { defferExecution } from 'lib/utils'
export type Selection = {
type: 'default' | 'line-end' | 'line-mid'
@ -42,9 +43,12 @@ export type TooTip =
| 'yLineTo'
| 'angledLineThatIntersects'
export const toolTips: TooTip[] = [
'lineTo',
export const toolTips = [
'sketch_line',
'move',
// original tooltips
'line',
'lineTo',
'angledLine',
'angledLineOfXLength',
'angledLineOfYLength',
@ -55,7 +59,7 @@ export const toolTips: TooTip[] = [
'xLineTo',
'yLineTo',
'angledLineThatIntersects',
]
] as any as TooTip[]
export type GuiModes =
| {
@ -65,6 +69,7 @@ export type GuiModes =
mode: 'sketch'
sketchMode: TooTip
isTooltip: true
waitingFirstClick: boolean
rotation: Rotation
position: Position
id?: string
@ -83,6 +88,7 @@ export type GuiModes =
}
| {
mode: 'canEditSketch'
pathId: string
pathToNode: PathToNode
rotation: Rotation
position: Position
@ -94,15 +100,6 @@ export type GuiModes =
position: Position
}
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type PaneType =
| 'code'
| 'variables'
@ -130,8 +127,8 @@ export interface StoreState {
kclErrors: KCLError[]
addKCLError: (err: KCLError) => void
resetKCLErrors: () => void
ast: Program | null
setAst: (ast: Program | null) => void
ast: Program
setAst: (ast: Program) => void
updateAst: (
ast: Program,
optionalParams?: {
@ -141,7 +138,9 @@ export interface StoreState {
) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
code: string
defferedCode: string
setCode: (code: string) => void
defferedSetCode: (code: string) => void
formatCode: () => void
errorState: {
isError: boolean
@ -166,8 +165,8 @@ export interface StoreState {
setIsStreamReady: (isStreamReady: boolean) => void
isLSPServerReady: boolean
setIsLSPServerReady: (isLSPServerReady: boolean) => void
isMouseDownInStream: boolean
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
buttonDownInStream: number | undefined
setButtonDownInStream: (buttonDownInStream: number | undefined) => void
didDragInStream: boolean
setDidDragInStream: (didDragInStream: boolean) => void
fileId: string
@ -177,6 +176,8 @@ export interface StoreState {
streamWidth: number
streamHeight: number
}) => void
isExecuting: boolean
setIsExecuting: (isExecuting: boolean) => void
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
@ -195,193 +196,220 @@ let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
if (editorView) {
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
set({ selectionRangeTypeMap })
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
(set, get) => {
const setDefferedCode = defferExecution(
(code: string) => set({ defferedCode: code }),
600
)
return {
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
if (editorView) {
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
})
setTimeout(() => {
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
set({ selectionRangeTypeMap })
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
}
})
})
},
setCursor2: (codeSelections) => {
const currestSelections = get().selectionRanges
const code = get().code
if (!codeSelections) {
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' },
],
})
return
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: get().isShiftDown
? [...currestSelections.codeBasedSelections, codeSelections]
: [codeSelections],
}
get().setCursor(selections)
},
selectionRangeTypeMap: {},
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
setSelectionRanges: (selectionRanges) =>
set({ selectionRanges, selectionRangeTypeMap: {} }),
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
set({ guiMode })
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
set((state) => ({ logs: [...state.logs, cleanLog] }))
} else {
set((state) => ({ logs: [...state.logs, log] }))
}
},
resetLogs: () => {
set({ logs: [] })
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
resetKCLErrors: () => {
set({ kclErrors: [] })
},
ast: null,
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource)
set({ ast: astWithUpdatedSource, code: newCode })
if (focusPath) {
const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
ranges.length &&
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
})
})
},
setCursor2: (codeSelections) => {
const currestSelections = get().selectionRanges
const code = get().code
if (!codeSelections) {
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{
type: 'default',
range: [start, end],
},
{ range: [0, code.length - 1], type: 'default' },
],
otherSelections: [],
})
})
}
},
updateAstAsync: async (ast, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
}, 100) as unknown as number
)
},
code: '',
setCode: (code) => {
set({ code })
},
formatCode: async () => {
const code = get().code
const ast = parser_wasm(code)
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, pendingMemory: {} },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isLSPServerReady: false,
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
isMouseDownInStream: false,
setIsMouseDownInStream: (isMouseDownInStream) => {
set({ isMouseDownInStream })
},
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })
},
// For stream event handling
fileId: '',
setFileId: (fileId) => set({ fileId }),
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
return
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: get().isShiftDown
? [...currestSelections.codeBasedSelections, codeSelections]
: [codeSelections],
}
get().setCursor(selections)
},
selectionRangeTypeMap: {},
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
setSelectionRanges: (selectionRanges) =>
set({ selectionRanges, selectionRangeTypeMap: {} }),
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
set({ guiMode })
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
set((state) => ({ logs: [...state.logs, cleanLog] }))
} else {
set((state) => ({ logs: [...state.logs, log] }))
}
},
resetLogs: () => {
set({ logs: [] })
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
resetKCLErrors: () => {
set({ kclErrors: [] })
},
ast: {
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
},
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource)
// tauri specific app settings
defaultDir: {
dir: '',
},
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }),
showHomeMenu: true,
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
}),
set({
ast: astWithUpdatedSource,
code: newCode,
defferedCode: newCode,
})
if (focusPath) {
const { node } = getNodeFromPath<any>(
astWithUpdatedSource,
focusPath
)
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
get().setCursor({
codeBasedSelections: [
{
type: 'default',
range: [start, end],
},
],
otherSelections: [],
})
})
}
},
updateAstAsync: async (ast, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
}, 100) as unknown as number
)
},
code: '',
defferedCode: '',
setCode: (code) => set({ code, defferedCode: code }),
defferedSetCode: (code) => {
set({ code })
setDefferedCode(code)
},
formatCode: async () => {
const code = get().code
const ast = parser_wasm(code)
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, return: null },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isLSPServerReady: false,
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
buttonDownInStream: undefined,
setButtonDownInStream: (buttonDownInStream) => {
set({ buttonDownInStream })
},
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })
},
// For stream event handling
fileId: '',
setFileId: (fileId) => set({ fileId }),
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
isExecuting: false,
setIsExecuting: (isExecuting) => set({ isExecuting }),
// tauri specific app settings
defaultDir: {
dir: '',
},
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }),
showHomeMenu: true,
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
}
},
{
name: 'store',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
['code', 'openPanes'].includes(key)
['code', 'defferedCode', 'openPanes'].includes(key)
)
),
}

730
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,11 +11,20 @@ crate-type = ["cdylib"]
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
serde_json = "1.0.93"
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
serde_json = "1.0.106"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
[dev-dependencies]
anyhow = "1"
image = "0.24.7"
kittycad = "0.2.25"
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.6.1"
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures = "0.3.28"
js-sys = "0.3.64"

View File

@ -14,9 +14,9 @@ proc-macro = true
convert_case = "0.6.0"
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0.186", features = ["derive"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.29", features = ["full"] }
syn = { version = "2.0.33", features = ["full"] }
[dev-dependencies]
expectorate = "1.0.7"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.20"
version = "0.1.26"
edition = "2021"
license = "MIT"
@ -9,18 +9,18 @@ license = "MIT"
[dependencies]
anyhow = { version = "1.0.75", features = ["backtrace"] }
clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
clap = { version = "4.4.3", features = ["cargo", "derive", "env", "unicode"] }
dashmap = "5.5.3"
derive-docs = { version = "0.1.1" }
derive-docs = { version = "0.1.3" }
#derive-docs = { path = "../derive-docs" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.47"
serde = {version = "1.0.188", features = ["derive"] }
serde_json = "1.0.106"
thiserror = "1.0.48"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }

4
src/wasm-lib/kcl/fuzz/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

2218
src/wasm-lib/kcl/fuzz/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
[package]
name = "kcl-lib-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.kcl-lib]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[profile.release]
debug = 1
[[bin]]
name = "parser"
path = "fuzz_targets/parser.rs"
test = false
doc = false

View File

@ -0,0 +1,14 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
extern crate kcl_lib;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let tokens = kcl_lib::tokeniser::lexer(s);
let parser = kcl_lib::parser::Parser::new(tokens);
if let Ok(_) = parser.ast() {
println!("OK");
}
}
});

File diff suppressed because it is too large Load Diff

View File

@ -87,6 +87,9 @@ impl EngineConnection {
if let Some(msg) = ws_resp.resp {
match msg {
OkWebSocketResponseData::MetricsRequest {} => {
// @paultag todo
}
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
println!("got ice server info: {:?}", ice_servers);
}

View File

@ -98,16 +98,14 @@ impl ProgramReturn {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum MemoryItem {
UserVal {
value: serde_json::Value,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
UserVal(UserVal),
SketchGroup(SketchGroup),
ExtrudeGroup(ExtrudeGroup),
#[ts(skip)]
ExtrudeTransform(ExtrudeTransform),
#[ts(skip)]
Function {
#[serde(skip)]
func: Option<MemoryFunction>,
@ -119,7 +117,16 @@ pub enum MemoryItem {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct UserVal {
pub value: serde_json::Value,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeTransform {
pub position: Position,
pub rotation: Rotation,
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
impl From<MemoryItem> for Vec<SourceRange> {
fn from(item: MemoryItem) -> Self {
match item {
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
impl MemoryItem {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let MemoryItem::UserVal { value, .. } = self {
Ok(value.clone())
if let MemoryItem::UserVal(user_val) = self {
Ok(user_val.value.clone())
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a user value: {:?}", self),
@ -186,7 +193,7 @@ impl MemoryItem {
/// A sketch group is a collection of paths.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct SketchGroup {
/// The id of the sketch group.
pub id: uuid::Uuid,
@ -238,7 +245,7 @@ impl SketchGroup {
/// An extrude group is a collection of extrude surfaces.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeGroup {
/// The id of the extrude group.
pub id: uuid::Uuid,
@ -276,15 +283,15 @@ pub enum BodyType {
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Position(pub [f64; 3]);
pub struct Position(#[ts(type = "[number, number, number]")] pub [f64; 3]);
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct Rotation(pub [f64; 4]);
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export)]
pub struct SourceRange(pub [usize; 2]);
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
impl SourceRange {
/// Create a new source range.
@ -401,8 +408,10 @@ impl From<SourceRange> for Metadata {
#[serde(rename_all = "camelCase")]
pub struct BasePath {
/// The from point.
#[ts(type = "[number, number]")]
pub from: [f64; 2],
/// The to point.
#[ts(type = "[number, number]")]
pub to: [f64; 2],
/// The name of the path.
pub name: String,
@ -595,7 +604,9 @@ pub fn execute(
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?;
let result = func.call_fn(&args, memory, engine)?;
memory.return_ = result;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("No such name {} defined", fn_name),
@ -642,7 +653,7 @@ pub fn execute(
for (index, param) in function_expression.params.iter().enumerate() {
fn_memory.add(
&param.name,
args.clone().get(index).unwrap().clone(),
args.get(index).unwrap().clone(),
param.into(),
)?;
}
@ -696,11 +707,39 @@ pub fn execute(
let result = bin_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::UnaryExpression(unary_expr) => {
let result = unary_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?.clone();
memory.return_ = Some(ProgramReturn::Value(value));
}
_ => (),
Value::Literal(literal) => {
memory.return_ = Some(ProgramReturn::Value(literal.into()));
}
Value::ArrayExpression(array_expr) => {
let result = array_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::ObjectExpression(obj_expr) => {
let result = obj_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::CallExpression(call_expr) => {
let result = call_expr.execute(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::MemberExpression(member_expr) => {
let result = member_expr.get_result(memory)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeExpression(pipe_expr) => {
let result = pipe_expr.get_result(memory, &mut pipe_info, engine)?;
memory.return_ = Some(ProgramReturn::Value(result));
}
Value::PipeSubstitution(_) => {}
Value::FunctionExpression(_) => {}
},
}
}
@ -774,16 +813,16 @@ show(part001)"#,
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_fn_definitions() {
let ast = r#"const def = (x) => {
let ast = r#"fn def = (x) => {
return x
}
const ghi = (x) => {
fn ghi = (x) => {
return x
}
const jkl = (x) => {
fn jkl = (x) => {
return x
}
const hmm = (x) => {
fn hmm = (x) => {
return x
}
@ -856,4 +895,281 @@ show(part001)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_literal_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = () => {
return -8
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing()], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_unary_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = (x) => {
return -x
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing(8)], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_array_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn thing = (x) => {
return [0, -x]
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line(thing(8), %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_call_in_pipe() {
let ast = r#"const w = 20
const l = 8
const h = 10
fn other_thing = (y) => {
return -y
}
fn thing = (x) => {
return other_thing(x)
}
const firstExtrude = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, thing(8)], %)
|> close(%)
|> extrude(h, %)
show(firstExtrude)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let ast = r#"fn box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
const fnBox = box(3, 6, 10)
show(fnBox)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_period() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj.start)
|> line([0, obj.l], %)
|> line([obj.w, 0], %)
|> line([0, -obj.l], %)
|> close(%)
|> extrude(obj.h, %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj["start"])
|> line([0, obj["l"]], %)
|> line([obj["w"], 0], %)
|> line([0, -obj["l"]], %)
|> close(%)
|> extrude(obj["h"], %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_mix_period_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj["start"])
|> line([0, obj["l"]], %)
|> line([obj["w"], 0], %)
|> line([10 - obj["w"], -obj.l], %)
|> close(%)
|> extrude(obj["h"], %)
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // ignore til we get loops
async fn test_execute_with_function_sketch_loop_objects() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchAt(obj.start)
|> line([0, obj.l], %)
|> line([obj.w, 0], %)
|> line([0, -obj.l], %)
|> close(%)
|> extrude(obj.h, %)
return myBox
}
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
const thisBox = box(var)
show(thisBox)
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // ignore til we get loops
async fn test_execute_with_function_sketch_loop_array() {
let ast = r#"fn box = (h, l, w, start) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
const thisBox = box(var[0], var[1], var[2], var[3])
show(thisBox)
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_array_with_function() {
let ast = r#"fn box = (array) => {
let myBox = startSketchAt(array[0])
|> line([0, array[1]], %)
|> line([array[2], 0], %)
|> line([0, -array[1]], %)
|> close(%)
|> extrude(array[3], %)
return myBox
}
const thisBox = box([[0,0], 6, 10, 3])
show(thisBox)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(5.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(1.0),
memory.root.get("myVar").unwrap().get_json_value().unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(7.4),
memory.root.get("thing").unwrap().get_json_value().unwrap()
);
}
}

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::{
abstract_syntax_tree_types::{
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, ValueMeta,
BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Identifier, Literal, MemberExpression, ValueMeta,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -81,6 +81,7 @@ pub enum MathExpression {
BinaryExpression(Box<BinaryExpression>),
ExtendedBinaryExpression(Box<ExtendedBinaryExpression>),
ParenthesisToken(Box<ParenthesisToken>),
MemberExpression(Box<MemberExpression>),
}
impl MathExpression {
@ -92,6 +93,7 @@ impl MathExpression {
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
MathExpression::MemberExpression(member_expression) => member_expression.start(),
}
}
@ -103,6 +105,7 @@ impl MathExpression {
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
MathExpression::MemberExpression(member_expression) => member_expression.end(),
}
}
}
@ -133,7 +136,7 @@ impl ReversePolishNotation {
}
let current_token = self.parser.get_token(0)?;
if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
if current_token.token_type == TokenType::Word {
if let Ok(next) = self.parser.get_token(1) {
if next.token_type == TokenType::Brace && next.value == "(" {
let closing_brace = self.parser.find_closing_brace(1, 0, "")?;
@ -149,6 +152,24 @@ impl ReversePolishNotation {
);
return rpn.parse();
}
if (current_token.token_type == TokenType::Word)
&& (next.token_type == TokenType::Period
|| (next.token_type == TokenType::Brace && next.value == "["))
{
// Find the end of the binary expression, ie the member expression.
let end = self.parser.make_member_expression(0)?.last_index;
let rpn = ReversePolishNotation::new(
&self.parser.tokens[end + 1..],
&self
.previous_postfix
.iter()
.cloned()
.chain(self.parser.tokens[0..end + 1].iter().cloned())
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
let rpn = ReversePolishNotation::new(
@ -164,7 +185,6 @@ impl ReversePolishNotation {
return rpn.parse();
} else if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::Word
|| current_token.token_type == TokenType::Keyword
|| current_token.token_type == TokenType::String
{
let rpn = ReversePolishNotation::new(
@ -180,6 +200,35 @@ impl ReversePolishNotation {
return rpn.parse();
} else if let Ok(binop) = BinaryOperator::from_str(current_token.value.as_str()) {
if !self.operators.is_empty() {
if binop == BinaryOperator::Sub {
// We need to check if we have a "sub" and if the previous token is a word or
// number or string, then we need to treat it as a negative number.
// This oddity only applies to the "-" operator.
if let Some(prevtoken) = self.previous_postfix.last() {
if prevtoken.token_type == TokenType::Operator {
// Get the next token and see if it is a number.
if let Ok(nexttoken) = self.parser.get_token(1) {
if nexttoken.token_type == TokenType::Number {
// We have a negative number/ word or string.
// Change the value of the token to be the negative number/ word or string.
let mut new_token = nexttoken.clone();
new_token.value = format!("-{}", nexttoken.value);
let rpn = ReversePolishNotation::new(
&self.parser.tokens[2..],
&self
.previous_postfix
.iter()
.cloned()
.chain(vec![new_token.clone()])
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
}
}
}
if let Ok(prevbinop) = BinaryOperator::from_str(self.operators[self.operators.len() - 1].value.as_str())
{
if prevbinop.precedence() >= binop.precedence() {
@ -196,6 +245,29 @@ impl ReversePolishNotation {
return rpn.parse();
}
}
} else if self.previous_postfix.is_empty()
&& current_token.token_type == TokenType::Operator
&& current_token.value == "-"
{
if let Ok(nexttoken) = self.parser.get_token(1) {
if nexttoken.token_type == TokenType::Number {
// We have a negative number/ word or string.
// Change the value of the token to be the negative number/ word or string.
let mut new_token = nexttoken.clone();
new_token.value = format!("-{}", nexttoken.value);
let rpn = ReversePolishNotation::new(
&self.parser.tokens[2..],
&self
.previous_postfix
.iter()
.cloned()
.chain(vec![new_token.clone()])
.collect::<Vec<Token>>(),
&self.operators,
);
return rpn.parse();
}
}
}
let rpn = ReversePolishNotation::new(
@ -228,8 +300,8 @@ impl ReversePolishNotation {
.collect::<Vec<Token>>(),
);
return rpn.parse();
} else if current_token.value == ")" {
if !self.operators.is_empty() && self.operators[self.operators.len() - 1].value != "(" {
} else if current_token.value == ")" && !self.operators.is_empty() {
if self.operators[self.operators.len() - 1].value != "(" {
// pop operators off the stack and push them to postFix until we find the matching '('
let rpn = ReversePolishNotation::new(
&self.parser.tokens,
@ -299,7 +371,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a),
}))
}));
}
};
}
@ -338,7 +410,7 @@ impl ReversePolishNotation {
start_extended: None,
})));
return self.build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Word || current_token.token_type == TokenType::Keyword {
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens.len() > 1 {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "("
@ -350,6 +422,18 @@ impl ReversePolishNotation {
)));
return self.build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
}
if reverse_polish_notation_tokens[1].token_type == TokenType::Period
|| (reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "[")
{
let mut new_stack = stack;
let member_expression = self.parser.make_member_expression(0)?;
new_stack.push(MathExpression::MemberExpression(Box::new(member_expression.expression)));
return self.build_tree(
&reverse_polish_notation_tokens[member_expression.last_index + 1..],
new_stack,
);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
@ -396,7 +480,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}));
}
};
let paran = match &stack[stack.len() - 2] {
@ -445,7 +529,7 @@ impl ReversePolishNotation {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}));
}
};
let mut new_stack = stack[0..stack.len() - 2].to_vec();
@ -483,6 +567,10 @@ impl ReversePolishNotation {
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start),
MathExpression::MemberExpression(member_expression) => (
BinaryPart::MemberExpression(member_expression.clone()),
member_expression.start,
),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -513,6 +601,10 @@ impl ReversePolishNotation {
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end),
MathExpression::MemberExpression(member_expression) => (
BinaryPart::MemberExpression(member_expression.clone()),
member_expression.end,
),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
@ -521,13 +613,7 @@ impl ReversePolishNotation {
}
};
let right_end = match right.0.clone() {
BinaryPart::BinaryExpression(_bin_exp) => right.1,
BinaryPart::Literal(lit) => lit.end,
BinaryPart::Identifier(ident) => ident.end,
BinaryPart::CallExpression(call) => call.end,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
};
let right_end = right.0.clone().end();
let tree = BinaryExpression {
operator: BinaryOperator::from_str(&current_token.value.clone()).map_err(|err| {
@ -562,25 +648,13 @@ impl MathParser {
pub fn parse(&mut self) -> Result<BinaryExpression, KclError> {
let rpn = self.rpn.parse()?;
let tree_with_maybe_bad_top_level_start_end = self.rpn.build_tree(&rpn, vec![])?;
let left_start = match tree_with_maybe_bad_top_level_start_end.clone().left {
BinaryPart::BinaryExpression(bin_exp) => bin_exp.start,
BinaryPart::Literal(lit) => lit.start,
BinaryPart::Identifier(ident) => ident.start,
BinaryPart::CallExpression(call) => call.start,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.start,
};
let left_start = tree_with_maybe_bad_top_level_start_end.clone().left.start();
let min_start = if left_start < tree_with_maybe_bad_top_level_start_end.start {
left_start
} else {
tree_with_maybe_bad_top_level_start_end.start
};
let right_end = match tree_with_maybe_bad_top_level_start_end.clone().right {
BinaryPart::BinaryExpression(bin_exp) => bin_exp.end,
BinaryPart::Literal(lit) => lit.end,
BinaryPart::Identifier(ident) => ident.end,
BinaryPart::CallExpression(call) => call.end,
BinaryPart::UnaryExpression(unary_exp) => unary_exp.end,
};
let right_end = tree_with_maybe_bad_top_level_start_end.clone().right.end();
let max_end = if right_end > tree_with_maybe_bad_top_level_start_end.end {
right_end
} else {
@ -629,6 +703,60 @@ mod test {
);
}
#[test]
fn test_parse_expression_add_no_spaces() {
let tokens = crate::tokeniser::lexer("1+2");
let mut parser = MathParser::new(&tokens);
let result = parser.parse().unwrap();
assert_eq!(
result,
BinaryExpression {
operator: BinaryOperator::Add,
start: 0,
end: 3,
left: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(1)),
raw: "1".to_string(),
start: 0,
end: 1,
})),
right: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(2)),
raw: "2".to_string(),
start: 2,
end: 3,
})),
}
);
}
#[test]
fn test_parse_expression_sub_no_spaces() {
let tokens = crate::tokeniser::lexer("1 -2");
let mut parser = MathParser::new(&tokens);
let result = parser.parse().unwrap();
assert_eq!(
result,
BinaryExpression {
operator: BinaryOperator::Sub,
start: 0,
end: 4,
left: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(1)),
raw: "1".to_string(),
start: 0,
end: 1,
})),
right: BinaryPart::Literal(Box::new(Literal {
value: serde_json::Value::Number(serde_json::Number::from(2)),
raw: "2".to_string(),
start: 3,
end: 4,
})),
}
);
}
#[test]
fn test_parse_expression_plus_followed_by_star() {
let tokens = crate::tokeniser::lexer("1 + 2 * 3");

File diff suppressed because it is too large Load Diff

View File

@ -233,6 +233,7 @@ impl LanguageServer for Backend {
document_symbol_provider: Some(OneOf::Left(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Left(true)),
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
SemanticTokensRegistrationOptions {
text_document_registration_options: {
@ -552,19 +553,14 @@ impl LanguageServer for Backend {
return Ok(None);
};
// Now recast it.
// Make spaces for the tab size.
/*let mut tab_size = String::new();
for _ in 0..params.options.tab_size {
tab_size.push(' ');
}*/
// TODO: use the tab size.
let mut recast = ast.recast("", false).trim().to_string();
if let Some(insert_final_newline) = params.options.insert_final_newline {
if insert_final_newline {
recast.push('\n');
}
}
let recast = ast.recast(
&crate::abstract_syntax_tree_types::FormatOptions {
tab_size: params.options.tab_size as usize,
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
use_tabs: !params.options.insert_spaces,
},
0,
);
let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code);
Ok(Some(vec![TextEdit {
@ -572,6 +568,43 @@ impl LanguageServer for Backend {
range,
}]))
}
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
let filename = params.text_document_position.text_document.uri.to_string();
let Some(current_code) = self.current_code_map.get(&filename) else {
return Ok(None);
};
// Parse the ast.
// I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file.
let tokens = crate::tokeniser::lexer(&current_code);
let parser = crate::parser::Parser::new(tokens);
let Ok(mut ast) = parser.ast() else {
return Ok(None);
};
// Let's convert the position to a character index.
let pos = position_to_char_index(params.text_document_position.position, &current_code);
// Now let's perform the rename on the ast.
ast.rename_symbol(&params.new_name, pos);
// Now recast it.
let recast = ast.recast(&Default::default(), 0);
let source_range = SourceRange([0, current_code.len() - 1]);
let range = source_range.to_lsp_range(&current_code);
Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([(
params.text_document_position.text_document.uri,
vec![TextEdit {
new_text: recast,
range,
}],
)])),
document_changes: None,
change_annotations: None,
}))
}
}
/// Get completions from our stdlib.

View File

@ -0,0 +1,70 @@
//! Functions related to mathematics.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{errors::KclError, executor::MemoryItem, std::Args};
/// Computes the cosine of a number (in radians).
pub fn cos(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_cos(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the sine of a number (in radians).
#[stdlib {
name = "cos",
}]
fn inner_cos(num: f64) -> Result<f64, KclError> {
Ok(num.cos())
}
/// Computes the sine of a number (in radians).
pub fn sin(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_sin(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the sine of a number (in radians).
#[stdlib {
name = "sin",
}]
fn inner_sin(num: f64) -> Result<f64, KclError> {
Ok(num.sin())
}
/// Computes the tangent of a number (in radians).
pub fn tan(args: &mut Args) -> Result<MemoryItem, KclError> {
let num = args.get_number()?;
let result = inner_tan(num)?;
args.make_user_val_from_f64(result)
}
/// Computes the tangent of a number (in radians).
#[stdlib {
name = "tan",
}]
fn inner_tan(num: f64) -> Result<f64, KclError> {
Ok(num.tan())
}
/// Return the value of `pi`.
pub fn pi(args: &mut Args) -> Result<MemoryItem, KclError> {
let result = inner_pi()?;
args.make_user_val_from_f64(result)
}
/// Return the value of `pi`.
#[stdlib {
name = "pi",
}]
fn inner_pi() -> Result<f64, KclError> {
Ok(std::f64::consts::PI)
}

View File

@ -1,6 +1,7 @@
//! Functions implemented for language execution.
pub mod extrude;
pub mod math;
pub mod segment;
pub mod sketch;
pub mod utils;
@ -61,6 +62,10 @@ impl StdLib {
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),
Box::new(crate::std::math::Tan),
Box::new(crate::std::math::Pi),
];
let mut fns = HashMap::new();
@ -103,12 +108,12 @@ impl<'a> Args<'a> {
}
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::UserVal {
Ok(MemoryItem::UserVal(crate::executor::UserVal {
value: j,
meta: vec![Metadata {
source_range: self.source_range,
}],
})
}))
}
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
@ -122,6 +127,21 @@ impl<'a> Args<'a> {
)?))
}
fn get_number(&self) -> Result<f64, KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a number as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
parse_json_number_as_f64(&first_value, self.source_range)
}
fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
let mut numbers: Vec<f64> = Vec::new();
for arg in &self.args {
@ -471,7 +491,7 @@ pub fn leg_angle_x(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "legAngX",
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).acos() * 180.0 / std::f64::consts::PI
(leg.min(hypotenuse) / hypotenuse).acos().to_degrees()
}
/// Returns the angle of the given leg for y.
@ -486,7 +506,7 @@ pub fn leg_angle_y(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "legAngY",
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).asin() * 180.0 / std::f64::consts::PI
(leg.min(hypotenuse) / hypotenuse).asin().to_degrees()
}
/// The primitive types that can be used in a KCL file.
@ -591,7 +611,7 @@ mod tests {
buf.push_str(&fn_docs);
}
expectorate::assert_contents("../../../docs/kcl.md", &buf);
expectorate::assert_contents("../../../docs/kcl/std.md", &buf);
}
#[test]
@ -606,7 +626,7 @@ mod tests {
}
expectorate::assert_contents(
"../../../docs/kcl.json",
"../../../docs/kcl/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(),
);
}

View File

@ -230,7 +230,7 @@ fn inner_angle_to_match_length_x(
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
Ok(angle_r.to_degrees())
}
}
@ -285,6 +285,6 @@ fn inner_angle_to_match_length_y(
if diff > length {
Ok(0.0)
} else {
Ok(angle_r * 180.0 / std::f64::consts::PI)
Ok(angle_r.to_degrees())
}
}

View File

@ -161,34 +161,12 @@ pub enum LineData {
/// A point with a tag.
PointWithTag {
/// The to point.
to: PointOrDefault,
to: [f64; 2],
/// The tag.
tag: String,
},
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
/// A point or a default value.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum PointOrDefault {
/// A point.
Point([f64; 2]),
/// A string like `default`.
Default(String),
}
impl PointOrDefault {
fn get_point_with_default(&self, default: [f64; 2]) -> [f64; 2] {
match self {
PointOrDefault::Point(point) => *point,
PointOrDefault::Default(_) => default,
}
}
}
/// Draw a line.
@ -205,12 +183,9 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0];
let inner_args = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let to = [from.x + inner_args[0], from.y + inner_args[1]];
@ -283,10 +258,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([length, 0.0]),
tag,
},
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [length, 0.0], tag },
AxisLineData::Length(length) => LineData::Point([length, 0.0]),
};
@ -308,10 +280,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
}]
fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([0.0, length]),
tag,
},
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { to: [0.0, length], tag },
AxisLineData::Length(length) => LineData::Point([0.0, length]),
};
@ -360,8 +329,8 @@ fn inner_angled_line(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
from.y + length * f64::sin(angle * std::f64::consts::PI / 180.0),
from.x + length * f64::cos(angle.to_radians()),
from.y + length * f64::sin(angle.to_radians()),
];
let id = uuid::Uuid::new_v4();
@ -382,6 +351,20 @@ fn inner_angled_line(
},
};
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Line {
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
@ -413,10 +396,7 @@ fn inner_angled_line_of_x_length(
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
LineData::PointWithTag { to, tag }
} else {
LineData::Point(to)
},
@ -469,7 +449,7 @@ fn inner_angled_line_to_x(
};
let x_component = x_to - from.x;
let y_component = x_component * f64::tan(angle * std::f64::consts::PI / 180.0);
let y_component = x_component * f64::tan(angle.to_radians());
let y_to = from.y + y_component;
let new_sketch_group = inner_line_to(
@ -511,10 +491,7 @@ fn inner_angled_line_of_y_length(
let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag {
to: PointOrDefault::Point(to),
tag,
}
LineData::PointWithTag { to, tag }
} else {
LineData::Point(to)
},
@ -549,7 +526,7 @@ fn inner_angled_line_to_y(
};
let y_component = y_to - from.y;
let x_component = y_component / f64::tan(angle * std::f64::consts::PI / 180.0);
let x_component = y_component / f64::tan(angle.to_radians());
let x_to = from.x + x_component;
let new_sketch_group = inner_line_to(
@ -640,11 +617,9 @@ pub fn start_sketch_at(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "startSketchAt",
}]
fn inner_start_sketch_at(data: LineData, args: &mut Args) -> Result<SketchGroup, KclError> {
let default = [0.0, 0.0];
let to = match &data {
LineData::PointWithTag { to, .. } => to.get_point_with_default(default),
LineData::PointWithTag { to, .. } => *to,
LineData::Point(to) => *to,
LineData::Default(_) => default,
};
let id = uuid::Uuid::new_v4();
@ -978,16 +953,12 @@ mod tests {
use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
use crate::std::sketch::LineData;
#[test]
fn test_deserialize_line_data() {
let mut str_json = "\"default\"".to_string();
let data: LineData = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, LineData::Default("default".to_string()));
let data = LineData::Point([0.0, 1.0]);
str_json = serde_json::to_string(&data).unwrap();
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "[0.0,1.0]");
str_json = "[0, 1]".to_string();
@ -999,7 +970,7 @@ mod tests {
assert_eq!(
data,
LineData::PointWithTag {
to: PointOrDefault::Point([0.0, 1.0]),
to: [0.0, 1.0],
tag: "thing".to_string()
}
);

View File

@ -6,7 +6,7 @@ use crate::{
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let x = b[0] - a[0];
let y = b[1] - a[1];
normalise_angle(y.atan2(x) * 180.0 / std::f64::consts::PI)
normalise_angle(y.atan2(x).to_degrees())
}
pub fn normalise_angle(angle: f64) -> f64 {
@ -98,8 +98,8 @@ pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[0] + f64::cos(line2_angle.to_radians()) * 10.0,
line2_point[1] + f64::sin(line2_angle.to_radians()) * 10.0,
];
intersect(line1[0], line1[1], line2_point, line2_point_b)
}
@ -145,7 +145,7 @@ fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let y_component = x_component * f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
let y_component = x_component * f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
-1.0
} else {
@ -156,7 +156,7 @@ pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] {
pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360
let x_component = y_component / f64::tan(normalised_angle * std::f64::consts::PI / 180.0);
let x_component = y_component / f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
-1.0
} else {
@ -166,8 +166,8 @@ pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
}
pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f64, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle_deg * (std::f64::consts::PI / 180.0);
let end_angle = end_angle_deg * (std::f64::consts::PI / 180.0);
let start_angle = start_angle_deg.to_radians();
let end_angle = end_angle_deg.to_radians();
let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x),
@ -214,8 +214,8 @@ pub fn arc_angles(
let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x);
let start_angle_deg = start_angle * (180.0 / std::f64::consts::PI);
let end_angle_deg = end_angle * (180.0 / std::f64::consts::PI);
let start_angle_deg = start_angle.to_degrees();
let end_angle_deg = end_angle.to_degrees();
Ok((start_angle_deg, end_angle_deg))
}

View File

@ -34,6 +34,8 @@ pub enum TokenType {
Colon,
/// A period.
Period,
/// A double period: `..`.
DoublePeriod,
/// A line comment.
LineComment,
/// A block comment.
@ -54,7 +56,12 @@ impl TryFrom<TokenType> for SemanticTokenType {
TokenType::LineComment => Self::COMMENT,
TokenType::BlockComment => Self::COMMENT,
TokenType::Function => Self::FUNCTION,
TokenType::Whitespace | TokenType::Brace | TokenType::Comma | TokenType::Colon | TokenType::Period => {
TokenType::Whitespace
| TokenType::Brace
| TokenType::Comma
| TokenType::Colon
| TokenType::Period
| TokenType::DoublePeriod => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}
})
@ -130,12 +137,12 @@ impl From<&Token> for crate::executor::SourceRange {
}
lazy_static! {
static ref NUMBER: Regex = Regex::new(r"^-?\d+(\.\d+)?").unwrap();
static ref NUMBER: Regex = Regex::new(r"^(\d+(\.\d*)?|\.\d+)\b").unwrap();
static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
// TODO: these should be generated using our struct types for these.
static ref KEYWORD: Regex =
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|true|false|nil|and|or|not|var|const)\b").unwrap();
Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|mut|loop|true|false|nil|and|or|not|var|const)\b").unwrap();
static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
@ -147,6 +154,7 @@ lazy_static! {
static ref COMMA: Regex = Regex::new(r"^,").unwrap();
static ref COLON: Regex = Regex::new(r"^:").unwrap();
static ref PERIOD: Regex = Regex::new(r"^\.").unwrap();
static ref DOUBLE_PERIOD: Regex = Regex::new(r"^\.\.").unwrap();
static ref LINECOMMENT: Regex = Regex::new(r"^//.*").unwrap();
static ref BLOCKCOMMENT: Regex = Regex::new(r"^/\*[\s\S]*?\*/").unwrap();
}
@ -196,6 +204,9 @@ fn is_comma(character: &str) -> bool {
fn is_colon(character: &str) -> bool {
COLON.is_match(character)
}
fn is_double_period(character: &str) -> bool {
DOUBLE_PERIOD.is_match(character)
}
fn is_period(character: &str) -> bool {
PERIOD.is_match(character)
}
@ -206,8 +217,8 @@ fn is_block_comment(character: &str) -> bool {
BLOCKCOMMENT.is_match(character)
}
fn match_first(str: &str, regex: &Regex) -> Option<String> {
regex.find(str).map(|the_match| the_match.as_str().to_string())
fn match_first(s: &str, regex: &Regex) -> Option<String> {
regex.find(s).map(|the_match| the_match.as_str().to_string())
}
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
@ -219,8 +230,8 @@ fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
}
}
fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
let str_from_index = &str[start_index..];
fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
let str_from_index = &s.chars().skip(start_index).collect::<String>();
if is_string(str_from_index) {
return Some(make_token(
TokenType::String,
@ -296,13 +307,6 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_number(str_from_index) {
return Some(make_token(
TokenType::Number,
&match_first(str_from_index, &NUMBER)?,
start_index,
));
}
if is_operator(str_from_index) {
return Some(make_token(
TokenType::Operator,
@ -310,6 +314,13 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_number(str_from_index) {
return Some(make_token(
TokenType::Number,
&match_first(str_from_index, &NUMBER)?,
start_index,
));
}
if is_keyword(str_from_index) {
return Some(make_token(
TokenType::Keyword,
@ -331,6 +342,13 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
start_index,
));
}
if is_double_period(str_from_index) {
return Some(make_token(
TokenType::DoublePeriod,
&match_first(str_from_index, &DOUBLE_PERIOD)?,
start_index,
));
}
if is_period(str_from_index) {
return Some(make_token(
TokenType::Period,
@ -348,21 +366,22 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
None
}
pub fn lexer(str: &str) -> Vec<Token> {
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= str.len() {
return previous_tokens;
}
let token = return_token_at_index(str, current_index);
let Some(token) = token else {
return recursively_tokenise(str, current_index + 1, previous_tokens);
};
let mut new_tokens = previous_tokens;
let token_length = token.value.len();
new_tokens.push(token);
recursively_tokenise(str, current_index + token_length, new_tokens)
fn recursively_tokenise(s: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= s.len() {
return previous_tokens;
}
recursively_tokenise(str, 0, Vec::new())
let token = return_token_at_index(s, current_index);
let Some(token) = token else {
return recursively_tokenise(s, current_index + 1, previous_tokens);
};
let mut new_tokens = previous_tokens;
let token_length = token.value.len();
new_tokens.push(token);
recursively_tokenise(s, current_index + token_length, new_tokens)
}
pub fn lexer(s: &str) -> Vec<Token> {
recursively_tokenise(s, 0, Vec::new())
}
#[cfg(test)]
@ -375,19 +394,18 @@ mod tests {
fn is_number_test() {
assert!(is_number("1"));
assert!(is_number("1 abc"));
assert!(is_number("1abc"));
assert!(is_number("1.1"));
assert!(is_number("1.1 abc"));
assert!(!is_number("a"));
assert!(is_number("1"));
assert!(is_number(".1"));
assert!(is_number("5?"));
assert!(is_number("5 + 6"));
assert!(is_number("5 + a"));
assert!(is_number("-5"));
assert!(is_number("5.5"));
assert!(is_number("-5.5"));
assert!(!is_number("1abc"));
assert!(!is_number("a"));
assert!(!is_number("?"));
assert!(!is_number("?5"));

View File

@ -76,7 +76,8 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(json_str).map_err(JsError::from)?;
let result = program.recast("", false);
// Use the default options until we integrate into the UI the ability to change them.
let result = program.recast(&Default::default(), 0);
Ok(JsValue::from_serde(&result)?)
}

View File

@ -0,0 +1,98 @@
use anyhow::Result;
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
let ws = client
.modeling()
.commands_ws(None, None, None, None, Some(false))
.await?;
// Create a temporary file to write the output to.
let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
let tokens = kcl_lib::tokeniser::lexer(code);
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
let mut engine = kcl_lib::engine::EngineConnection::new(
ws,
std::env::temp_dir().display().to_string().as_str(),
output_file.display().to_string().as_str(),
)
.await?;
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)?;
// Send a snapshot request to the engine.
engine.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::executor::SourceRange::default(),
kittycad::types::ModelingCmd::TakeSnapshot {
format: kittycad::types::ImageFormat::Png,
},
)?;
// Wait for the snapshot to be taken.
engine.wait_for_snapshot().await;
// Read the output file.
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
Ok(actual)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let code = r#"fn box = (h, l, w) => {
const myBox = startSketchAt([0,0])
|> line([0, l], %)
|> line([w, 0], %)
|> line([0, -l], %)
|> close(%)
|> extrude(h, %)
return myBox
}
const fnBox = box(3, 6, 10)
show(fnBox)"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_angled_line() {
let code = r#"const part001 = startSketchAt([4.83, 12.56])
|> line([15.1, 2.48], %)
|> line({ to: [3.15, -9.85], tag: 'seg01' }, %)
|> line([-15.17, -4.1], %)
|> angledLine([segAng('seg01', %), 12.35], %)
|> line([-13.02, 10.03], %)
|> close(%)
|> extrude(4, %)
show(part001)"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1207,7 +1207,7 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0", "@codemirror/state@^6.2.1":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.1.tgz#6dc8d8e5abb26b875e3164191872d69a5e85bd73"
integrity sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==
@ -1530,10 +1530,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
"@kittycad/lib@^0.0.37":
version "0.0.37"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.37.tgz#ec4f6c4fb5d06402a19339f3374036b6582d2265"
integrity sha512-P8p9FeLV79/0Lfd0RioBta1drzhmpROnU4YV38+zsAA4LhibQCTjeekRkxVvHztGumPxz9pPsAeeLJyuz2RWKQ==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"
@ -1629,6 +1629,13 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
"@replit/codemirror-interact@^6.3.0":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@replit/codemirror-interact/-/codemirror-interact-6.3.0.tgz#977432c0b8f1a2995b93b1d5acaac27dbbb30c37"
integrity sha512-kB7ukZaZZkeKEiN5KLFOq9snxnFZRBjICLkKu5bqfPrPJXYDBmirzzpZE1dPX6qtNH5jTE3m3I1lJ+ltxk08fA==
dependencies:
"@codemirror/state" "^6.2.1"
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"