Compare commits

...

39 Commits

Author SHA1 Message Date
0add26cf61 Cut release v0.22.2 (#2685) 2024-06-17 17:44:30 -04:00
b54fc534c2 Patterning a pattern should always work (#2680)
* patterning a pattern should alwayus work

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

* add images;

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

* std lib

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

* bu,mp

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

* fix tests

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

* update lock

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

* bump

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

* fixes

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

* fixes

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

---------

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

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

* add shell

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 14:36:56 -05:00
bdeab4f87d Bump clap from 4.5.4 to 4.5.7 in /src/wasm-lib (#2643)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.4 to 4.5.7.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.4...v4.5.7)

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

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

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

* generate docs

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

* generate docs

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

---------

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

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

* Bump kittycad.ts lib

* Update cargo.lock again

* Bump lib again, and fix fillet typing

* Update kittycad.rs to v0.3.5

* Revert schemars to v0.8.17

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

This can happen because `src-tauri` is a separate Cargo project from
`src/wasm-lib`, but the former includes the latter as a dependency. So,
when wasm-lib updates a dep (e.g. bump databake from 1.7 to 1.8), the
former will, upon recompilation, pull in the newer databake dep. But
programmers in the wasm-lib repo don't usually work in the src-tauri repo
and so the src-tauri repo doesn't get updated.
2024-06-14 11:48:31 +02:00
efaae2b193 Bump bson from 2.10.0 to 2.11.0 in /src/wasm-lib (#2614)
Bumps [bson](https://github.com/mongodb/bson-rust) from 2.10.0 to 2.11.0.
- [Release notes](https://github.com/mongodb/bson-rust/releases)
- [Commits](https://github.com/mongodb/bson-rust/compare/v2.10.0...v2.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-13 15:42:47 -04:00
7e4ebacb72 Stale Cargo.lock, clippy 1.79 fixes (#2651)
* Update Cargo.lock in src-tauri, fix clippy

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

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

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

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

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

* ty clippy :)

* add in a lint test

* add in some docstrings

* whoops

* sigh

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

* uno reverse card

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

* wtf stop it robot fuck

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

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

This reverts commit 5b18f3c035.

* hurm

* try harder to type slower

* try harder? this all passes locally.

* try this now

* simplify, add debugging for trace

* fix enter use

* re-order again

* reorder a bit more

* enter

* ok fine no other enters?

* nerd

* wip

* move control of clearing to typescript

* move result out

* err check

* remove log

* remove clear

* remove add to diag

* THERE CAN BE ONLY ONE

* _err

* dedupe

* Revert "dedupe"

This reverts commit f66de88200.

* attempt to dedupe

* clear diagnostics on mock execute, too

* handle dupe diagnostics

* fmt

* dedupe tsc

* == vs ===

* fix dedupe

* return this to the wasm for now

* clear the map every go around

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

---------

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

* remove lame theme color outline from tooltips

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-06-11 07:49:30 -05:00
081e34a600 Bump regex from 1.10.4 to 1.10.5 in /src/wasm-lib (#2637)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.4 to 1.10.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.4...1.10.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 17:29:24 -07:00
541400f4be Cut release v0.22.1 (#2634) 2024-06-07 14:49:29 -04:00
39d249030d remove more page.clicks (#2630)
* remove more page.clicks

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

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

* Working context menu!

* Show keyboard shortcuts in file tree context menu

* Add context menu to Gizmo

* Little polish on components

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

* Updated Cargo lock

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

And use the mouse zoom for sketch mode

* test tweaks

* test tweak

---------

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

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

* Bring that beat back

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

* Add hyper for tests

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-06 13:05:01 -07:00
c74b695a71 Remove an orphaned grackle file (#2611) 2024-06-06 14:48:58 -05:00
d0c244e05e Do not aggressively disconnect when video stream goes down (#2621) 2024-06-06 11:40:39 -04:00
a315b77f02 More selection verification (#2619) 2024-06-06 11:55:22 +00:00
15c854ff18 verify sketches can be selected outside of sketches (#2618) 2024-06-06 08:07:42 +00:00
acd3a5717d improve selections and remove redundant edit_mode (#2617) 2024-06-06 16:03:10 +10:00
8a2555550f Adding a sample using a custom axis in revolve.rs (#2596)
* Adding a sample using a custom axis in revolve.rs

* Adding updated docs and snapshot of generated part

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

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

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

nice and clickable

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

* initial mouse position fix

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

* animation loop / disposal optimization

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

* reset camera tweak

* add cam target to debug panel

* test stub

* reset camera position handle removed from gizmo

it is now a button in the debug panel

* gizmo refactoring

* small fix

* reset camera view

bug fix

* nicer updateCameraToAxis

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

* micro refactoring

* playwright update

* playwright remove timeout + fmt

* hide gizmo while loading stream

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

This reverts commit f0a506d6b9.

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

This reverts commit 2781261331.

* try make gizmo test more realiable

* tweak

* refactoring

* increase timeout time

* 1 sec wait after mouse click

* 3 sec timeout

* better clickPosition

* test with 10 sec timeout

* 0.5 sec timeout

* add passive update for gizmo to avoid some edge cases

* default_camera_get_settings after click

* try and remove timeouts

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-06-05 12:43:12 +00:00
a5e7782d9a Re-enable rust cache for src-tauri on Windows (#2586) 2024-06-05 06:06:25 -04:00
79b0b70688 Bump ts-rs from badbac0 to be0349d in /src/wasm-lib (#2602) 2024-06-05 03:40:42 -05:00
1d134c1be0 Timeout ahead of flaky sign out (#2593) 2024-06-05 04:36:26 -04:00
1c58572234 cache playwright follow up (#2605)
cache plawwright follow up
2024-06-05 05:53:21 +00:00
ecee51e82b cache playwright browsers (#2604) 2024-06-05 05:10:49 +00:00
74 changed files with 12508 additions and 3898 deletions

View File

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

View File

@ -54,3 +54,8 @@ jobs:
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings cargo clippy --all --tests --benches -- -D warnings
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating
run: |
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"

View File

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

View File

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

323
docs/kcl/chamfer.md Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

311
docs/kcl/shell.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -50,3 +50,25 @@ export const TEST_SETTINGS_CORRUPTED = {
textWrapping: true, textWrapping: true,
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies Partial<SaveSettingsPayload>
export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|> startProfileAt([20, 0], %)
|> line([7.13, 4 + 0], %)
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> lineTo([20.14 + 0, -0.14 + 0], %)
|> xLineTo(29 + 0, %)
|> yLine(-3.14 + 0, %, 'a')
|> xLine(1.63, %)
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|> angledLineThatIntersects({
angle: 3.14,
intersectTag: 'a',
offset: 0
}, %)
|> tangentialArcTo([13.14 + 0, 13.14], %)
|> close(%)
|> extrude(5 + 7, %)
`

View File

@ -12,14 +12,16 @@ async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor() await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' }) await page
.getByTestId('loading')
.waitFor({ state: 'detached', timeout: 20_000 })
await page.getByTestId('start-sketch').waitFor() await page.getByTestId('start-sketch').waitFor()
} }
async function removeCurrentCode(page: Page) { async function removeCurrentCode(page: Page) {
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control' const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.click('.cm-content') await page.locator('.cm-content').click()
await page.keyboard.down(hotkey) await page.keyboard.down(hotkey)
await page.keyboard.press('a') await page.keyboard.press('a')
await page.keyboard.up(hotkey) await page.keyboard.up(hotkey)
@ -28,12 +30,12 @@ async function removeCurrentCode(page: Page) {
} }
async function sendCustomCmd(page: Page, cmd: EngineCommand) { async function sendCustomCmd(page: Page, cmd: EngineCommand) {
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd)) await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
await page.click('[data-testid="custom-cmd-send-button"]') await page.getByTestId('custom-cmd-send-button').click()
} }
async function clearCommandLogs(page: Page) { async function clearCommandLogs(page: Page) {
await page.click('[data-testid="clear-commands"]') await page.getByTestId('clear-commands').click()
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string) {
@ -162,12 +164,7 @@ export const getMovementUtils = (opts: any) => {
return ret.then(() => [last.x, last.y]) return ret.then(() => [last.x, last.y])
} }
const expectCodeToBe = async (str: string) => { return { toSU, click00r }
await expect(opts.page.locator('.cm-content')).toHaveText(str)
await opts.page.waitForTimeout(100)
}
return { toSU, click00r, expectCodeToBe }
} }
export async function getUtils(page: Page) { export async function getUtils(page: Page) {
@ -228,6 +225,7 @@ export async function getUtils(page: Page) {
.locator(locator) .locator(locator)
.boundingBox() .boundingBox()
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })), .then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'),
doAndWaitForCmd: async ( doAndWaitForCmd: async (
fn: () => Promise<void>, fn: () => Promise<void>,
commandType: string, commandType: string,

View File

@ -152,6 +152,7 @@ describe('ZMA (Tauri)', () => {
}) })
it('signs out', async () => { it('signs out', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
const menuButton = await $('[data-testid="user-sidebar-toggle"]') const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton) await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]') const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')

View File

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

1723
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -48,12 +48,14 @@ export type ReactCameraProperties =
type: 'perspective' type: 'perspective'
fov?: number fov?: number
position: [number, number, number] position: [number, number, number]
target: [number, number, number]
quaternion: [number, number, number, number] quaternion: [number, number, number, number]
} }
| { | {
type: 'orthographic' type: 'orthographic'
zoom?: number zoom?: number
position: [number, number, number] position: [number, number, number]
target: [number, number, number]
quaternion: [number, number, number, number] quaternion: [number, number, number, number]
} }
@ -442,7 +444,7 @@ export class CameraControls {
this.handleEnd() this.handleEnd()
return return
} }
this.throttledEngCmd({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'default_camera_zoom', type: 'default_camera_zoom',
@ -454,11 +456,11 @@ export class CameraControls {
return return
} }
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0 // Else "clientToEngine" (Sketch Mode) or forceUpdate
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad // From onMouseMove zoom handling which seems to be really smooth
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed) this.pendingZoom *= 1 + event.deltaY * 0.01
this.handleEnd() this.handleEnd()
} }
@ -773,6 +775,75 @@ export class CameraControls {
}) })
} }
async updateCameraToAxis(
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
): Promise<void> {
const distance = this.camera.position.distanceTo(this.target)
const vantage = this.target.clone()
let up = { x: 0, y: 0, z: 1 }
if (axis === 'x') {
vantage.x += distance
} else if (axis === 'y') {
vantage.y += distance
} else if (axis === 'z') {
vantage.z += distance
up = { x: -1, y: 0, z: 0 }
} else if (axis === '-x') {
vantage.x -= distance
} else if (axis === '-y') {
vantage.y -= distance
} else if (axis === '-z') {
vantage.z -= distance
up = { x: -1, y: 0, z: 0 }
}
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: this.target,
vantage: vantage,
up: up,
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}
async resetCameraPosition(): Promise<void> {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: this.target,
vantage: {
x: this.target.x,
y: this.target.y - 128,
z: this.target.z + 64,
},
up: { x: 0, y: 0, z: 1 },
},
})
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
},
})
}
async tweenCameraToQuaternion( async tweenCameraToQuaternion(
targetQuaternion: Quaternion, targetQuaternion: Quaternion,
targetPosition = new Vector3(), targetPosition = new Vector3(),
@ -957,6 +1028,11 @@ export class CameraControls {
roundOff(this.camera.position.y, 2), roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2), roundOff(this.camera.position.z, 2),
], ],
target: [
roundOff(this.target.x, 2),
roundOff(this.target.y, 2),
roundOff(this.target.z, 2),
],
quaternion: [ quaternion: [
roundOff(this.camera.quaternion.x, 2), roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2), roundOff(this.camera.quaternion.y, 2),

View File

@ -699,6 +699,15 @@ export const CamDebugSettings = () => {
} }
}} }}
/> />
<div>
<button
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset Camera Position
</button>
</div>
{camSettings.type === 'perspective' && ( {camSettings.type === 'perspective' && (
<input <input
type="range" type="range"
@ -816,6 +825,71 @@ export const CamDebugSettings = () => {
</li> </li>
</ul> </ul>
</div> </div>
<div>
target
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-target"
value={camSettings.target[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
parseFloat(e.target.value),
camSettings.target[1],
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-target"
value={camSettings.target[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
parseFloat(e.target.value),
camSettings.target[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-target"
value={camSettings.target[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.camControls.setCam({
...camSettings,
target: [
camSettings.target[0],
camSettings.target[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div> </div>
) )
} }

View File

@ -51,14 +51,6 @@ function CommandBarSelectionInput({
inputRef.current?.focus() inputRef.current?.focus()
}, [selection, inputRef]) }, [selection, inputRef])
// Exit engine's edit mode when this input step is active,
// and re-enter it when it's not.
// In future the engine's edit mode will go away and this will be handled differently.
useEffect(() => {
kclManager.exitEditMode()
return () => kclManager.defaultSelectionFilter()
}, [])
// Fast-forward through this arg if it's marked as skippable // Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already // and we have a valid selection already
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,30 +11,8 @@
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2)); --_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
--_p-block: 4px; --_p-block: 4px;
--_bg: var(--chalkboard-10); --_bg: var(--chalkboard-10);
--_shadow-alpha: 5%; --_shadow-alpha: 8%;
--_theme-alpha: 0.15; --_theme-alpha: 0.15;
--_theme-outline: drop-shadow(
0 1px 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
)
drop-shadow(
1px 0 0
oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
var(--_theme-alpha)
)
)
drop-shadow(
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
var(--primary-hue) / var(--_theme-alpha))
);
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
@ -61,16 +39,15 @@
background: var(--_bg); background: var(--_bg);
@apply text-chalkboard-110; @apply text-chalkboard-110;
will-change: filter; will-change: filter;
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha))) filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha))) drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
var(--_theme-outline);
} }
:global(.dark) .tooltip { :global(.dark) .tooltip {
--_bg: var(--chalkboard-110); --_bg: var(--chalkboard-90);
--_theme-alpha: 40%; --_theme-alpha: 40%;
--_shadow-alpha: 16%;
@apply text-chalkboard-10; @apply text-chalkboard-10;
filter: var(--_theme-outline);
} }
.tooltip:dir(rtl) { .tooltip:dir(rtl) {

View File

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

View File

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

View File

@ -89,9 +89,10 @@ export class KclManager {
return this._kclErrors return this._kclErrors
} }
set kclErrors(kclErrors) { set kclErrors(kclErrors) {
console.log('[lsp] not lsp, actually typescript: ', kclErrors)
this._kclErrors = kclErrors this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors) let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.setDiagnostics(diagnostics) editorManager.addDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors) this._kclErrorsCallBack(kclErrors)
} }
@ -185,6 +186,11 @@ export class KclManager {
const currentExecutionId = executionId || Date.now() const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
this.isExecuting = true this.isExecuting = true
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
@ -234,6 +240,7 @@ export class KclManager {
} = { updates: 'none' } } = { updates: 'none' }
) { ) {
await this.ensureWasmInit() await this.ensureWasmInit()
const newCode = recast(ast) const newCode = recast(ast)
const newAst = this.safeParse(newCode) const newAst = this.safeParse(newCode)
if (!newAst) return if (!newAst) return
@ -243,6 +250,11 @@ export class KclManager {
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
this._ast = { ...newAst } this._ast = { ...newAst }
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast: newAst, ast: newAst,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
@ -365,13 +377,6 @@ export class KclManager {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true) void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
} }
exitEditMode() {
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
}
defaultSelectionFilter() { defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager) defaultSelectionFilter(this.programMemory, this.engineCommandManager)
} }
@ -386,24 +391,11 @@ function defaultSelectionFilter(
) as SketchGroup | ExtrudeGroup ) as SketchGroup | ExtrudeGroup
firstSketchOrExtrudeGroup && firstSketchOrExtrudeGroup &&
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_batch_req', type: 'modeling_cmd_req',
batch_id: uuidv4(), cmd_id: uuidv4(),
responses: false, cmd: {
requests: [ type: 'set_selection_filter',
{ filter: ['face', 'edge', 'solid2d', 'curve'],
cmd_id: uuidv4(), },
cmd: {
type: 'edit_mode_enter',
target: firstSketchOrExtrudeGroup.id,
},
},
{
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'edge', 'solid2d'],
},
},
],
}) })
} }

View File

@ -147,7 +147,6 @@ export enum ConnectionError {
Unset = 0, Unset = 0,
LongLoadingTime, LongLoadingTime,
LostVideoStream,
ICENegotiate, ICENegotiate,
DataChannelError, DataChannelError,
WebSocketError, WebSocketError,
@ -168,8 +167,6 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
[ConnectionError.Unset]: '', [ConnectionError.Unset]: '',
[ConnectionError.LongLoadingTime]: [ConnectionError.LongLoadingTime]:
'Loading is taking longer than expected...', 'Loading is taking longer than expected...',
[ConnectionError.LostVideoStream]:
'Lost connection to video stream... Reconnecting...',
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.', [ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
[ConnectionError.DataChannelError]: 'The data channel signaled an error.', [ConnectionError.DataChannelError]: 'The data channel signaled an error.',
[ConnectionError.WebSocketError]: 'The websocket signaled an error.', [ConnectionError.WebSocketError]: 'The websocket signaled an error.',
@ -315,8 +312,6 @@ class EngineConnection extends EventTarget {
if (next.type === EngineConnectionStateType.Disconnecting) { if (next.type === EngineConnectionStateType.Disconnecting) {
const sub = next.value const sub = next.value
if (sub.type === DisconnectingType.Error) { if (sub.type === DisconnectingType.Error) {
console.log(sub)
// Record the last step we failed at. // Record the last step we failed at.
// (Check the current state that we're about to override that // (Check the current state that we're about to override that
// it was a Connecting state.) // it was a Connecting state.)
@ -759,8 +754,6 @@ class EngineConnection extends EventTarget {
// when assuming we're the only consumer or that all messages will // when assuming we're the only consumer or that all messages will
// be carefully formatted here. // be carefully formatted here.
console.log(event)
if (typeof event.data !== 'string') { if (typeof event.data !== 'string') {
return return
} }
@ -781,7 +774,6 @@ class EngineConnection extends EventTarget {
`Error in response to request ${message.request_id}:\n${errorsString} `Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}` failed cmd type was ${artifactThatFailed?.commandType}`
) )
console.log(artifactThatFailed)
} else { } else {
console.error(`Error from server:\n${errorsString}`) console.error(`Error from server:\n${errorsString}`)
} }
@ -872,7 +864,6 @@ class EngineConnection extends EventTarget {
this.pc this.pc
?.createOffer() ?.createOffer()
.then((offer: RTCSessionDescriptionInit) => { .then((offer: RTCSessionDescriptionInit) => {
console.log(offer)
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -944,7 +935,6 @@ class EngineConnection extends EventTarget {
case 'trickle_ice': case 'trickle_ice':
let candidate = resp.data?.candidate let candidate = resp.data?.candidate
console.log('trickle_ice: using this candidate: ', candidate)
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit) void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
break break
@ -1347,20 +1337,10 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.addEventListener( this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack, EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => { (({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
console.log('received track', mediaStream)
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
if (this.engineConnection) { console.error(
this.engineConnection.state = { 'video track mute: check webrtc internals -> inbound rtp'
type: EngineConnectionStateType.Disconnecting, )
value: {
type: DisconnectingType.Error,
value: {
error: ConnectionError.LostVideoStream,
},
},
}
}
}) })
setMediaStream(mediaStream) setMediaStream(mediaStream)
@ -1673,7 +1653,6 @@ export class EngineCommandManager extends EventTarget {
(command.cmd.type === 'highlight_set_entity' || (command.cmd.type === 'highlight_set_entity' ||
command.cmd.type === 'mouse_move' || command.cmd.type === 'mouse_move' ||
command.cmd.type === 'camera_drag_move' || command.cmd.type === 'camera_drag_move' ||
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any)) command.cmd.type === ('default_camera_perspective_settings' as any))
) )
) { ) {
@ -1688,7 +1667,6 @@ export class EngineCommandManager extends EventTarget {
command.type === 'modeling_cmd_req' && command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage command.cmd.type !== lastMessage
) { ) {
console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type lastMessage = command.cmd.type
} }
if (command.type === 'modeling_cmd_batch_req') { if (command.type === 'modeling_cmd_batch_req') {
@ -1702,7 +1680,6 @@ export class EngineCommandManager extends EventTarget {
if ( if (
(cmd.type === 'camera_drag_move' || (cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move' || cmd.type === 'handle_mouse_drag_move' ||
cmd.type === 'default_camera_look_at' ||
cmd.type === ('default_camera_perspective_settings' as any)) && cmd.type === ('default_camera_perspective_settings' as any)) &&
this.engineConnection?.unreliableDataChannel && this.engineConnection?.unreliableDataChannel &&
!forceWebsocket !forceWebsocket

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Outlet, useNavigate } from 'react-router-dom'
import Introduction from './Introduction' import Introduction from './Introduction'
import Camera from './Camera' import Camera from './Camera'
import Sketching from './Sketching' import Sketching from './Sketching'
import { useCallback } from 'react' import { useCallback, useEffect } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative' import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import Streaming from './Streaming' import Streaming from './Streaming'
@ -94,17 +94,31 @@ export function useNextClick(newStatus: string) {
export function useDismiss() { export function useDismiss() {
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { const {
settings: { send }, settings: { state, send },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const navigate = useNavigate() const navigate = useNavigate()
return useCallback(() => { const settingsCallback = useCallback(() => {
send({ send({
type: 'set.app.onboardingStatus', type: 'set.app.onboardingStatus',
data: { level: 'user', value: 'dismissed' }, data: { level: 'user', value: 'dismissed' },
}) })
navigate(filePath) }, [send])
}, [send, navigate, filePath])
/**
* A "listener" for the XState to return to "idle" state
* when the user dismisses the onboarding, using the callback above
*/
useEffect(() => {
if (
state.context.app.onboardingStatus.user === 'dismissed' &&
state.matches('idle')
) {
navigate(filePath)
}
}, [filePath, navigate, state])
return settingsCallback
} }
// Get the 1-indexed step number of the current onboarding step // Get the 1-indexed step number of the current onboarding step

View File

@ -297,9 +297,9 @@ dependencies = [
[[package]] [[package]]
name = "bson" name = "bson"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2" checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
dependencies = [ dependencies = [
"ahash", "ahash",
"base64 0.13.1", "base64 0.13.1",
@ -406,9 +406,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.4" version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -416,9 +416,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.2" version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -430,9 +430,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.4" version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "databake" name = "databake"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a" checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
dependencies = [ dependencies = [
"databake-derive", "databake-derive",
"proc-macro2", "proc-macro2",
@ -681,9 +681,9 @@ dependencies = [
[[package]] [[package]]
name = "databake-derive" name = "databake-derive"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43" checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1152,9 +1152,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.28" version = "0.14.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -1369,7 +1369,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.58" version = "0.1.60"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1436,9 +1436,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.3.3" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3" checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1451,7 +1451,7 @@ dependencies = [
"format_serde_error", "format_serde_error",
"futures", "futures",
"http 0.2.12", "http 0.2.12",
"itertools 0.12.1", "itertools 0.13.0",
"log", "log",
"mime_guess", "mime_guess",
"parse-display", "parse-display",
@ -2037,9 +2037,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.4" version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2378,9 +2378,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29" checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
@ -2395,9 +2395,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3" checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2945,9 +2945,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.23.0" version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0" checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@ -2975,9 +2975,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.13" version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -2996,9 +2996,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.13" version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.2.5",
"serde", "serde",
@ -3158,7 +3158,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "8.1.0" version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef" source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
dependencies = [ dependencies = [
"chrono", "chrono",
"thiserror", "thiserror",
@ -3170,7 +3170,7 @@ dependencies = [
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "8.1.0" version = "8.1.0"
source = "git+https://github.com/Aleph-Alpha/ts-rs#badbac08e61e65b312880aa64e9ece2976f1bbef" source = "git+https://github.com/Aleph-Alpha/ts-rs#be0349d5fb07a8ccab713887a61e90e3bc773c7a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3444,6 +3444,7 @@ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"futures", "futures",
"gloo-utils", "gloo-utils",
"hyper",
"image", "image",
"js-sys", "js-sys",
"kcl-lib", "kcl-lib",

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.1.58" version = "0.1.60"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -16,9 +16,9 @@ async-recursion = "1.1.1"
async-trait = "0.1.80" async-trait = "0.1.80"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.4", default-features = false, optional = true } clap = { version = "4.5.7", default-features = false, optional = true }
dashmap = "5.5.3" dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.18", path = "../derive-docs" } derive-docs = { version = "0.1.18", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
@ -35,7 +35,7 @@ serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.116" serde_json = "1.0.116"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.61"
toml = "0.8.13" toml = "0.8.14"
# TODO: change this to a cargo release once 8.1.1 comes out # TODO: change this to a cargo release once 8.1.1 comes out
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] } ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }
@ -54,9 +54,9 @@ web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5" approx = "0.5"
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] } bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
tokio = { version = "1.38.0", features = ["full"] } tokio = { version = "1.38.0", features = ["full"] }
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] } tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] } tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features] [features]

View File

@ -63,9 +63,10 @@ impl StdLibFnArg {
pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> { pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> {
if self.type_ == "SketchGroup" if self.type_ == "SketchGroup"
|| self.type_ == "ExtrudeGroup"
|| self.type_ == "SketchSurface"
|| self.type_ == "SketchGroupSet" || self.type_ == "SketchGroupSet"
|| self.type_ == "ExtrudeGroup"
|| self.type_ == "ExtrudeGroupSet"
|| self.type_ == "SketchSurface"
{ {
return Ok(Some((index, format!("${{{}:{}}}", index, "%")))); return Ok(Some((index, format!("${{{}:{}}}", index, "%"))));
} }

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::executor::SourceRange; use crate::{executor::SourceRange, lsp::IntoDiagnostic};
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)] #[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[ts(export)] #[ts(export)]
@ -42,19 +42,9 @@ pub struct KclErrorDetails {
} }
impl KclError { impl KclError {
/// Get the error message, line and column from the error and input code. /// Get the error message.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) { pub fn get_message(&self) -> String {
// Calculate the line and column of the error from the source range. format!("{}: {}", self.error_type(), self.message())
let (line, column) = if let Some(range) = self.source_ranges().first() {
let line = input[..range.0[0]].lines().count();
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
(Some(line), Some(column))
} else {
(None, None)
};
(format!("{}: {}", self.error_type(), self.message()), line, column)
} }
pub fn error_type(&self) -> &'static str { pub fn error_type(&self) -> &'static str {
@ -106,24 +96,6 @@ impl KclError {
} }
} }
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let (message, _, _) = self.get_message_line_column(code);
let source_ranges = self.source_ranges();
Diagnostic {
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self { pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone(); let mut new = self.clone();
match &mut new { match &mut new {
@ -163,6 +135,26 @@ impl KclError {
} }
} }
impl IntoDiagnostic for KclError {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let message = self.get_message();
let source_ranges = self.source_ranges();
Diagnostic {
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
severity: Some(DiagnosticSeverity::ERROR),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
}
/// This is different than to_string() in that it will serialize the Error /// This is different than to_string() in that it will serialize the Error
/// the struct as JSON so we can deserialize it on the js side. /// the struct as JSON so we can deserialize it on the js side.
impl From<KclError> for String { impl From<KclError> for String {

View File

@ -11,7 +11,7 @@ use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange}; use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
use crate::{ use crate::{
ast::types::{BodyItem, FunctionExpression, KclNone, Value}, ast::types::{BodyItem, FunctionExpression, KclNone, Program, Value},
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
fs::FileManager, fs::FileManager,
@ -975,6 +975,8 @@ impl Default for PipeInfo {
} }
/// The executor context. /// The executor context.
/// Cloning will return another handle to the same engine connection/session,
/// as this uses `Arc` under the hood.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ExecutorContext { pub struct ExecutorContext {
pub engine: Arc<Box<dyn EngineManager>>, pub engine: Arc<Box<dyn EngineManager>>,
@ -1310,6 +1312,43 @@ impl ExecutorContext {
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) { pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
self.settings.units = units; self.settings.units = units;
} }
/// Execute the program, then get a PNG screenshot.
pub async fn execute_and_prepare_snapshot(&self, program: Program) -> Result<kittycad::types::TakeSnapshot> {
let _ = self.run(program, None).await?;
// Zoom to fit.
self.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
crate::executor::SourceRange::default(),
kittycad::types::ModelingCmd::ZoomToFit {
object_ids: Default::default(),
padding: 0.1,
},
)
.await?;
// Send a snapshot request to the engine.
let resp = self
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
crate::executor::SourceRange::default(),
kittycad::types::ModelingCmd::TakeSnapshot {
format: kittycad::types::ImageFormat::Png,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
} = resp
else {
anyhow::bail!("Unexpected response from engine: {:?}", resp);
};
Ok(data)
}
} }
/// For each argument given, /// For each argument given,

View File

@ -11,6 +11,7 @@ pub mod engine;
pub mod errors; pub mod errors;
pub mod executor; pub mod executor;
pub mod fs; pub mod fs;
pub mod lint;
pub mod lsp; pub mod lsp;
pub mod parser; pub mod parser;
pub mod settings; pub mod settings;

View File

@ -0,0 +1,64 @@
use crate::ast::types;
/// The "Node" type wraps all the AST elements we're able to find in a KCL
/// file. Tokens we walk through will be one of these.
#[derive(Clone, Debug)]
pub enum Node<'a> {
Program(&'a types::Program),
ExpressionStatement(&'a types::ExpressionStatement),
VariableDeclaration(&'a types::VariableDeclaration),
ReturnStatement(&'a types::ReturnStatement),
VariableDeclarator(&'a types::VariableDeclarator),
Literal(&'a types::Literal),
Identifier(&'a types::Identifier),
BinaryExpression(&'a types::BinaryExpression),
FunctionExpression(&'a types::FunctionExpression),
CallExpression(&'a types::CallExpression),
PipeExpression(&'a types::PipeExpression),
PipeSubstitution(&'a types::PipeSubstitution),
ArrayExpression(&'a types::ArrayExpression),
ObjectExpression(&'a types::ObjectExpression),
MemberExpression(&'a types::MemberExpression),
UnaryExpression(&'a types::UnaryExpression),
Parameter(&'a types::Parameter),
ObjectProperty(&'a types::ObjectProperty),
MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier),
}
macro_rules! impl_from {
($node:ident, $t: ident) => {
impl<'a> From<&'a types::$t> for Node<'a> {
fn from(v: &'a types::$t) -> Self {
Node::$t(v)
}
}
};
}
impl_from!(Node, Program);
impl_from!(Node, ExpressionStatement);
impl_from!(Node, VariableDeclaration);
impl_from!(Node, ReturnStatement);
impl_from!(Node, VariableDeclarator);
impl_from!(Node, Literal);
impl_from!(Node, Identifier);
impl_from!(Node, BinaryExpression);
impl_from!(Node, FunctionExpression);
impl_from!(Node, CallExpression);
impl_from!(Node, PipeExpression);
impl_from!(Node, PipeSubstitution);
impl_from!(Node, ArrayExpression);
impl_from!(Node, ObjectExpression);
impl_from!(Node, MemberExpression);
impl_from!(Node, UnaryExpression);
impl_from!(Node, Parameter);
impl_from!(Node, ObjectProperty);
impl_from!(Node, MemberObject);
impl_from!(Node, LiteralIdentifier);

View File

@ -0,0 +1,233 @@
use super::Node;
use crate::ast::types::{
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
};
use anyhow::Result;
/// Walker is implemented by things that are able to walk an AST tree to
/// produce lints. This trait is implemented automatically for a few of the
/// common types, but can be manually implemented too.
pub trait Walker<'a> {
/// Walk will visit every element of the AST.
fn walk(&self, n: Node<'a>) -> Result<bool>;
}
impl<'a, FnT> Walker<'a> for FnT
where
FnT: Fn(Node<'a>) -> Result<bool>,
{
fn walk(&self, n: Node<'a>) -> Result<bool> {
self(n)
}
}
/// Run the Walker against all [Node]s in a [Program].
pub fn walk<'a, WalkT>(prog: &'a Program, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(prog.into())?;
for bi in &prog.body {
walk_body_item(bi, f)?;
}
Ok(())
}
fn walk_variable_declarator<'a, WalkT>(node: &'a VariableDeclarator, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
f.walk((&node.id).into())?;
walk_value(&node.init, f)?;
Ok(())
}
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
f.walk((&node.identifier).into())?;
Ok(())
}
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
Ok(())
}
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
Ok(())
}
fn walk_member_expression<'a, WalkT>(node: &'a MemberExpression, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
walk_member_object(&node.object, f)?;
walk_literal_identifier(&node.property, f)?;
Ok(())
}
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
match node {
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into())?,
BinaryPart::Identifier(id) => f.walk(id.as_ref().into())?,
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into())?,
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into())?,
BinaryPart::UnaryExpression(ue) => {
walk_unary_expression(ue, f)?;
true
}
BinaryPart::MemberExpression(me) => {
walk_member_expression(me, f)?;
true
}
};
Ok(())
}
fn walk_value<'a, WalkT>(node: &'a Value, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
match node {
Value::Literal(lit) => {
f.walk(lit.as_ref().into())?;
}
Value::Identifier(id) => {
// sometimes there's a bare Identifier without a Value::Identifier.
f.walk(id.as_ref().into())?;
}
Value::BinaryExpression(be) => {
f.walk(be.as_ref().into())?;
walk_binary_part(&be.left, f)?;
walk_binary_part(&be.right, f)?;
}
Value::FunctionExpression(fe) => {
f.walk(fe.as_ref().into())?;
for arg in &fe.params {
walk_parameter(arg, f)?;
}
walk(&fe.body, f)?;
}
Value::CallExpression(ce) => {
f.walk(ce.as_ref().into())?;
f.walk((&ce.callee).into())?;
for e in &ce.arguments {
walk_value::<WalkT>(e, f)?;
}
}
Value::PipeExpression(pe) => {
f.walk(pe.as_ref().into())?;
for e in &pe.body {
walk_value::<WalkT>(e, f)?;
}
}
Value::PipeSubstitution(ps) => {
f.walk(ps.as_ref().into())?;
}
Value::ArrayExpression(ae) => {
f.walk(ae.as_ref().into())?;
for e in &ae.elements {
walk_value::<WalkT>(e, f)?;
}
}
Value::ObjectExpression(oe) => {
walk_object_expression(oe, f)?;
}
Value::MemberExpression(me) => {
walk_member_expression(me, f)?;
}
Value::UnaryExpression(ue) => {
walk_unary_expression(ue, f)?;
}
_ => {
println!("{:?}", node);
unimplemented!()
}
}
Ok(())
}
/// Walk through an [ObjectProperty].
fn walk_object_property<'a, WalkT>(node: &'a ObjectProperty, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
walk_value(&node.value, f)?;
Ok(())
}
/// Walk through an [ObjectExpression].
fn walk_object_expression<'a, WalkT>(node: &'a ObjectExpression, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
for prop in &node.properties {
walk_object_property(prop, f)?;
}
Ok(())
}
/// walk through an [UnaryExpression].
fn walk_unary_expression<'a, WalkT>(node: &'a UnaryExpression, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
f.walk(node.into())?;
walk_binary_part(&node.argument, f)?;
Ok(())
}
/// walk through a [BodyItem].
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<()>
where
WalkT: Walker<'a>,
{
// We don't walk a BodyItem since it's an enum itself.
match node {
BodyItem::ExpressionStatement(xs) => {
f.walk(xs.into())?;
walk_value(&xs.expression, f)?;
}
BodyItem::VariableDeclaration(vd) => {
f.walk(vd.into())?;
for dec in &vd.declarations {
walk_variable_declarator(dec, f)?;
}
}
BodyItem::ReturnStatement(rs) => {
f.walk(rs.into())?;
walk_value(&rs.argument, f)?;
}
}
Ok(())
}

View File

@ -0,0 +1,131 @@
use crate::{
ast::types::VariableDeclarator,
executor::SourceRange,
lint::{
rule::{def_finding, Discovered, Finding},
Node,
},
};
use anyhow::Result;
def_finding!(
Z0001,
"Identifiers must be lowerCamelCase",
"\
By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
nor CammelCase. 🐪
For instance, a good identifier for the variable representing 'box height'
would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
more information there's a pretty good Wikipedia page at
https://en.wikipedia.org/wiki/Camel_case
"
);
fn lint_lower_camel_case(decl: &VariableDeclarator) -> Result<Vec<Discovered>> {
let mut findings = vec![];
let ident = &decl.id;
let name = &ident.name;
if !name.chars().next().unwrap().is_lowercase() {
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
return Ok(findings);
}
if name.contains('-') || name.contains('_') {
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
return Ok(findings);
}
Ok(findings)
}
pub fn lint_variables(decl: Node) -> Result<Vec<Discovered>> {
let Node::VariableDeclaration(decl) = decl else {
return Ok(vec![]);
};
Ok(decl
.declarations
.iter()
.flat_map(|v| lint_lower_camel_case(v).unwrap_or_default())
.collect())
}
#[cfg(test)]
mod tests {
use super::{lint_variables, Z0001};
use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
#[test]
fn z0001_const() {
assert_finding!(lint_variables, Z0001, "const Thickness = 0.5");
assert_finding!(lint_variables, Z0001, "const THICKNESS = 0.5");
assert_finding!(lint_variables, Z0001, "const THICC_NES = 0.5");
assert_finding!(lint_variables, Z0001, "const thicc_nes = 0.5");
}
test_finding!(z0001_full_bad, lint_variables, Z0001, "\
// Define constants
const pipeLength = 40
const pipeSmallDia = 10
const pipeLargeDia = 20
const thickness = 0.5
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
const Part001 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line([thickness, 0], %)
|> line([0, -1], %)
|> angledLineToX({
angle: 60,
to: pipeSmallDia + thickness
}, %)
|> line([0, -pipeLength], %)
|> angledLineToX({
angle: -60,
to: pipeLargeDia + thickness
}, %)
|> line([0, -1], %)
|> line([-thickness, 0], %)
|> line([0, 1], %)
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|> line([0, pipeLength], %)
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|> close(%)
|> revolve({ axis: 'y' }, %)
");
test_no_finding!(z0001_full_good, lint_variables, Z0001, "\
// Define constants
const pipeLength = 40
const pipeSmallDia = 10
const pipeLargeDia = 20
const thickness = 0.5
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
const part001 = startSketchOn('XY')
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|> line([thickness, 0], %)
|> line([0, -1], %)
|> angledLineToX({
angle: 60,
to: pipeSmallDia + thickness
}, %)
|> line([0, -pipeLength], %)
|> angledLineToX({
angle: -60,
to: pipeLargeDia + thickness
}, %)
|> line([0, -1], %)
|> line([-thickness, 0], %)
|> line([0, 1], %)
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|> line([0, pipeLength], %)
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|> close(%)
|> revolve({ axis: 'y' }, %)
");
}

View File

@ -0,0 +1,4 @@
mod camel_case;
#[allow(unused_imports)]
pub use camel_case::{lint_variables, Z0001};

View File

@ -0,0 +1,9 @@
mod ast_node;
mod ast_walk;
pub mod checks;
mod rule;
pub use ast_node::Node;
pub use ast_walk::walk;
// pub(crate) use rule::{def_finding, finding};
pub use rule::{lint, Discovered, Finding};

View File

@ -0,0 +1,180 @@
use super::{walk, Node};
use crate::{ast::types::Program, executor::SourceRange, lsp::IntoDiagnostic};
use anyhow::Result;
use std::sync::{Arc, Mutex};
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
/// Check the provided AST for any found rule violations.
///
/// The Rule trait is automatically implemented for a few other types,
/// but it can also be manually implemented as required.
pub trait Rule<'a> {
/// Check the AST at this specific node for any Finding(s).
fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
}
impl<'a, FnT> Rule<'a> for FnT
where
FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
{
fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
self(n)
}
}
/// Specific discovered lint rule Violation of a particular Finding.
#[derive(Clone, Debug)]
pub struct Discovered {
/// Zoo Lint Finding information.
pub finding: Finding,
/// Further information about the specific finding.
pub description: String,
/// Source code location.
pub pos: SourceRange,
/// Is this discovered issue overridden by the programmer?
pub overridden: bool,
}
impl IntoDiagnostic for Discovered {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
let message = self.finding.title.to_owned();
let source_range = self.pos;
Diagnostic {
range: source_range.to_lsp_range(code),
severity: Some(DiagnosticSeverity::INFORMATION),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("lint".to_string()),
message,
related_information: None,
tags: None,
data: None,
}
}
}
/// Abstract lint problem type.
#[derive(Clone, Debug, PartialEq)]
pub struct Finding {
/// Unique identifier for this particular issue.
pub code: &'static str,
/// Short one-line description of this issue.
pub title: &'static str,
/// Long human-readable description of this issue.
pub description: &'static str,
/// Is this discovered issue experimental?
pub experimental: bool,
}
impl Finding {
/// Create a new Discovered finding at the specific Position.
pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
Discovered {
description,
finding: self.clone(),
pos,
overridden: false,
}
}
}
macro_rules! def_finding {
( $code:ident, $title:expr, $description:expr ) => {
/// Generated Finding
pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
};
}
pub(crate) use def_finding;
macro_rules! finding {
( $code:ident, $title:expr, $description:expr ) => {
$crate::lint::rule::Finding {
code: stringify!($code),
title: $title,
description: $description,
experimental: false,
}
};
}
pub(crate) use finding;
#[cfg(test)]
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
/// Check the provided Program for any Findings.
pub fn lint<'a, RuleT>(prog: &'a Program, rule: RuleT) -> Result<Vec<Discovered>>
where
RuleT: Rule<'a>,
{
let v = Arc::new(Mutex::new(vec![]));
walk(prog, &|node: Node<'a>| {
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
findings.append(&mut rule.check(node)?);
Ok(true)
})?;
let x = v.lock().unwrap();
Ok(x.clone())
}
#[cfg(test)]
mod test {
macro_rules! assert_no_finding {
( $check:expr, $finding:expr, $kcl:expr ) => {
let tokens = $crate::token::lexer($kcl).unwrap();
let parser = $crate::parser::Parser::new(tokens);
let prog = parser.ast().unwrap();
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
if discovered_finding.finding == $finding {
assert!(false, "Finding {:?} was emitted", $finding.code);
}
}
};
}
macro_rules! assert_finding {
( $check:expr, $finding:expr, $kcl:expr ) => {
let tokens = $crate::token::lexer($kcl).unwrap();
let parser = $crate::parser::Parser::new(tokens);
let prog = parser.ast().unwrap();
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
if discovered_finding.finding == $finding {
return;
}
}
assert!(false, "Finding {:?} was not emitted", $finding.code);
};
}
macro_rules! test_finding {
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
#[test]
fn $name() {
$crate::lint::rule::assert_finding!($check, $finding, $kcl);
}
};
}
macro_rules! test_no_finding {
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
#[test]
fn $name() {
$crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
}
};
}
pub(crate) use assert_finding;
pub(crate) use assert_no_finding;
pub(crate) use test_finding;
pub(crate) use test_no_finding;
}

View File

@ -36,9 +36,9 @@ use tower_lsp::{
use super::backend::{InnerHandle, UpdateHandle}; use super::backend::{InnerHandle, UpdateHandle};
use crate::{ use crate::{
ast::types::VariableKind, ast::types::VariableKind,
errors::KclError,
executor::SourceRange, executor::SourceRange,
lsp::{backend::Backend as _, safemap::SafeMap}, lint::{checks, lint},
lsp::{backend::Backend as _, safemap::SafeMap, util::IntoDiagnostic},
parser::PIPE_OPERATOR, parser::PIPE_OPERATOR,
}; };
@ -166,6 +166,7 @@ impl crate::lsp::backend::Backend for Backend {
} }
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) { async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
self.clear_diagnostics_map(&params.uri).await;
// We already updated the code map in the shared backend. // We already updated the code map in the shared backend.
// Lets update the tokens. // Lets update the tokens.
@ -251,14 +252,14 @@ impl crate::lsp::backend::Backend for Backend {
// Execute the code if we have an executor context. // Execute the code if we have an executor context.
// This function automatically executes if we should & updates the diagnostics if we got // This function automatically executes if we should & updates the diagnostics if we got
// errors. // errors.
let result = self.execute(&params, ast).await; if self.execute(&params, ast.clone()).await.is_err() {
if result.is_err() { // if there was an issue, let's bail and avoid trying to lint.
// We return early because we got errors, and we don't want to clear the diagnostics.
return; return;
} }
// Lets update the diagnostics, since we got no errors. for discovered_finding in lint(&ast, checks::lint_variables).into_iter().flatten() {
self.clear_diagnostics(&params.uri).await; self.add_to_diagnostics(&params, discovered_finding).await;
}
} }
} }
@ -356,30 +357,7 @@ impl Backend {
.await; .await;
} }
async fn add_to_diagnostics(&self, params: &TextDocumentItem, err: KclError) { async fn clear_diagnostics_map(&self, uri: &url::Url) {
let diagnostic = err.to_lsp_diagnostic(&params.text);
// We got errors, update the diagnostics.
self.diagnostics_map
.insert(
params.uri.to_string(),
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: vec![diagnostic.clone()],
},
}),
)
.await;
// Publish the diagnostic.
// If the client supports it.
self.client
.publish_diagnostics(params.uri.clone(), vec![diagnostic], None)
.await;
}
async fn clear_diagnostics(&self, uri: &url::Url) {
self.diagnostics_map self.diagnostics_map
.insert( .insert(
uri.to_string(), uri.to_string(),
@ -392,10 +370,43 @@ impl Backend {
}), }),
) )
.await; .await;
}
// Publish the diagnostic, we reset it here so the client knows the code compiles now. async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
// If the client supports it. &self,
self.client.publish_diagnostics(uri.clone(), vec![], None).await; params: &TextDocumentItem,
diagnostic: DiagT,
) {
self.client
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostic))
.await;
let diagnostic = diagnostic.to_lsp_diagnostic(&params.text);
let DocumentDiagnosticReport::Full(mut report) = self
.diagnostics_map
.get(params.uri.clone().as_str())
.await
.unwrap_or(DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: vec![],
},
}))
else {
unreachable!();
};
report.full_document_diagnostic_report.items.push(diagnostic);
self.diagnostics_map
.insert(params.uri.to_string(), DocumentDiagnosticReport::Full(report.clone()))
.await;
self.client
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
.await;
} }
async fn execute(&self, params: &TextDocumentItem, ast: crate::ast::types::Program) -> Result<()> { async fn execute(&self, params: &TextDocumentItem, ast: crate::ast::types::Program) -> Result<()> {

View File

@ -7,3 +7,5 @@ mod safemap;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod util; pub mod util;
pub use util::IntoDiagnostic;

View File

@ -1498,6 +1498,53 @@ async fn test_kcl_lsp_diagnostic_has_errors() {
} }
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_diagnostic_has_lints() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///testlint.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"let THING = 10"#.to_string(),
},
})
.await;
server.wait_on_handle().await;
// Send diagnostics request.
let diagnostics = server
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///testlint.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
identifier: None,
previous_result_id: None,
})
.await
.unwrap();
// Check the diagnostics.
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
assert_eq!(
diagnostics.full_document_diagnostic_report.items[0].message,
"Identifiers must be lowerCamelCase"
);
} else {
panic!("Expected full diagnostics");
}
} else {
panic!("Expected diagnostics");
}
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_copilot_lsp_set_editor_info() { async fn test_copilot_lsp_set_editor_info() {
let server = copilot_lsp_server().await.unwrap(); let server = copilot_lsp_server().await.unwrap();

View File

@ -1,7 +1,7 @@
//! Utility functions for working with ropes and positions. //! Utility functions for working with ropes and positions.
use ropey::Rope; use ropey::Rope;
use tower_lsp::lsp_types::Position; use tower_lsp::lsp_types::{Diagnostic, Position};
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> { pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize) Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
@ -31,3 +31,10 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
let line_start = offset - char_offset; let line_start = offset - char_offset;
Some(rope.slice(line_start..offset).to_string()) Some(rope.slice(line_start..offset).to_string())
} }
/// Convert an object into a [lsp_types::Diagnostic] given the
/// [TextDocumentItem]'s `.text` field.
pub trait IntoDiagnostic {
/// Convert the traited object to a [lsp_types::Diagnostic].
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
}

View File

@ -0,0 +1,126 @@
//! Standard library chamfers.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, MemoryItem},
std::Args,
};
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// Data for chamfers.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ChamferData {
/// The radius of the chamfer.
pub radius: f64,
/// The tags of the paths you want to chamfer.
pub tags: Vec<EdgeReference>,
}
/// A string or a uuid.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Ord, PartialOrd, Eq, Hash)]
#[ts(export)]
#[serde(untagged)]
pub enum EdgeReference {
/// A uuid of an edge.
Uuid(uuid::Uuid),
/// A tag name of an edge.
Tag(String),
}
/// Create chamfers on tagged paths.
pub async fn chamfer(args: Args) -> Result<MemoryItem, KclError> {
let (data, extrude_group): (ChamferData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let extrude_group = inner_chamfer(data, extrude_group, args).await?;
Ok(MemoryItem::ExtrudeGroup(extrude_group))
}
/// Create chamfers on tagged paths.
///
/// ```no_run
/// const width = 20
/// const length = 10
/// const thickness = 1
/// const chamferRadius = 2
///
/// const mountingPlateSketch = startSketchOn("XY")
/// |> startProfileAt([-width/2, -length/2], %)
/// |> lineTo([width/2, -length/2], %, 'edge1')
/// |> lineTo([width/2, length/2], %, 'edge2')
/// |> lineTo([-width/2, length/2], %, 'edge3')
/// |> close(%, 'edge4')
///
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
/// |> chamfer({
/// radius: chamferRadius,
/// tags: [
/// getNextAdjacentEdge('edge1', %),
/// getNextAdjacentEdge('edge2', %),
/// getNextAdjacentEdge('edge3', %),
/// getNextAdjacentEdge('edge4', %)
/// ],
/// }, %)
/// ```
#[stdlib {
name = "chamfer",
}]
async fn inner_chamfer(
data: ChamferData,
extrude_group: Box<ExtrudeGroup>,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Check if tags contains any duplicate values.
let mut tags = data.tags.clone();
tags.sort();
tags.dedup();
if tags.len() != data.tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
for tag in data.tags {
let edge_id = match tag {
EdgeReference::Uuid(uuid) => uuid,
EdgeReference::Tag(tag) => {
extrude_group
.sketch_group_values
.iter()
.find(|p| p.get_name() == tag)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found with tag: `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
.get_base()
.geo_meta
.id
}
};
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DFilletEdge {
edge_id,
object_id: extrude_group.id,
radius: data.radius,
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
cut_type: Some(kittycad::types::CutType::Chamfer),
},
)
.await?;
}
Ok(extrude_group)
}

View File

@ -117,6 +117,7 @@ async fn inner_fillet(
object_id: extrude_group.id, object_id: extrude_group.id,
radius: data.radius, radius: data.radius,
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future. tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
cut_type: Some(kittycad::types::CutType::Fillet),
}, },
) )
.await?; .await?;

View File

@ -275,7 +275,7 @@ pub async fn min(args: Args) -> Result<MemoryItem, KclError> {
tags = ["math"], tags = ["math"],
}] }]
fn inner_min(args: Vec<f64>) -> f64 { fn inner_min(args: Vec<f64>) -> f64 {
let mut min = std::f64::MAX; let mut min = f64::MAX;
for arg in args.iter() { for arg in args.iter() {
if *arg < min { if *arg < min {
min = *arg; min = *arg;
@ -312,7 +312,7 @@ pub async fn max(args: Args) -> Result<MemoryItem, KclError> {
tags = ["math"], tags = ["math"],
}] }]
fn inner_max(args: Vec<f64>) -> f64 { fn inner_max(args: Vec<f64>) -> f64 {
let mut max = std::f64::MIN; let mut max = f64::MIN;
for arg in args.iter() { for arg in args.iter() {
if *arg > max { if *arg > max {
max = *arg; max = *arg;

View File

@ -1,5 +1,6 @@
//! Functions implemented for language execution. //! Functions implemented for language execution.
pub mod chamfer;
pub mod extrude; pub mod extrude;
pub mod fillet; pub mod fillet;
pub mod helix; pub mod helix;
@ -10,6 +11,7 @@ pub mod patterns;
pub mod revolve; pub mod revolve;
pub mod segment; pub mod segment;
pub mod shapes; pub mod shapes;
pub mod shell;
pub mod sketch; pub mod sketch;
pub mod types; pub mod types;
pub mod utils; pub mod utils;
@ -29,7 +31,8 @@ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
ExecutorContext, ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SketchGroupSet, SketchSurface, SourceRange, ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet,
SketchSurface, SourceRange,
}, },
std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}, std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag},
}; };
@ -81,11 +84,13 @@ lazy_static! {
Box::new(crate::std::patterns::PatternLinear3D), Box::new(crate::std::patterns::PatternLinear3D),
Box::new(crate::std::patterns::PatternCircular2D), Box::new(crate::std::patterns::PatternCircular2D),
Box::new(crate::std::patterns::PatternCircular3D), Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::chamfer::Chamfer),
Box::new(crate::std::fillet::Fillet), Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge), Box::new(crate::std::fillet::GetOppositeEdge),
Box::new(crate::std::fillet::GetNextAdjacentEdge), Box::new(crate::std::fillet::GetNextAdjacentEdge),
Box::new(crate::std::fillet::GetPreviousAdjacentEdge), Box::new(crate::std::fillet::GetPreviousAdjacentEdge),
Box::new(crate::std::helix::Helix), Box::new(crate::std::helix::Helix),
Box::new(crate::std::shell::Shell),
Box::new(crate::std::revolve::Revolve), Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::revolve::GetEdge), Box::new(crate::std::revolve::GetEdge),
Box::new(crate::std::import::Import), Box::new(crate::std::import::Import),
@ -769,6 +774,52 @@ impl Args {
Ok((data, sketch_surface, tag)) Ok((data, sketch_surface, tag))
} }
fn get_data_and_extrude_group_set<T: serde::de::DeserializeOwned>(&self) -> Result<(T, ExtrudeGroupSet), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: T = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected an ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
})
})?;
let extrude_set = if let MemoryItem::ExtrudeGroup(eg) = second_value {
ExtrudeGroupSet::ExtrudeGroup(eg.clone())
} else if let MemoryItem::ExtrudeGroups { value } = second_value {
ExtrudeGroupSet::ExtrudeGroups(value.clone())
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a ExtrudeGroup or Vector of ExtrudeGroups as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
}));
};
Ok((data, extrude_set))
}
fn get_data_and_extrude_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<ExtrudeGroup>), KclError> { fn get_data_and_extrude_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<ExtrudeGroup>), KclError> {
let first_value = self let first_value = self
.args .args

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet}, executor::{ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet},
std::{types::Uint, Args}, std::{types::Uint, Args},
}; };
@ -141,7 +141,7 @@ async fn inner_pattern_linear_2d(
/// A linear pattern on a 3D model. /// A linear pattern on a 3D model.
pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> { pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
let (data, extrude_group): (LinearPattern3dData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?; let (data, extrude_group_set): (LinearPattern3dData, ExtrudeGroupSet) = args.get_data_and_extrude_group_set()?;
if data.axis == [0.0, 0.0, 0.0] { if data.axis == [0.0, 0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
@ -152,7 +152,7 @@ pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
})); }));
} }
let extrude_groups = inner_pattern_linear_3d(data, extrude_group, args).await?; let extrude_groups = inner_pattern_linear_3d(data, extrude_group_set, args).await?;
Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }) Ok(MemoryItem::ExtrudeGroups { value: extrude_groups })
} }
@ -178,26 +178,36 @@ pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
}] }]
async fn inner_pattern_linear_3d( async fn inner_pattern_linear_3d(
data: LinearPattern3dData, data: LinearPattern3dData,
extrude_group: Box<ExtrudeGroup>, extrude_group_set: ExtrudeGroupSet,
args: Args, args: Args,
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> { ) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
let starting_extrude_groups = match extrude_group_set {
ExtrudeGroupSet::ExtrudeGroup(extrude_group) => vec![extrude_group],
ExtrudeGroupSet::ExtrudeGroups(extrude_groups) => extrude_groups,
};
if args.ctx.is_mock { if args.ctx.is_mock {
return Ok(vec![extrude_group.clone()]); return Ok(starting_extrude_groups);
} }
let geometries = pattern_linear( let mut extrude_groups = Vec::new();
LinearPattern::ThreeD(data), for extrude_group in starting_extrude_groups.iter() {
Geometry::ExtrudeGroup(extrude_group), let geometries = pattern_linear(
args.clone(), LinearPattern::ThreeD(data.clone()),
) Geometry::ExtrudeGroup(extrude_group.clone()),
.await?; args.clone(),
)
.await?;
let Geometries::ExtrudeGroups(extrude_groups) = geometries else { let Geometries::ExtrudeGroups(new_extrude_groups) = geometries else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a vec of extrude groups".to_string(), message: "Expected a vec of extrude groups".to_string(),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
}; };
extrude_groups.extend(new_extrude_groups);
}
Ok(extrude_groups) Ok(extrude_groups)
} }
@ -335,9 +345,9 @@ impl CircularPattern {
/// A circular pattern on a 2D sketch. /// A circular pattern on a 2D sketch.
pub async fn pattern_circular_2d(args: Args) -> Result<MemoryItem, KclError> { pub async fn pattern_circular_2d(args: Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (CircularPattern2dData, Box<SketchGroup>) = args.get_data_and_sketch_group()?; let (data, sketch_group_set): (CircularPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?;
let sketch_groups = inner_pattern_circular_2d(data, sketch_group, args).await?; let sketch_groups = inner_pattern_circular_2d(data, sketch_group_set, args).await?;
Ok(MemoryItem::SketchGroups { value: sketch_groups }) Ok(MemoryItem::SketchGroups { value: sketch_groups })
} }
@ -364,35 +374,45 @@ pub async fn pattern_circular_2d(args: Args) -> Result<MemoryItem, KclError> {
}] }]
async fn inner_pattern_circular_2d( async fn inner_pattern_circular_2d(
data: CircularPattern2dData, data: CircularPattern2dData,
sketch_group: Box<SketchGroup>, sketch_group_set: SketchGroupSet,
args: Args, args: Args,
) -> Result<Vec<Box<SketchGroup>>, KclError> { ) -> Result<Vec<Box<SketchGroup>>, KclError> {
let starting_sketch_groups = match sketch_group_set {
SketchGroupSet::SketchGroup(sketch_group) => vec![sketch_group],
SketchGroupSet::SketchGroups(sketch_groups) => sketch_groups,
};
if args.ctx.is_mock { if args.ctx.is_mock {
return Ok(vec![sketch_group]); return Ok(starting_sketch_groups);
} }
let geometries = pattern_circular( let mut sketch_groups = Vec::new();
CircularPattern::TwoD(data), for sketch_group in starting_sketch_groups.iter() {
Geometry::SketchGroup(sketch_group), let geometries = pattern_circular(
args.clone(), CircularPattern::TwoD(data.clone()),
) Geometry::SketchGroup(sketch_group.clone()),
.await?; args.clone(),
)
.await?;
let Geometries::SketchGroups(sketch_groups) = geometries else { let Geometries::SketchGroups(new_sketch_groups) = geometries else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a vec of sketch groups".to_string(), message: "Expected a vec of sketch groups".to_string(),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
}; };
sketch_groups.extend(new_sketch_groups);
}
Ok(sketch_groups) Ok(sketch_groups)
} }
/// A circular pattern on a 3D model. /// A circular pattern on a 3D model.
pub async fn pattern_circular_3d(args: Args) -> Result<MemoryItem, KclError> { pub async fn pattern_circular_3d(args: Args) -> Result<MemoryItem, KclError> {
let (data, extrude_group): (CircularPattern3dData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?; let (data, extrude_group_set): (CircularPattern3dData, ExtrudeGroupSet) = args.get_data_and_extrude_group_set()?;
let extrude_groups = inner_pattern_circular_3d(data, extrude_group, args).await?; let extrude_groups = inner_pattern_circular_3d(data, extrude_group_set, args).await?;
Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }) Ok(MemoryItem::ExtrudeGroups { value: extrude_groups })
} }
@ -416,26 +436,36 @@ pub async fn pattern_circular_3d(args: Args) -> Result<MemoryItem, KclError> {
}] }]
async fn inner_pattern_circular_3d( async fn inner_pattern_circular_3d(
data: CircularPattern3dData, data: CircularPattern3dData,
extrude_group: Box<ExtrudeGroup>, extrude_group_set: ExtrudeGroupSet,
args: Args, args: Args,
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> { ) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
let starting_extrude_groups = match extrude_group_set {
ExtrudeGroupSet::ExtrudeGroup(extrude_group) => vec![extrude_group],
ExtrudeGroupSet::ExtrudeGroups(extrude_groups) => extrude_groups,
};
if args.ctx.is_mock { if args.ctx.is_mock {
return Ok(vec![extrude_group]); return Ok(starting_extrude_groups);
} }
let geometries = pattern_circular( let mut extrude_groups = Vec::new();
CircularPattern::ThreeD(data), for extrude_group in starting_extrude_groups.iter() {
Geometry::ExtrudeGroup(extrude_group), let geometries = pattern_circular(
args.clone(), CircularPattern::ThreeD(data.clone()),
) Geometry::ExtrudeGroup(extrude_group.clone()),
.await?; args.clone(),
)
.await?;
let Geometries::ExtrudeGroups(extrude_groups) = geometries else { let Geometries::ExtrudeGroups(new_extrude_groups) = geometries else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a vec of extrude groups".to_string(), message: "Expected a vec of extrude groups".to_string(),
source_ranges: vec![args.source_range], source_ranges: vec![args.source_range],
})); }));
}; };
extrude_groups.extend(new_extrude_groups);
}
Ok(extrude_groups) Ok(extrude_groups)
} }

View File

@ -200,6 +200,24 @@ pub async fn revolve(args: Args) -> Result<MemoryItem, KclError> {
/// axis: getOppositeEdge('revolveAxis', box) /// axis: getOppositeEdge('revolveAxis', box)
/// }, %) /// }, %)
/// ``` /// ```
///
/// ```no_run
/// const sketch001 = startSketchOn('XY')
/// |> startProfileAt([10, 0], %)
/// |> line([5, -5], %)
/// |> line([5, 5], %)
/// |> lineTo([profileStartX(%), profileStartY(%)], %)
/// |> close(%)
///
/// const part001 = revolve({
/// axis: {
/// custom: {
/// axis: [0.0, 1.0, 0.0],
/// origin: [0.0, 0.0, 0.0]
/// }
/// }
/// }, sketch001)
/// ```
#[stdlib { #[stdlib {
name = "revolve", name = "revolve",
}] }]

View File

@ -0,0 +1,140 @@
//! Standard library shells.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeSurface, MemoryItem},
std::{sketch::StartOrEnd, Args},
};
/// A tag for a face.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case", untagged)]
#[display("{0}")]
pub enum FaceTag {
StartOrEnd(StartOrEnd),
/// A string tag for the face you want to sketch on.
String(String),
}
/// Data for shells.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ShellData {
/// The thickness of the shell.
pub thickness: f64,
/// The faces you want removed.
pub faces: Vec<FaceTag>,
}
/// Create a shell.
pub async fn shell(args: Args) -> Result<MemoryItem, KclError> {
let (data, extrude_group): (ShellData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
let extrude_group = inner_shell(data, extrude_group, args).await?;
Ok(MemoryItem::ExtrudeGroup(extrude_group))
}
/// Shell a solid.
///
/// ```no_run
/// const firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
///
/// // Remove the end face for the extrusion.
/// shell({
/// faces: ['end'],
/// thickness: 0.25,
/// }, firstSketch)
/// ```
#[stdlib {
name = "shell",
}]
async fn inner_shell(
data: ShellData,
extrude_group: Box<ExtrudeGroup>,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
if data.faces.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one face".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut face_ids = Vec::new();
for tag in data.faces {
let extrude_plane_id = match tag {
FaceTag::String(ref s) => {
if s.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty tag for the face".to_string(),
source_ranges: vec![args.source_range],
}));
}
extrude_group
.value
.iter()
.find_map(|extrude_surface| match extrude_surface {
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
Some(extrude_plane.face_id)
}
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => Some(extrude_arc.face_id),
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
})
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range],
})
})?
}
FaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: "Expected a start face".to_string(),
source_ranges: vec![args.source_range],
})
})?,
FaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: "Expected an end face".to_string(),
source_ranges: vec![args.source_range],
})
})?,
};
face_ids.push(extrude_plane_id);
}
if face_ids.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one valid face".to_string(),
source_ranges: vec![args.source_range],
}));
}
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid3DShellFace {
face_ids,
object_id: extrude_group.id,
shell_thickness: data.thickness,
},
)
.await?;
Ok(extrude_group)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1880,10 +1880,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.64": "@kittycad/lib@^0.0.67":
version "0.0.64" version "0.0.67"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f" resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.67.tgz#b8edc66d83e41a79a7f238ba41bc27f0101935fb"
integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg== integrity sha512-Uy2fve75bgpnlPiIgKrnKAqiko+1hlTCPSIPky6mv7Hrnwn7FhWAeeesdyc1Xws9Ae18kNyA2po8udK6PjZPkA==
dependencies: dependencies:
node-fetch "3.3.2" node-fetch "3.3.2"
openapi-types "^12.0.0" openapi-types "^12.0.0"