Compare commits

...

36 Commits

Author SHA1 Message Date
cccedceea0 Bump to v0.3.0 (#378) 2023-09-04 11:01:51 -04:00
ed68a34560 disable high dpi video streaming (#374) 2023-09-01 20:29:03 -04:00
00ee913e3f Upload release artifacts to the release (on top of dl.kittycad.io) (#371)
* Upload release artifacts to the release (on top of dl.kittycad.io)
Fixes #365

* Remove test
2023-09-01 14:20:48 -04:00
46cc67e2db messing around with arc and bezier (#363)
updates



fixes



updates



add another test



updates



updates



updates



updates



updates



updates



add test for error;



updates



updates



fixups



updates



updates



fixes



updates



fixes



updates



fixes



updates



updates



updates



bump

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-31 22:19:23 -07:00
ff1be34f54 Revert mute-reset behavior (#369)
I have a hunch this is causing more problems than it fixes.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-31 22:57:58 -04:00
848bf61277 Don't fetch for user if in dev with a local engine (#368)
* Don't fetch for user if in dev with a local engine
But rather return a dummy user (created by @paultag) so that
teammates using locally-running engines can bypass auth.

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

* Use env var to be more explicit

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-08-31 16:23:12 -04:00
043333d3bc Franknoirot/fix onboarding units feedback followup (#367) 2023-08-31 16:08:15 -04:00
19d90b8081 bump kitty lib (#364) 2023-09-01 06:07:27 +10:00
4837c52908 Redo how Spans are used from the Engine (#359)
* Redo how Spans are used from the Engine

I don't like all the Sentry-specific stuff we've got to work around, and
I want to add a bunch more spans and more cleanly end the transaction.

This isn't generic enough to pull out of this code (yet?), but we
clearly need some class of abstraction due to the highly async pattern
in the WebRTC code.

I want to add in more tags, but there are a lot of events we need to
wait on. I'd like to hook into the <video> 'play' eventListener, but
it's hard to do from all the way down in the Engine.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-31 12:59:46 -04:00
afcf820bdd Franknoirot/fix onboarding units (#366)
* Fix up camera step copy and pane opacity for step

* Fix broken onboarding redirect with double slash

* Fix pane height for web bug from blur filter
I found a bug with browser behavior, at least on Chrome.
If you use `backdrop-filter: blur()` at all, you can't
have any children that overflow. The browser will ignore
any attempt and make those children max full-height.
This broke our side panels after I added blur, but
only in Chrome/browser target.

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

* Fix bug with unit system
Changing the unit system didn't also change the
base unit in the onboarding anymore. It needed
updated to use XState the same way as `/settings`

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

* Fix AppHeader item spacing when there's no toolbar

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-08-31 10:41:24 -04:00
18959510f8 Franknoirot/expandable toolbar (#343)
* Add basic Popover functionality

* Fix up light mode of basic bar

* Add support for 2D and 3D mode styling

* Turn toolbar buttons back on

* Remove ActionButton until after tool logic refactor

* Add transitions

* Add styles to always center toolbar in header
2023-08-31 09:47:59 -04:00
798cbe968a Franknoirot/live system theme (#358)
* Only show the Replay Onboarding button in file settings
Resolves #351. Eventually we will implement more sophisticated
logic for which settings should be shown where.

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

* Remove unnecessary console.log

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

* Respond to system theme changes in real-time
If the user has their "theme" setting to "system".
I tried to use the [XState invoked callback approach](https://xstate.js.org/docs/guides/communication.html#invoking-callbacks),
but I could not find any way to respond to the latest context/state values within the
media listener; I kept receiving stale state.

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

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-08-31 09:34:13 -04:00
9cbc088ba3 Only show the Replay Onboarding button in file settings (#355) 2023-08-31 08:27:05 -04:00
2693a5609b Add subtle transitions to sidebars (#344) 2023-08-31 08:17:52 -04:00
3507da7b39 Tweak text constrast, blinking cursor (#338) 2023-08-31 08:17:26 -04:00
56cfb6d1f0 remove excessive serialisation (#362)
remove excessive serialization
2023-08-31 14:44:22 +10:00
2b974ef1de Bump schemars from 0.8.12 to 0.8.13 in /src/wasm-lib (#341)
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.12 to 0.8.13.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.12...v0.8.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 21:06:40 -07:00
253f1992fd Bump tauri-build from 1.3.0 to 1.4.0 in /src-tauri (#282)
Bumps [tauri-build](https://github.com/tauri-apps/tauri) from 1.3.0 to 1.4.0.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v1.3...tauri-build-v1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 21:06:03 -07:00
76d3794b45 Bump bson from 2.6.1 to 2.7.0 in /src/wasm-lib (#360)
Bumps [bson](https://github.com/mongodb/bson-rust) from 2.6.1 to 2.7.0.
- [Release notes](https://github.com/mongodb/bson-rust/releases)
- [Commits](https://github.com/mongodb/bson-rust/compare/v2.6.1...v2.7.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>
2023-08-30 21:05:35 -07:00
e52c8c9db6 Bump kittycad from 0.2.22 to 0.2.23 in /src/wasm-lib (#361)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.2.22 to 0.2.23.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.2.22...v0.2.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 21:05:21 -07:00
eb48d51309 rename lossy to unreliable (#357)
* rename lossy to unreliable

* fmt

* missed a rename
2023-08-31 07:39:03 +10:00
f3274e03ff refactor callbacks (#334)
refactor callbacks
2023-08-30 15:19:37 -04:00
46937199a3 Start to clean up Sentry now that the app is back up again. (#356)
* Start to clean up Sentry now that the app is back up again.

Remove Sentry from local development I thought .env.development
was for dev.kc.io, not just local dev. Someone can add this to .local
if they need to test the Sentry stuff for now.

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-08-30 13:14:52 -04:00
e2a4798c2f Update production Sentry values (#354)
Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-08-30 10:52:26 -04:00
659e6d5b45 Add in Sentry, WebRTC Statistics (#345)
Collect WebRTC Statistical Information

Add in some instrumentation to track the duration of the setup phase,
and set up a job to make periodic use of the WebRTC Statistics API
to collect information about the connection (specifically, each track),
including transport-level information, timing information and bandwidth
information.

Sentry isn't the best place for that information, but it'll work until we
can work out a good long-term solution for it.

Signed-off-by: Paul Tagliamonte <paul@kittycad.io>
2023-08-30 10:34:14 -04:00
1fbd0ad675 Bump to v0.2.0 (#352) 2023-08-29 22:19:09 -04:00
743ea1af4d Kcl updates for snapshot stuff for cli (#350)
* updates for snapshot file

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 19:13:30 -07:00
2b1a556b81 Wrap await in try/catch to fix sign-in in tauri builds (#349)
Fixes #342
2023-08-29 21:45:40 -04:00
853389ba22 bump kittycad.rs (#348)
fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 16:31:19 -07:00
023af60781 fmt and move error stuff locally (#347)
* fmt

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

* some fixups for errors

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

* some fixups for errors

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

* bump version

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

* fix tsc

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 14:12:48 -07:00
18db6f2dc1 change name for initial publish to cargo (#340)
* change name for initial publish to cargo

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

* add license

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

* updates

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

* updates

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-29 12:35:45 -07:00
4afec15323 bugfix: don't show a toast when onboarding changes (#346) 2023-08-29 15:27:02 -04:00
152108f7a5 Refactor to just CommandBar and GlobalState (#337)
* Refactor to just CommandBar and GlobalState

* @Irev-Dev review: consolidate uses of useContext
2023-08-29 10:48:55 -04:00
32d928ae0c Franknoirot/cmd bar (#328)
* Add XState and naive ActionBar

* Add basic dialog and combobox

* Selectable commands in command bar

* Add a few (broken) file actions

* Home commands

* Add subcommand descriptions, cleanup on navigate

* Refactor: move command creation and types to lib

* Refactor to allow any machine to add commands

* Add auth to command bar, add ability to hide cmds

* Refactor: consolidate theme utilities

* Add settings as machine and command set

* Fix: type tweaks

* Fix: only allow auth to navigate from signin

* Remove zustand-powered settings

* Fix: remove zustand settings from App

* Fix: browser infinite redirect

* Feature: allow commands to be hidden per-platform

* Fix: tsc errors

* Fix: hide default project directory from cmd bar

* Polish: transitions, css tweaks

* Feature: label current value in options settings

* Fix broken debug panel UI

* Refactor: move settings toasts to actions

* Tweak: css rounding

* Fix: set default directory recursion and reload 🐞

* Refactor: move machines to their own directory

* Fix formatting

* @Irev-Dev clean-up catches, import cleanup
2023-08-29 10:31:49 +10:00
6f0fae625f quick fik (#339) 2023-08-29 10:28:27 +10:00
9bc47cf14a fix export and prepare for cli lib (#325)
* add a test

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

* fixes

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

* fix

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

* fixup and make cleaner cfg options

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

* updates

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

* fixes

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

* redo

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

* rearrange

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

* fixes

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

* updates

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

* updates

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

* bincode error

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

* updates

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

* working

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

* switch to bson

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

* remove all bincode

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

* fix clippy

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-28 14:58:24 -07:00
91 changed files with 6849 additions and 2380 deletions

View File

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

View File

@ -1,4 +1,7 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

@ -43,7 +43,5 @@ jobs:
- name: Run cargo build - name: Run cargo build
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo build --all --no-default-features --features noweb
cargo build --all --no-default-features --features web
cargo build --all cargo build --all
shell: bash shell: bash

View File

@ -45,4 +45,7 @@ jobs:
shell: bash shell: bash
run: |- run: |-
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast cargo test --all
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

View File

@ -124,6 +124,8 @@ jobs:
publish-apps-release: publish-apps-release:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
if: github.event_name == 'release' if: github.event_name == 'release'
permissions:
contents: write
needs: [build-test-web, build-apps] needs: [build-test-web, build-apps]
env: env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }} VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
@ -189,3 +191,8 @@ jobs:
with: with:
path: last_update.json path: last_update.json
destination: dl.kittycad.io/releases/modeling-app destination: dl.kittycad.io/releases/modeling-app
- name: Upload release files to Github
uses: softprops/action-gh-release@v1
with:
files: artifact/*/kittycad-modeling-app*

1
.gitignore vendored
View File

@ -25,5 +25,6 @@ yarn-error.log*
# rust # rust
src/wasm-lib/target src/wasm-lib/target
src/wasm-lib/bindings src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info src/wasm-lib/lcov.info

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,8 @@
* [`angledLineThatIntersects`](#angledLineThatIntersects) * [`angledLineThatIntersects`](#angledLineThatIntersects)
* [`startSketchAt`](#startSketchAt) * [`startSketchAt`](#startSketchAt)
* [`close`](#close) * [`close`](#close)
* [`arc`](#arc)
* [`bezierCurve`](#bezierCurve)
## Functions ## Functions
@ -3046,3 +3048,351 @@ close(sketch_group: SketchGroup) -> SketchGroup
### arc
Draw an arc.
```
arc(data: ArcData, sketch_group: SketchGroup) -> SketchGroup
```
#### Arguments
* `data`: `ArcData` - Data to draw an arc.
```
{
// The end angle.
"angle_end": number,
// The start angle.
"angle_start": number,
// The radius.
"radius": number,
// The tag.
"tag": string,
} |
{
// The end angle.
"angle_end": number,
// The start angle.
"angle_start": number,
// The radius.
"radius": number,
} |
{
// The center.
"center": [number],
// The radius.
"radius": number,
// The tag.
"tag": string,
// The to point.
"to": [number],
} |
{
// The center.
"center": [number],
// The radius.
"radius": number,
// The to point.
"to": [number],
}
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
#### Returns
* `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
### bezierCurve
Draw a bezier curve.
```
bezierCurve(data: BezierData, sketch_group: SketchGroup) -> SketchGroup
```
#### Arguments
* `data`: `BezierData` - Data to draw a bezier curve.
```
{
// The first control point.
"control1": [number],
// The second control point.
"control2": [number],
// The tag.
"tag": string,
// The to point.
"to": [number],
} |
{
// The first control point.
"control1": [number],
// The second control point.
"control2": [number],
// The to point.
"to": [number],
}
```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```
#### Returns
* `SketchGroup` - A sketch group is a collection of paths.
```
{
// The id of the sketch group.
"id": uuid,
// The position of the sketch group.
"position": [number],
// The rotation of the sketch group.
"rotation": [number],
// The starting path.
"start": {
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
},
// The paths in the sketch group.
"value": [{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
// The x coordinate.
"x": number,
// The y coordinate.
"y": number,
} |
{
// The from point.
"from": [number],
// The name of the path.
"name": string,
// The to point.
"to": [number],
"type": string,
}],
}
```

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.1.0", "version": "0.3.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.4.2",
@ -8,8 +8,10 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
"@kittycad/lib": "^0.0.34", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.35",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0", "@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0", "@testing-library/react": "^13.0.0",
@ -22,6 +24,7 @@
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"formik": "^2.4.3", "formik": "^2.4.3",
"fuse.js": "^6.6.2",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"re-resizable": "^6.9.9", "re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
@ -54,15 +57,15 @@
"build:both:local": "yarn build:wasm && vite build", "build:both:local": "yarn build:wasm && vite build",
"test": "vitest --mode development", "test": "vitest --mode development",
"test:nowatch": "vitest run --mode development", "test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)", "test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
"test:cov": "vitest run --coverage --mode development", "test:cov": "vitest run --coverage --mode development",
"simpleserver:ci": "http-server ./public --cors -p 3000 &", "simpleserver:ci": "http-server ./public --cors -p 3000 &",
"simpleserver": "http-server ./public --cors -p 3000", "simpleserver": "http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src", "fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src", "fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg --no-default-features --features web && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta", "build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src", "lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json" "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
}, },

77
src-tauri/Cargo.lock generated
View File

@ -648,6 +648,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.1" version = "0.3.1"
@ -1150,7 +1156,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap", "indexmap 1.9.3",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1163,6 +1169,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.3.3"
@ -1378,7 +1390,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde", "serde",
] ]
@ -1628,6 +1651,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.6.2" version = "0.6.2"
@ -2122,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.2",
"indexmap", "indexmap 1.9.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
"serde", "serde",
@ -2695,14 +2724,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "2.3.3" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.21.2",
"chrono", "chrono",
"hex", "hex",
"indexmap", "indexmap 1.9.3",
"indexmap 2.0.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@ -2711,9 +2741,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "2.3.3" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@ -3022,6 +3052,7 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"attohttpc", "attohttpc",
"base64 0.21.2",
"cocoa", "cocoa",
"dirs-next", "dirs-next",
"embed_plist", "embed_plist",
@ -3034,6 +3065,7 @@ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"http", "http",
"ignore", "ignore",
"minisign-verify",
"objc", "objc",
"once_cell", "once_cell",
"open", "open",
@ -3055,19 +3087,21 @@ dependencies = [
"tauri-utils", "tauri-utils",
"tempfile", "tempfile",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"url", "url",
"uuid", "uuid",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows 0.39.0", "windows 0.39.0",
"zip",
] ]
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389" checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -3078,7 +3112,6 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"winnow",
] ]
[[package]] [[package]]
@ -3176,12 +3209,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864" checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
dependencies = [ dependencies = [
"brotli", "brotli",
"ctor", "ctor",
"dunce",
"glob", "glob",
"heck 0.4.1", "heck 0.4.1",
"html5ever", "html5ever",
@ -3397,7 +3431,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
"nom8", "nom8",
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -3410,7 +3444,7 @@ version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime 0.6.2", "toml_datetime 0.6.2",
@ -4228,3 +4262,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
]

View File

@ -12,14 +12,14 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.3.0", features = [] } tauri-build = { version = "1.4.0", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
oauth2 = "4.4.1" oauth2 = "4.4.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.3.0", features = [ "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] } tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0" toml = "0.6.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "kittycad-modeling-app", "productName": "kittycad-modeling-app",
"version": "0.1.0" "version": "0.3.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'
import { App } from './App' import { App } from './App'
import { describe, test, vi } from 'vitest' import { describe, test, vi } from 'vitest'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './hooks/useAuthMachine' import { GlobalStateProvider } from './components/GlobalStateProvider'
import CommandBarProvider from 'components/CommandBar'
let listener: ((rect: any) => void) | undefined = undefined let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver { ;(global as any).ResizeObserver = class ResizeObserver {
@ -43,7 +44,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context // wrap in router and xState context
return ( return (
<BrowserRouter> <BrowserRouter>
<GlobalStateProvider>{children}</GlobalStateProvider> <CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
} }

View File

@ -18,7 +18,7 @@ import {
lineHighlightField, lineHighlightField,
addLineHighlight, addLineHighlight,
} from './editor/highlightextension' } from './editor/highlightextension'
import { PaneType, Selections, Themes, useStore } from './useStore' import { PaneType, Selections, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs' import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel' import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel' import { MemoryPanel } from './components/MemoryPanel'
@ -41,14 +41,15 @@ import {
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env' import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils' import { getNormalisedCoordinates } from './lib/utils'
import { getSystemTheme } from './lib/getSystemTheme' import { Themes, getSystemTheme } from './lib/theme'
import { isTauri } from './lib/isTauri' import { isTauri } from './lib/isTauri'
import { useLoaderData, useParams } from 'react-router-dom' import { useLoaderData, useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs' import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from './lib/tauriFS' import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { IndexLoaderData } from './Router' import { IndexLoaderData } from './Router'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useAuthMachine } from './hooks/useAuthMachine' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
export function App() { export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -83,11 +84,8 @@ export function App() {
cmdId, cmdId,
setCmdId, setCmdId,
formatCode, formatCode,
debugPanel,
theme,
openPanes, openPanes,
setOpenPanes, setOpenPanes,
onboardingStatus,
didDragInStream, didDragInStream,
setDidDragInStream, setDidDragInStream,
setStreamDimensions, setStreamDimensions,
@ -122,18 +120,23 @@ export function App() {
cmdId: s.cmdId, cmdId: s.cmdId,
setCmdId: s.setCmdId, setCmdId: s.setCmdId,
formatCode: s.formatCode, formatCode: s.formatCode,
debugPanel: s.debugPanel,
addKCLError: s.addKCLError, addKCLError: s.addKCLError,
theme: s.theme,
openPanes: s.openPanes, openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes, setOpenPanes: s.setOpenPanes,
onboardingStatus: s.onboardingStatus,
didDragInStream: s.didDragInStream, didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream, setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions, setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const [token] = useAuthMachine((s) => s?.context?.token)
const {
auth: {
context: { token },
},
settings: {
context: { showDebugPanel, theme, onboardingStatus },
},
} = useGlobalStateContext()
const editorTheme = theme === Themes.System ? getSystemTheme() : theme const editorTheme = theme === Themes.System ? getSystemTheme() : theme
@ -152,7 +155,7 @@ export function App() {
useHotkeys('shift + d', () => togglePane('debug')) useHotkeys('shift + d', () => togglePane('debug'))
const paneOpacity = const paneOpacity =
onboardingStatus === 'camera' onboardingStatus === onboardingPaths.CAMERA
? 'opacity-20' ? 'opacity-20'
: didDragInStream : didDragInStream
? 'opacity-40' ? 'opacity-40'
@ -250,9 +253,9 @@ export function App() {
const streamWidth = streamRef?.current?.offsetWidth const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth * pixelDensity : 0 const width = streamWidth ? streamWidth : 0
const quadWidth = Math.round(width / 4) * 4 const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight * pixelDensity : 0 const height = streamHeight ? streamHeight : 0
const quadHeight = Math.round(height / 4) * 4 const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => { useLayoutEffect(() => {
@ -276,6 +279,8 @@ export function App() {
useEffect(() => { useEffect(() => {
if (!isStreamReady) return if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => { const asyncWrap = async () => {
try { try {
if (!code) { if (!code) {
@ -286,11 +291,8 @@ export function App() {
setAst(_ast) setAst(_ast)
resetLogs() resetLogs()
resetKCLErrors() resetKCLErrors()
if (engineCommandManager) { engineCommandManager.endSession()
engineCommandManager.endSession() engineCommandManager.startNewSession()
engineCommandManager.startNewSession()
}
if (!engineCommandManager) return
const programMemory = await _executor( const programMemory = await _executor(
_ast, _ast,
{ {
@ -324,22 +326,29 @@ export function App() {
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
setArtifactMap({ artifactMap, sourceRangeMap }) setArtifactMap({ artifactMap, sourceRangeMap })
engineCommandManager.onHover((id) => { const unSubHover = engineCommandManager.subscribeToUnreliable({
if (!id) { event: 'highlight_set_entity',
setHighlightRange([0, 0]) callback: ({ data }) => {
} else { if (!data?.entity_id) {
const sourceRange = sourceRangeMap[id] setHighlightRange([0, 0])
setHighlightRange(sourceRange) } else {
} const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
}
},
}) })
engineCommandManager.onClick((selections) => { const unSubClick = engineCommandManager.subscribeTo({
if (!selections) { event: 'select_with_point',
setCursor2() callback: ({ data }) => {
return if (!data?.entity_id) {
} setCursor2()
const { id, type } = selections return
setCursor2({ range: sourceRangeMap[id], type }) }
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
}) })
unsubFn.push(unSubHover, unSubClick)
if (programMemory !== undefined) { if (programMemory !== undefined) {
setProgramMemory(programMemory) setProgramMemory(programMemory)
} }
@ -356,7 +365,10 @@ export function App() {
} }
} }
asyncWrap() asyncWrap()
}, [code, isStreamReady]) return () => {
unsubFn.forEach((fn) => fn())
}
}, [code, isStreamReady, engineCommandManager])
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message) engineCommandManager?.sendSceneCommand(message)
@ -510,7 +522,7 @@ export function App() {
</div> </div>
</Resizable> </Resizable>
<Stream className="absolute inset-0 z-0" /> <Stream className="absolute inset-0 z-0" />
{debugPanel && ( {showDebugPanel && (
<DebugPanel <DebugPanel
title="Debug" title="Debug"
className={ className={

View File

@ -1,9 +1,12 @@
import Loading from './components/Loading' import Loading from './components/Loading'
import { useAuthMachine } from './hooks/useAuthMachine' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
// Wrapper around protected routes, used in src/Router.tsx // Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => { export const Auth = ({ children }: React.PropsWithChildren) => {
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn')) const {
auth: { state },
} = useGlobalStateContext()
const isLoggedIn = state.matches('checkIfLoggedIn')
return isLoggedIn ? ( return isLoggedIn ? (
<Loading>Loading KittyCAD Modeling App...</Loading> <Loading>Loading KittyCAD Modeling App...</Loading>

View File

@ -3,8 +3,15 @@ import {
createBrowserRouter, createBrowserRouter,
Outlet, Outlet,
redirect, redirect,
useLocation,
RouterProvider, RouterProvider,
} from 'react-router-dom' } from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage' import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings' import { Settings } from './routes/Settings'
import Onboarding, { import Onboarding, {
@ -24,7 +31,47 @@ import {
} from './lib/tauriFS' } from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api' import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner' import DownloadAppBanner from './components/DownloadAppBanner'
import { GlobalStateProvider } from './hooks/useAuthMachine' import { GlobalStateProvider } from './components/GlobalStateProvider'
import {
SETTINGS_PERSIST_KEY,
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
dsn: VITE_KC_SENTRY_DSN,
// TODO(paultag): pass in the right env here.
// environment: "production",
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
// TODO: Add in kittycad.io endpoints
tracePropagationTargets: ['localhost'],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
const prependRoutes = const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => { (routesObject: Record<string, string>) => (prepend: string) => {
@ -68,7 +115,11 @@ const addGlobalContextToElements = (
'element' in route 'element' in route
? { ? {
...route, ...route,
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>, element: (
<CommandBarProvider>
<GlobalStateProvider>{route.element}</GlobalStateProvider>
</CommandBarProvider>
),
} }
: route : route
) )
@ -95,26 +146,25 @@ const router = createBrowserRouter(
request, request,
params, params,
}): Promise<IndexLoaderData | Response> => { }): Promise<IndexLoaderData | Response> => {
const store = localStorage.getItem('store') const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
if (store === null) { const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
return redirect(paths.ONBOARDING.INDEX) ContextFrom<typeof settingsMachine>
} else { >
const status = JSON.parse(store).state.onboardingStatus || ''
const notEnRouteToOnboarding =
!request.url.includes(paths.ONBOARDING.INDEX) &&
request.method === 'GET'
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
(status !== undefined && status.length === 0) ||
!(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) { const status = persistedSettings.onboardingStatus || ''
return redirect( const notEnRouteToOnboarding = !request.url.includes(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status paths.ONBOARDING.INDEX
) )
} // '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
status.length === 0 || !(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) {
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
)
} }
if (params.id && params.id !== 'new') { if (params.id && params.id !== 'new') {
@ -164,9 +214,23 @@ const router = createBrowserRouter(
if (!isTauri()) { if (!isTauri()) {
return redirect(paths.FILE + '/new') return redirect(paths.FILE + '/new')
} }
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const projectDir = await initializeProjectDirectory() const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
const projectsNoMeta = (await readDir(projectDir.dir)).filter( ContextFrom<typeof settingsMachine>
>
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
JSON.stringify({
...persistedSettings,
defaultDirectory: projectDir,
})
)
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory isProjectDirectory
) )
const projects = await Promise.all( const projects = await Promise.all(

60
src/Toolbar.module.css Normal file
View File

@ -0,0 +1,60 @@
.toolbarWrapper {
@apply relative;
}
.toolbar {
@apply flex gap-4 items-center rounded-full;
@apply border border-cool-20/30 bg-cool-10/50;
}
:global(.dark) .toolbar {
@apply border-cool-100/50 bg-cool-120/50;
}
:global(.sketch) .toolbar {
@apply border-fern-20/20 bg-fern-10/20;
}
:global(.dark .sketch) .toolbar {
@apply border-fern-120/50 bg-fern-100/30;
}
.toolbarCap {
@apply text-sm font-bold;
@apply bg-cool-20/50 text-cool-100;
}
:global(.dark) .toolbarCap {
@apply bg-cool-90/50 text-cool-30;
}
:global(.sketch) .toolbarCap {
@apply bg-fern-20/50 text-fern-100;
}
:global(.dark .sketch) .toolbarCap {
@apply bg-fern-90/50 text-fern-30;
}
.label {
@apply self-stretch flex items-center px-4 py-1;
@apply rounded-l-full;
}
.popoverToggle {
@apply self-stretch m-0 flex items-center px-4 py-1;
@apply rounded-r-full border-none;
@apply hover:bg-cool-20;
}
:global(.dark) .popoverToggle {
@apply hover:bg-cool-90;
}
:global(.sketch) .popoverToggle {
@apply hover:bg-fern-20;
}
:global(.dark .sketch) .popoverToggle {
@apply hover:bg-fern-90;
}

View File

@ -11,6 +11,11 @@ import { SetAngleLength } from './components/Toolbar/setAngleLength'
import { ConvertToVariable } from './components/Toolbar/ConvertVariable' import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance' import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween' import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
import { Fragment, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
import { Popover, Transition } from '@headlessui/react'
import styles from './Toolbar.module.css'
export const Toolbar = () => { export const Toolbar = () => {
const { const {
@ -29,72 +34,26 @@ export const Toolbar = () => {
programMemory: s.programMemory, programMemory: s.programMemory,
})) }))
return ( useEffect(() => {
<div> console.log('guiMode', guiMode)
{guiMode.mode === 'default' && ( }, [guiMode])
<button
onClick={() => { function ToolbarButtons() {
setGuiMode({ return (
mode: 'sketch', <>
sketchMode: 'selectFace', {guiMode.mode === 'default' && (
})
}}
>
Start Sketch
</button>
)}
{guiMode.mode === 'canEditExtrude' && (
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
programMemory
)
updateAst(modifiedAst)
}}
>
SketchOnFace
</button>
)}
{(guiMode.mode === 'canEditSketch' || false) && (
<button
onClick={() => {
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
pathToNode: guiMode.pathToNode,
rotation: guiMode.rotation,
position: guiMode.position,
})
}}
>
Edit Sketch
</button>
)}
{guiMode.mode === 'canEditSketch' && (
<>
<button <button
onClick={() => { onClick={() => {
if (!ast) return setGuiMode({
const pathToNode = getNodePathFromSourceRange( mode: 'sketch',
ast, sketchMode: 'selectFace',
selectionRanges.codeBasedSelections[0].range })
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}} }}
> >
ExtrudeSketch Start Sketch
</button> </button>
)}
{guiMode.mode === 'canEditExtrude' && (
<button <button
onClick={() => { onClick={() => {
if (!ast) return if (!ast) return
@ -102,77 +61,182 @@ export const Toolbar = () => {
ast, ast,
selectionRanges.codeBasedSelections[0].range selectionRanges.codeBasedSelections[0].range
) )
const { modifiedAst, pathToExtrudeArg } = extrudeSketch( const { modifiedAst } = sketchOnExtrudedFace(
ast, ast,
pathToNode, pathToNode,
false programMemory
) )
updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) updateAst(modifiedAst)
}} }}
> >
ExtrudeSketch (w/o pipe) SketchOnFace
</button> </button>
</> )}
)} {(guiMode.mode === 'canEditSketch' || false) && (
<button
{guiMode.mode === 'sketch' && ( onClick={() => {
<button onClick={() => setGuiMode({ mode: 'default' })}> setGuiMode({
Exit sketch mode: 'sketch',
</button> sketchMode: 'sketchEdit',
)} pathToNode: guiMode.pathToNode,
{toolTips rotation: guiMode.rotation,
.filter( position: guiMode.position,
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName) })
(sketchFnName) => ['line'].includes(sketchFnName) }}
) >
.map((sketchFnName) => { Edit Sketch
if ( </button>
guiMode.mode !== 'sketch' || )}
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit') {guiMode.mode === 'canEditSketch' && (
) <>
return null
return (
<button <button
key={sketchFnName} onClick={() => {
onClick={() => if (!ast) return
setGuiMode({ const pathToNode = getNodePathFromSourceRange(
...guiMode, ast,
...(guiMode.sketchMode === sketchFnName selectionRanges.codeBasedSelections[0].range
? { )
sketchMode: 'sketchEdit', const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion ast,
} pathToNode
: { )
sketchMode: sketchFnName, updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
isTooltip: true, }}
}),
})
}
> >
{sketchFnName} ExtrudeSketch
{guiMode.sketchMode === sketchFnName && '✅'}
</button> </button>
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode,
false
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
>
ExtrudeSketch (w/o pipe)
</button>
</>
)}
{guiMode.mode === 'sketch' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>
Exit sketch
</button>
)}
{toolTips
.filter(
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
(sketchFnName) => ['line'].includes(sketchFnName)
) )
})} .map((sketchFnName) => {
<br></br> if (
<ConvertToVariable /> guiMode.mode !== 'sketch' ||
<HorzVert horOrVert="horizontal" /> !('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
<HorzVert horOrVert="vertical" /> )
<EqualLength /> return null
<EqualAngle /> return (
<SetHorzVertDistance buttonType="alignEndsVertically" /> <button
<SetHorzVertDistance buttonType="setHorzDistance" /> key={sketchFnName}
<SetAbsDistance buttonType="snapToYAxis" /> onClick={() =>
<SetAbsDistance buttonType="xAbs" /> setGuiMode({
<SetHorzVertDistance buttonType="alignEndsHorizontally" /> ...guiMode,
<SetAbsDistance buttonType="snapToXAxis" /> ...(guiMode.sketchMode === sketchFnName
<SetHorzVertDistance buttonType="setVertDistance" /> ? {
<SetAbsDistance buttonType="yAbs" /> sketchMode: 'sketchEdit',
<SetAngleLength angleOrLength="setAngle" /> // todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
<SetAngleLength angleOrLength="setLength" /> }
<Intersect /> : {
<RemoveConstrainingValues /> sketchMode: sketchFnName,
<SetAngleBetween /> isTooltip: true,
</div> }),
})
}
>
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
</button>
)
})}
<ConvertToVariable />
<HorzVert horOrVert="horizontal" />
<HorzVert horOrVert="vertical" />
<EqualLength />
<EqualAngle />
<SetHorzVertDistance buttonType="alignEndsVertically" />
<SetHorzVertDistance buttonType="setHorzDistance" />
<SetAbsDistance buttonType="snapToYAxis" />
<SetAbsDistance buttonType="xAbs" />
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
<SetAbsDistance buttonType="snapToXAxis" />
<SetHorzVertDistance buttonType="setVertDistance" />
<SetAbsDistance buttonType="yAbs" />
<SetAngleLength angleOrLength="setAngle" />
<SetAngleLength angleOrLength="setLength" />
<Intersect />
<RemoveConstrainingValues />
<SetAngleBetween />
</>
)
}
return (
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
<div className={styles.toolbar}>
<span className={styles.toolbarCap + ' ' + styles.label}>
{guiMode.mode === 'sketch' ? '2D' : '3D'}
</span>
<menu className="flex flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
<ToolbarButtons />
</menu>
<Popover.Button
className={styles.toolbarCap + ' ' + styles.popoverToggle}
>
<FontAwesomeIcon icon={faSearch} />
</Popover.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
</Transition>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 translate-y-1 scale-95"
enterTo="opacity-100 translate-y-0 scale-100"
leave="transition ease-out duration-75"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-2"
>
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
<section className="flex justify-between items-center">
<p
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
>
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
</p>
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
</Popover.Button>
</section>
<section>
<ToolbarButtons />
</section>
</Popover.Panel>
</Transition>
</Popover>
) )
} }

View File

@ -8,11 +8,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const iconSizes = { const iconSizes = {
sm: 12, sm: 12,
md: 14.4, md: 14.4,
lg: 18, lg: 20,
xl: 28,
} }
export interface ActionIconProps extends React.PropsWithChildren { export interface ActionIconProps extends React.PropsWithChildren {
icon?: SolidIconDefinition | BrandIconDefinition icon?: SolidIconDefinition | BrandIconDefinition
className?: string
bgClassName?: string bgClassName?: string
iconClassName?: string iconClassName?: string
size?: keyof typeof iconSizes size?: keyof typeof iconSizes
@ -20,6 +22,7 @@ export interface ActionIconProps extends React.PropsWithChildren {
export const ActionIcon = ({ export const ActionIcon = ({
icon = faCircleExclamation, icon = faCircleExclamation,
className,
bgClassName, bgClassName,
iconClassName, iconClassName,
size = 'md', size = 'md',
@ -28,7 +31,9 @@ export const ActionIcon = ({
return ( return (
<div <div
className={ className={
'p-1 w-fit inline-grid place-content-center ' + `p-${
size === 'xl' ? '2' : '1'
} w-fit inline-grid place-content-center ${className} ` +
(bgClassName || (bgClassName ||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10') 'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
} }
@ -40,7 +45,7 @@ export const ActionIcon = ({
height={iconSizes[size]} height={iconSizes[size]}
className={ className={
iconClassName || iconClassName ||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100' 'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
} }
/> />
)} )}

View File

@ -0,0 +1,7 @@
/*
Some CSS cannot be represented
in Tailwind, such as complex grid layouts.
*/
.header {
grid-template-columns: 1fr auto 1fr;
}

View File

@ -2,7 +2,8 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router' import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useAuthMachine } from '../hooks/useAuthMachine' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -18,12 +19,18 @@ export const AppHeader = ({
className = '', className = '',
enableMenu = false, enableMenu = false,
}: AppHeaderProps) => { }: AppHeaderProps) => {
const [user] = useAuthMachine((s) => s?.context?.user) const {
auth: {
context: { user },
},
} = useGlobalStateContext()
return ( return (
<header <header
className={ className={
'overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' + (showToolbar ? 'grid ' : 'flex justify-between ') +
styles.header +
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
className className
} }
> >
@ -35,7 +42,11 @@ export const AppHeader = ({
</div> </div>
)} )}
{/* If there are children, show them, otherwise show User menu */} {/* If there are children, show them, otherwise show User menu */}
{children || <UserSidebarMenu user={user} />} {children || (
<div className="ml-auto">
<UserSidebarMenu user={user} />
</div>
)}
</header> </header>
) )
} }

View File

@ -1,10 +1,10 @@
.panel { .panel {
@apply relative overflow-auto z-0; @apply relative overflow-auto z-0;
@apply bg-chalkboard-20/40; @apply bg-chalkboard-10/70 backdrop-blur-sm;
} }
:global(.dark) .panel { :global(.dark) .panel {
@apply bg-chalkboard-110/50; @apply bg-chalkboard-110/50 backdrop-blur-0;
} }
.header { .header {

View File

@ -0,0 +1,290 @@
import { Combobox, Dialog, Transition } from '@headlessui/react'
import {
Dispatch,
Fragment,
SetStateAction,
createContext,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionIcon } from './ActionIcon'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { Command, SubCommand } from '../lib/commands'
import { useCommandsContext } from 'hooks/useCommandsContext'
export type SortedCommand = {
item: Partial<Command | SubCommand> & { name: string }
}
export const CommandsContext = createContext(
{} as {
commands: Command[]
addCommands: (commands: Command[]) => void
removeCommands: (commands: Command[]) => void
commandBarOpen: boolean
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
}
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commands, internalSetCommands] = useState([] as Command[])
const [commandBarOpen, setCommandBarOpen] = useState(false)
const addCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
}
const removeCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) =>
prevCommands.filter((command) => !newCommands.includes(command))
)
}
return (
<CommandsContext.Provider
value={{
commands,
addCommands,
removeCommands,
commandBarOpen,
setCommandBarOpen,
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
useHotkeys('meta+k', () => {
if (commands.length === 0) return
setCommandBarOpen(!commandBarOpen)
})
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
null
)
// keep track of the current subcommand index
const [subCommandIndex, setSubCommandIndex] = useState<number>()
const [subCommandData, setSubCommandData] = useState<{
[key: string]: string
}>({})
// if the subcommand index is null, we're not in a subcommand
const inSubCommand =
selectedCommand &&
'meta' in selectedCommand.item &&
selectedCommand.item.meta?.args !== undefined &&
subCommandIndex !== undefined
const currentSubCommand =
inSubCommand && 'meta' in selectedCommand.item
? selectedCommand.item.meta?.args[subCommandIndex]
: undefined
const [query, setQuery] = useState('')
const availableCommands =
inSubCommand && currentSubCommand
? currentSubCommand.type === 'string'
? query
? [{ name: query }]
: currentSubCommand.options
: currentSubCommand.options
: commands
const fuse = new Fuse(availableCommands || [], {
keys: ['name', 'description'],
})
const filteredCommands = query
? fuse.search(query)
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
function clearState() {
setQuery('')
setCommandBarOpen(false)
setSelectedCommand(null)
setSubCommandIndex(undefined)
setSubCommandData({})
}
function handleCommandSelection(entry: SortedCommand) {
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
setSelectedCommand(entry)
setSubCommandIndex(0)
setQuery('')
return
}
const { item } = entry
// If we have just selected a command with no subcommands, run it
const isCommandWithoutSubcommands =
'callback' in item && !('meta' in item && item.meta)
if (isCommandWithoutSubcommands) {
if (item.callback === undefined) return
item.callback()
setCommandBarOpen(false)
return
}
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (
selectedCommand &&
subCommandIndex !== undefined &&
'meta' in selectedCommand.item
) {
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
if (subCommand) {
const newSubCommandData = {
...subCommandData,
[subCommand.name]: item.name,
}
const newSubCommandIndex = subCommandIndex + 1
// If we have subcommands and have gathered all the data required
// from them, run the command with the gathered data
if (
selectedCommand.item.callback &&
selectedCommand.item.meta?.args.length === newSubCommandIndex
) {
selectedCommand.item.callback(newSubCommandData)
setCommandBarOpen(false)
} else {
// Otherwise, set the subcommand data and increment the subcommand index
setSubCommandData(newSubCommandData)
setSubCommandIndex(newSubCommandIndex)
setQuery('')
}
}
}
}
function getDisplayValue(command: Command) {
if (command.meta?.displayValue === undefined || !command.meta.args)
return command.name
return command.meta?.displayValue(
command.meta.args.map((c) =>
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
)
)
}
return (
<Transition.Root
show={
commandBarOpen &&
availableCommands?.length !== undefined &&
availableCommands.length > 0
}
as={Fragment}
afterLeave={() => clearState()}
>
<Dialog
onClose={() => {
setCommandBarOpen(false)
clearState()
}}
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
as={Fragment}
>
<Combobox
value={selectedCommand}
onChange={handleCommandSelection}
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
as="div"
>
<div className="flex gap-2 items-center">
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
<div>
{inSubCommand && (
<p className="text-liquid-70 dark:text-liquid-30">
{selectedCommand.item &&
getDisplayValue(selectedCommand.item as Command)}
</p>
)}
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
className="bg-transparent focus:outline-none w-full"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
setCommandBarOpen(false)
if (
inSubCommand &&
event.key === 'Backspace' &&
!event.currentTarget.value
) {
setSubCommandIndex(subCommandIndex - 1)
setSelectedCommand(null)
}
}}
displayValue={(command: SortedCommand) =>
command !== null ? command.item.name : ''
}
placeholder={
inSubCommand
? `Enter <${currentSubCommand?.name}>`
: 'Search for a command'
}
value={query}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
</div>
</div>
<Combobox.Options static className="max-h-96 overflow-y-auto">
{filteredCommands?.map((commandResult) => (
<Combobox.Option
key={commandResult.item.name}
value={commandResult}
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
>
<p>{commandResult.item.name}</p>
{(commandResult.item as SubCommand).description && (
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
{(commandResult.item as SubCommand).description}
</p>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
</Transition.Child>
</Dialog>
</Transition.Root>
)
}
export default CommandBarProvider

View File

@ -0,0 +1,158 @@
import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import {
authCommandBarMeta,
authMachine,
TOKEN_PERSIST_KEY,
} from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import {
SETTINGS_PERSIST_KEY,
settingsCommandBarMeta,
settingsMachine,
} from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import { setThemeClass, Themes } from 'lib/theme'
import {
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
type GlobalContext = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
export const GlobalStateContext = createContext({} as GlobalContext)
export const GlobalStateProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commands } = useCommandsContext()
// Settings machine setup
const retrievedSettings = useRef(
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
)
const persistedSettings = Object.assign(
settingsMachine.initialState.context,
JSON.parse(retrievedSettings.current) as Partial<
(typeof settingsMachine)['context']
>
)
const [settingsState, settingsSend] = useMachine(settingsMachine, {
context: persistedSettings,
actions: {
toastSuccess: (context, event) => {
const truncatedNewValue =
'data' in event && event.data instanceof Object
? (context[Object.keys(event.data)[0] as keyof typeof context]
.toString()
.substring(0, 28) as any)
: undefined
toast.success(
event.type +
(truncatedNewValue
? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : ''
}"`
: '')
)
},
},
})
useStateMachineCommands({
state: settingsState,
send: settingsSend,
commands,
owner: 'settings',
commandBarMeta: settingsCommandBarMeta,
})
// Listen for changes to the system theme and update the app theme accordingly
// This is only done if the theme setting is set to 'system'.
// It can't be done in XState (in an invoked callback, for example)
// because there doesn't seem to be a good way to listen to
// events outside of the machine that also depend on the machine's context
useEffect(() => {
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
}
matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener)
}, [settingsState.context])
// Auth machine setup
const [authState, authSend] = useMachine(authMachine, {
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) {
navigate(paths.INDEX)
}
},
},
})
useStateMachineCommands({
state: authState,
send: authSend,
commands,
commandBarMeta: authCommandBarMeta,
owner: 'auth',
})
return (
<GlobalStateContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
send: settingsSend,
},
}}
>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalStateProvider
export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, {
method: 'POST',
credentials: 'include',
})
}

View File

@ -1,7 +1,8 @@
import ReactJson from 'react-json-view' import ReactJson from 'react-json-view'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Themes, useStore } from '../useStore' import { useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
const ReactJsonTypeHack = ReactJson as any const ReactJsonTypeHack = ReactJson as any

View File

@ -1,8 +1,9 @@
import ReactJson from 'react-json-view' import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes, useStore } from '../useStore' import { useStore } from '../useStore'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor' import { ProgramMemory } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps { interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System> theme?: Exclude<Themes, Themes.System>

View File

@ -1,10 +1,11 @@
import { Popover } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons' import { faHome } from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata, paths } from '../Router' import { ProjectWithEntryPointMetadata, paths } from '../Router'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton' import { ExportButton } from './ExportButton'
import { Fragment } from 'react'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -34,7 +35,7 @@ const ProjectSidebarMenu = ({
) : ( ) : (
<Popover className="relative"> <Popover className="relative">
<Popover.Button <Popover.Button
className="border-0 px-1 pr-2 pl-0 flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-energy-50" className="border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
data-testid="project-sidebar-toggle" data-testid="project-sidebar-toggle"
> >
<img <img
@ -46,54 +47,77 @@ const ProjectSidebarMenu = ({
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'} {isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
</span> </span>
</Popover.Button> </Popover.Button>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> <Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
</Transition>
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 shadow-md rounded-r-lg overflow-hidden"> <Transition
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100"> enter="duration-100 ease-out"
<img enterFrom="opacity-0 -translate-x-1/4"
src="/kitt-8bit-winking.svg" enterTo="opacity-100 translate-x-0"
alt="KittyCAD App" leave="duration-75 ease-in"
className="h-9 w-auto" leaveFrom="opacity-100 translate-x-0"
/> leaveTo="opacity-0 -translate-x-4"
as={Fragment}
>
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="h-9 w-auto"
/>
<div> <div>
<p <p
className="m-0 text-energy-10 text-mono" className="m-0 text-energy-10 text-mono"
data-testid="projectName" data-testid="projectName"
> >
{project?.name ? project.name : 'KittyCAD Modeling App'} {project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypoint_metadata && (
<p className="m-0 text-energy-40 text-xs" data-testid="createdAt">
Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
</p> </p>
{project?.entrypoint_metadata && (
<p
className="m-0 text-energy-40 text-xs"
data-testid="createdAt"
>
Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="p-4 flex flex-col gap-2">
<ExportButton
className={{
button:
'border-transparent dark:border-transparent dark:hover:border-energy-60',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
}}
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
>
Go to Home
</ActionButton>
)} )}
</div> </div>
</div> </Popover.Panel>
<div className="p-4 flex flex-col gap-2"> </Transition>
<ExportButton
className={{
button:
'border-transparent dark:border-transparent dark:hover:border-energy-60',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
}}
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
>
Go to Home
</ActionButton>
)}
</div>
</Popover.Panel>
</Popover> </Popover>
) )
} }

View File

@ -2,7 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { GlobalStateProvider } from '../hooks/useAuthMachine' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
type User = Models['User_type'] type User = Models['User_type']
@ -94,7 +95,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context // wrap in router and xState context
return ( return (
<BrowserRouter> <BrowserRouter>
<GlobalStateProvider>{children}</GlobalStateProvider> <CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
} }

View File

@ -1,13 +1,13 @@
import { Popover } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useState } from 'react' import { Fragment, useState } from 'react'
import { paths } from '../Router' import { paths } from '../Router'
import makeUrlPathRelative from '../lib/makeUrlPathRelative' import makeUrlPathRelative from '../lib/makeUrlPathRelative'
import { useAuthMachine } from '../hooks/useAuthMachine'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type User = Models['User_type'] type User = Models['User_type']
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false) const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const [_, send] = useAuthMachine() const {
auth: { send },
} = useGlobalStateContext()
// Fallback logic for displaying user's "name": // Fallback logic for displaying user's "name":
// 1. user.name // 1. user.name
@ -59,82 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
Menu Menu
</ActionButton> </ActionButton>
)} )}
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> <Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
</Transition>
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg overflow-hidden"> <Transition
{({ close }) => ( enter="duration-100 ease-out"
<> enterFrom="opacity-0 translate-x-1/4"
{user && ( enterTo="opacity-100 translate-x-0"
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100"> leave="duration-75 ease-in"
{user.image && !imageLoadFailed && ( leaveFrom="opacity-100 translate-x-0"
<div className="rounded-full shadow-inner overflow-hidden"> leaveTo="opacity-0 translate-x-4"
<img as={Fragment}
src={user.image} >
alt={user.name || ''} <Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
className="h-8 w-8" {({ close }) => (
referrerPolicy="no-referrer" <>
onError={() => setImageLoadFailed(true)} {user && (
/> <div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
</div> {user.image && !imageLoadFailed && (
)} <div className="rounded-full shadow-inner overflow-hidden">
<img
<div> src={user.image}
<p alt={user.name || ''}
className="m-0 text-liquid-10 text-mono" className="h-8 w-8"
data-testid="username" referrerPolicy="no-referrer"
> onError={() => setImageLoadFailed(true)}
{displayedName || ''} />
</p> </div>
{displayedName !== user.email && (
<p
className="m-0 text-liquid-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)} )}
<div>
<p
className="m-0 text-liquid-10 text-mono"
data-testid="username"
>
{displayedName || ''}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-liquid-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)}
</div>
</div> </div>
)}
<div className="p-4 flex flex-col gap-2">
<ActionButton
Element="button"
icon={{ icon: faGear }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
navigate(makeUrlPathRelative(paths.SETTINGS))
}}
>
Settings
</ActionButton>
<ActionButton
Element="link"
to="https://github.com/KittyCAD/modeling-app/discussions"
icon={{ icon: faGithub }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
>
Request a feature
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('Log out')}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
>
Sign out
</ActionButton>
</div> </div>
)} </>
<div className="p-4 flex flex-col gap-2"> )}
<ActionButton </Popover.Panel>
Element="button" </Transition>
icon={{ icon: faGear }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
navigate(makeUrlPathRelative(paths.SETTINGS))
}}
>
Settings
</ActionButton>
<ActionButton
Element="link"
to="https://github.com/KittyCAD/modeling-app/discussions"
icon={{ icon: faGithub }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
>
Request a feature
</ActionButton>
<ActionButton
Element="button"
onClick={() => send('logout')}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
>
Sign out
</ActionButton>
</div>
</>
)}
</Popover.Panel>
</Popover> </Popover>
) )
} }

View File

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

View File

@ -1,54 +0,0 @@
import { createActorContext } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
import withBaseUrl from '../lib/withBaseURL'
export const AuthMachineContext = createActorContext(authMachine)
export const GlobalStateProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
return (
<AuthMachineContext.Provider
machine={() =>
authMachine.withConfig({
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => navigate(paths.INDEX),
},
})
}
>
{children}
</AuthMachineContext.Provider>
)
}
export function useAuthMachine<T>(
selector: (
state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0]
) => T = () => null as T
): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] {
// useActor api normally `[state, send] = useActor`
// we're only interested in send because of the selector
const send = AuthMachineContext.useActor()[1]
const selection = AuthMachineContext.useSelector(selector)
return [selection, send]
}
export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, {
method: 'POST',
credentials: 'include',
})
}

View File

@ -0,0 +1,6 @@
import { CommandsContext } from 'components/CommandBar'
import { useContext } from 'react'
export const useCommandsContext = () => {
return useContext(CommandsContext)
}

View File

@ -0,0 +1,6 @@
import { GlobalStateContext } from 'components/GlobalStateProvider'
import { useContext } from 'react'
export const useGlobalStateContext = () => {
return useContext(GlobalStateContext)
}

View File

@ -0,0 +1,42 @@
import { useEffect } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate'
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
import { useCommandsContext } from './useCommandsContext'
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
state: StateFrom<T>
send: Function
commandBarMeta?: CommandBarMeta
commands: Command[]
owner: string
}
export default function useStateMachineCommands<T extends AnyStateMachine>({
state,
send,
commandBarMeta,
owner,
}: UseStateMachineCommandsArgs<T>) {
const { addCommands, removeCommands } = useCommandsContext()
useEffect(() => {
const newCommands = state.nextEvents
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) =>
createMachineCommand<T>({
type,
state,
send,
commandBarMeta,
owner,
})
)
.filter((c) => c !== null) as Command[]
addCommands(newCommands)
return () => {
removeCommands(newCommands)
}
}, [state])
}

View File

@ -1,67 +0,0 @@
import { useEffect } from 'react'
import { useStore } from '../useStore'
import { parse } from 'toml'
import {
createDir,
BaseDirectory,
readDir,
readTextFile,
} from '@tauri-apps/api/fs'
export const useTauriBoot = () => {
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
defaultDir: s.defaultDir,
setDefaultDir: s.setDefaultDir,
setHomeMenuItems: s.setHomeMenuItems,
}))
useEffect(() => {
const isTauri = (window as any).__TAURI__
if (!isTauri) return
const run = async () => {
if (!defaultDir.base) {
createDir('puffin-projects/example', {
dir: BaseDirectory.Home,
recursive: true,
})
setDefaultDir({
base: BaseDirectory.Home,
dir: 'puffin-projects',
})
} else {
const directoryResult = await readDir(defaultDir.dir, {
dir: defaultDir.base,
recursive: true,
})
const puffinProjects = directoryResult.filter(
(file) =>
!file?.name?.startsWith('.') &&
file?.children?.find((child) => child?.name === 'wax.toml')
)
const tomlFiles = await Promise.all(
puffinProjects.map(async (file) => {
const parsedToml = parse(
await readTextFile(`${file.path}/wax.toml`, {
dir: defaultDir.base,
})
)
const mainPath = parsedToml?.package?.main
const projectName = parsedToml?.package?.name
return {
file,
mainPath,
projectName,
}
})
)
setHomeMenuItems(
tomlFiles.map(({ file, mainPath, projectName }) => ({
name: projectName,
path: mainPath ? `${file.path}/${mainPath}` : file.path,
}))
)
}
}
run()
}, [])
}

View File

@ -86,8 +86,18 @@ code {
@apply bg-transparent; @apply bg-transparent;
} }
#code-mirror-override .cm-activeLine,
#code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-10/50;
}
.dark #code-mirror-override .cm-activeLine,
.dark #code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-80/50;
}
#code-mirror-override .cm-gutters { #code-mirror-override .cm-gutters {
@apply bg-chalkboard-10/50; @apply bg-chalkboard-10/30;
} }
.dark #code-mirror-override .cm-gutters { .dark #code-mirror-override .cm-gutters {
@ -99,14 +109,24 @@ code {
} }
#code-mirror-override .cm-cursor { #code-mirror-override .cm-cursor {
display: block; display: block;
width: 200px; width: 1ch;
background: linear-gradient( @apply bg-liquid-40 mix-blend-multiply;
to right,
rgb(0, 55, 94) 0%, animation: blink 2s ease-out infinite;
#0084e2ff 2%, }
#0084e255 5%,
transparent 100% .dark #code-mirror-override .cm-cursor {
); @apply bg-liquid-50;
}
@keyframes blink {
0%,
100% {
opacity: 0;
}
15% {
opacity: 0.75;
}
} }
.react-json-view { .react-json-view {

View File

@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
import './index.css' import './index.css'
import reportWebVitals from './reportWebVitals' import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast' import { Toaster } from 'react-hot-toast'
import { Themes, useStore } from './useStore'
import { Router } from './Router' import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook' import { HotkeysProvider } from 'react-hotkeys-hook'
import { getSystemTheme } from './lib/getSystemTheme'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
function setThemeClass(state: Partial<{ theme: Themes }>) {
const systemTheme = state.theme === Themes.System && getSystemTheme()
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}
const { theme } = useStore.getState()
setThemeClass({ theme })
useStore.subscribe(setThemeClass)
root.render( root.render(
<HotkeysProvider> <HotkeysProvider>

View File

@ -3,7 +3,7 @@ import { parse_js } from '../wasm-lib/pkg/wasm_lib'
import { initPromise } from './rust' import { initPromise } from './rust'
import { Token } from './tokeniser' import { Token } from './tokeniser'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
export const rangeTypeFix = (ranges: number[][]): [number, number][] => export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
ranges.map(([start, end]) => [start, end]) ranges.map(([start, end]) => [start, end])
@ -16,10 +16,8 @@ export const parser_wasm = (code: string): Program => {
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg, parsed.msg,
parsed.kind === 'invalid_expression' rangeTypeFix(parsed.sourceRanges)
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
) )
console.log(kclError) console.log(kclError)
@ -36,10 +34,8 @@ export async function asyncParser(code: string): Promise<Program> {
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg, parsed.msg,
parsed.kind === 'invalid_expression' rangeTypeFix(parsed.sourceRanges)
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
) )
console.log(kclError) console.log(kclError)

View File

@ -1,20 +1,20 @@
export type { Program } from '../wasm-lib/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
export type { ObjectExpression } from '../wasm-lib/bindings/ObjectExpression' export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
export type { MemberExpression } from '../wasm-lib/bindings/MemberExpression' export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
export type { PipeExpression } from '../wasm-lib/bindings/PipeExpression' export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
export type { VariableDeclaration } from '../wasm-lib/bindings/VariableDeclaration' export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
export type { PipeSubstitution } from '../wasm-lib/bindings/PipeSubstitution' export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution'
export type { Identifier } from '../wasm-lib/bindings/Identifier' export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier'
export type { UnaryExpression } from '../wasm-lib/bindings/UnaryExpression' export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression'
export type { BinaryExpression } from '../wasm-lib/bindings/BinaryExpression' export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression'
export type { ReturnStatement } from '../wasm-lib/bindings/ReturnStatement' export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement'
export type { ExpressionStatement } from '../wasm-lib/bindings/ExpressionStatement' export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement'
export type { CallExpression } from '../wasm-lib/bindings/CallExpression' export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
export type { VariableDeclarator } from '../wasm-lib/bindings/VariableDeclarator' export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/bindings/BinaryPart' export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/bindings/Literal' export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { ArrayExpression } from '../wasm-lib/bindings/ArrayExpression' export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type SyntaxType = export type SyntaxType =
| 'Program' | 'Program'

View File

@ -1,5 +1,5 @@
import { Diagnostic } from '@codemirror/lint' import { Diagnostic } from '@codemirror/lint'
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
type ExtractKind<T> = T extends { kind: infer K } ? K : never type ExtractKind<T> = T extends { kind: infer K } ? K : never
export class KCLError { export class KCLError {

View File

@ -4,10 +4,10 @@ import {
ArtifactMap, ArtifactMap,
SourceRangeMap, SourceRangeMap,
} from './std/engineConnection' } from './std/engineConnection'
import { ProgramReturn } from '../wasm-lib/bindings/ProgramReturn' import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib' import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { rangeTypeFix } from './abstractSyntaxTree' import { rangeTypeFix } from './abstractSyntaxTree'
export type SourceRange = [number, number] export type SourceRange = [number, number]
@ -146,10 +146,8 @@ export const _executor = async (
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg, parsed.msg,
parsed.kind === 'invalid_expression' rangeTypeFix(parsed.sourceRanges)
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
) )
console.log(kclError) console.log(kclError)

View File

@ -1,9 +1,14 @@
import { SourceRange } from 'lang/executor' import { SourceRange } from 'lang/executor'
import { Selections } from 'useStore' import { Selections } from 'useStore'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' import {
VITE_KC_API_WS_MODELING_URL,
VITE_KC_CONNECTION_TIMEOUT_MS,
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
} from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
interface ResultCommand { interface ResultCommand {
type: 'result' type: 'result'
@ -22,16 +27,6 @@ export interface SourceRangeMap {
[key: string]: SourceRange [key: string]: SourceRange
} }
interface SelectionsArgs {
id: string
type: Selections['codeBasedSelections'][number]['type']
}
interface CursorSelectionsArgs {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
}
interface NewTrackArgs { interface NewTrackArgs {
conn: EngineConnection conn: EngineConnection
mediaStream: MediaStream mediaStream: MediaStream
@ -45,7 +40,7 @@ type WebSocketResponse = Models['OkWebSocketResponseData_type']
export class EngineConnection { export class EngineConnection {
websocket?: WebSocket websocket?: WebSocket
pc?: RTCPeerConnection pc?: RTCPeerConnection
lossyDataChannel?: RTCDataChannel unreliableDataChannel?: RTCDataChannel
private ready: boolean private ready: boolean
@ -107,6 +102,11 @@ export class EngineConnection {
isReady() { isReady() {
return this.ready return this.ready
} }
// shouldTrace will return true when Sentry should be used to instrument
// the Engine.
shouldTrace() {
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
}
// connect will attempt to connect to the Engine over a WebSocket, and // connect will attempt to connect to the Engine over a WebSocket, and
// establish the WebRTC connections. // establish the WebRTC connections.
// //
@ -116,6 +116,44 @@ export class EngineConnection {
// TODO(paultag): make this safe to call multiple times, and figure out // TODO(paultag): make this safe to call multiple times, and figure out
// when a connection is in progress (state: connecting or something). // when a connection is in progress (state: connecting or something).
// Information on the connect transaction
class SpanPromise {
span: Sentry.Span
promise: Promise<void>
resolve?: (v: void) => void
constructor(span: Sentry.Span) {
this.span = span
this.promise = new Promise((resolve) => {
this.resolve = (v: void) => {
// here we're going to invoke finish before resolving the
// promise so that a `.then()` will order strictly after
// all spans have -- for sure -- been resolved, rather than
// doing a `then` on this promise.
this.span.finish()
resolve(v)
}
})
}
}
let webrtcMediaTransaction: Sentry.Transaction
let websocketSpan: SpanPromise
let mediaTrackSpan: SpanPromise
let dataChannelSpan: SpanPromise
let handshakeSpan: SpanPromise
let iceSpan: SpanPromise
if (this.shouldTrace()) {
webrtcMediaTransaction = Sentry.startTransaction({
name: 'webrtc-media',
})
websocketSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'websocket' })
)
}
this.websocket = new WebSocket(this.url, []) this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer' this.websocket.binaryType = 'arraybuffer'
@ -129,6 +167,37 @@ export class EngineConnection {
}) })
this.websocket.addEventListener('open', (event) => { this.websocket.addEventListener('open', (event) => {
if (this.shouldTrace()) {
websocketSpan.resolve?.()
handshakeSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'handshake' })
)
iceSpan = new SpanPromise(
webrtcMediaTransaction.startChild({ op: 'ice' })
)
dataChannelSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'data-channel',
})
)
mediaTrackSpan = new SpanPromise(
webrtcMediaTransaction.startChild({
op: 'media-track',
})
)
}
Promise.all([
handshakeSpan.promise,
iceSpan.promise,
dataChannelSpan.promise,
mediaTrackSpan.promise,
]).then(() => {
console.log('All spans finished, reporting')
webrtcMediaTransaction?.finish()
})
this.onWebsocketOpen(this) this.onWebsocketOpen(this)
}) })
@ -162,7 +231,7 @@ export class EngineConnection {
} else { } else {
console.error(`Error from server:`) console.error(`Error from server:`)
} }
message.errors.forEach((error) => { message?.errors?.forEach((error) => {
console.error(` - ${error.error_code}: ${error.message}`) console.error(` - ${error.error_code}: ${error.message}`)
}) })
return return
@ -191,6 +260,13 @@ export class EngineConnection {
sdp: answer.sdp, sdp: answer.sdp,
}) })
) )
if (this.shouldTrace()) {
// When both ends have a local and remote SDP, we've been able to
// set up successfully. We'll still need to find the right ICE
// servers, but this is hand-shook.
handshakeSpan.resolve?.()
}
} }
} else if (resp.type === 'trickle_ice') { } else if (resp.type === 'trickle_ice') {
let candidate = resp.data?.candidate let candidate = resp.data?.candidate
@ -220,9 +296,9 @@ export class EngineConnection {
// PeerConnection and waiting for events to fire our callbacks. // PeerConnection and waiting for events to fire our callbacks.
this.pc.addEventListener('connectionstatechange', (event) => { this.pc.addEventListener('connectionstatechange', (event) => {
// if (this.pc?.iceConnectionState === 'disconnected') { if (this.pc?.iceConnectionState === 'connected') {
// this.close() iceSpan.resolve?.()
// } }
}) })
this.pc.addEventListener('icecandidate', (event) => { this.pc.addEventListener('icecandidate', (event) => {
@ -272,8 +348,142 @@ export class EngineConnection {
}) })
this.pc.addEventListener('track', (event) => { this.pc.addEventListener('track', (event) => {
console.log('received track', event)
const mediaStream = event.streams[0] const mediaStream = event.streams[0]
if (this.shouldTrace()) {
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
mediaStreamTrack.addEventListener('unmute', () => {
// let settings = mediaStreamTrack.getSettings()
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
// mediaTrackSpan.span.setTag("width", settings.width)
// mediaTrackSpan.span.setTag("height", settings.height)
mediaTrackSpan.resolve?.()
})
}
// Set up the background thread to keep an eye on statistical
// information about the WebRTC media stream from the server to
// us. We'll also eventually want more global statistical information,
// but this will give us a baseline.
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
setInterval(() => {
if (this.pc === undefined) {
return
}
if (!this.shouldTrace()) {
return
}
// Use the WebRTC Statistics API to collect statistical information
// about the WebRTC connection we're using to report to Sentry.
mediaStream.getVideoTracks().forEach((videoTrack) => {
let trackStats = new Map<string, any>()
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// Sentry only allows 10 metrics per transaction. We're going
// to have to pick carefully here, eventually send like a prom
// file or something to the peer.
const transaction = Sentry.startTransaction({
name: 'webrtc-stats',
})
videoTrackStats.forEach((videoTrackReport) => {
if (videoTrackReport.type === 'inbound-rtp') {
// RTC Stream Info
// transaction.setMeasurement(
// 'mediaStreamTrack.framesDecoded',
// videoTrackReport.framesDecoded,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesDropped',
videoTrackReport.framesDropped,
''
)
// transaction.setMeasurement(
// 'mediaStreamTrack.framesReceived',
// videoTrackReport.framesReceived,
// 'frame'
// )
transaction.setMeasurement(
'rtcFramesPerSecond',
videoTrackReport.framesPerSecond,
'fps'
)
transaction.setMeasurement(
'rtcFreezeCount',
videoTrackReport.freezeCount,
''
)
transaction.setMeasurement(
'rtcJitter',
videoTrackReport.jitter,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferDelay',
// videoTrackReport.jitterBufferDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferEmittedCount',
// videoTrackReport.jitterBufferEmittedCount,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferMinimumDelay',
// videoTrackReport.jitterBufferMinimumDelay,
// ''
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.jitterBufferTargetDelay',
// videoTrackReport.jitterBufferTargetDelay,
// ''
// )
transaction.setMeasurement(
'rtcKeyFramesDecoded',
videoTrackReport.keyFramesDecoded,
''
)
transaction.setMeasurement(
'rtcTotalFreezesDuration',
videoTrackReport.totalFreezesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalInterFrameDelay',
// videoTrackReport.totalInterFrameDelay,
// ''
// )
transaction.setMeasurement(
'rtcTotalPausesDuration',
videoTrackReport.totalPausesDuration,
'second'
)
// transaction.setMeasurement(
// 'mediaStreamTrack.totalProcessingDelay',
// videoTrackReport.totalProcessingDelay,
// 'second'
// )
} else if (videoTrackReport.type === 'transport') {
// // Bytes i/o
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesReceived',
// videoTrackReport.bytesReceived,
// 'byte'
// )
// transaction.setMeasurement(
// 'mediaStreamTrack.bytesSent',
// videoTrackReport.bytesSent,
// 'byte'
// )
}
})
transaction?.finish()
})
})
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
}
this.onNewTrack({ this.onNewTrack({
conn: this, conn: this,
mediaStream: mediaStream, mediaStream: mediaStream,
@ -285,45 +495,48 @@ export class EngineConnection {
let connectionStarted = new Date() let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => { this.pc.addEventListener('datachannel', (event) => {
this.lossyDataChannel = event.channel this.unreliableDataChannel = event.channel
console.log('accepted lossy data channel', event.channel.label) console.log('accepted unreliable data channel', event.channel.label)
this.lossyDataChannel.addEventListener('open', (event) => { this.unreliableDataChannel.addEventListener('open', (event) => {
console.log('lossy data channel opened', event) console.log('unreliable data channel opened', event)
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this) this.onDataChannelOpen(this)
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
this.onEngineConnectionOpen(this) this.onEngineConnectionOpen(this)
this.ready = true this.ready = true
}) })
this.lossyDataChannel.addEventListener('close', (event) => { this.unreliableDataChannel.addEventListener('close', (event) => {
console.log('lossy data channel closed') console.log('unreliable data channel closed')
this.close() this.close()
}) })
this.lossyDataChannel.addEventListener('error', (event) => { this.unreliableDataChannel.addEventListener('error', (event) => {
console.log('lossy data channel error') console.log('unreliable data channel error')
this.close() this.close()
}) })
}) })
this.onConnectionStarted(this) this.onConnectionStarted(this)
} }
send(message: object) { send(message: object | string) {
// TODO(paultag): Add in logic to determine the connection state and // TODO(paultag): Add in logic to determine the connection state and
// take actions if needed? // take actions if needed?
this.websocket?.send(JSON.stringify(message)) this.websocket?.send(
typeof message === 'string' ? message : JSON.stringify(message)
)
} }
close() { close() {
this.websocket?.close() this.websocket?.close()
this.pc?.close() this.pc?.close()
this.lossyDataChannel?.close() this.unreliableDataChannel?.close()
this.websocket = undefined this.websocket = undefined
this.pc = undefined this.pc = undefined
this.lossyDataChannel = undefined this.unreliableDataChannel = undefined
this.onClose(this) this.onClose(this)
this.ready = false this.ready = false
@ -331,6 +544,23 @@ export class EngineConnection {
} }
export type EngineCommand = Models['WebSocketRequest_type'] export type EngineCommand = Models['WebSocketRequest_type']
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
type UnreliableResponses = Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'highlight_set_entity' }
>
interface UnreliableSubscription<T extends UnreliableResponses['type']> {
event: T
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
}
interface Subscription<T extends ModelTypes> {
event: T
callback: (
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
) => void
}
export class EngineCommandManager { export class EngineCommandManager {
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
@ -340,10 +570,17 @@ export class EngineCommandManager {
engineConnection?: EngineConnection engineConnection?: EngineConnection
waitForReady: Promise<void> = new Promise(() => {}) waitForReady: Promise<void> = new Promise(() => {})
private resolveReady = () => {} private resolveReady = () => {}
onHoverCallback: (id?: string) => void = () => {}
onClickCallback: (selection?: SelectionsArgs) => void = () => {} subscriptions: {
onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void = [event: string]: {
() => {} [localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
unreliableSubscriptions: {
[event: string]: {
[localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
constructor({ constructor({
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
@ -373,20 +610,28 @@ export class EngineCommandManager {
}, },
onConnectionStarted: (engineConnection) => { onConnectionStarted: (engineConnection) => {
engineConnection?.pc?.addEventListener('datachannel', (event) => { engineConnection?.pc?.addEventListener('datachannel', (event) => {
let lossyDataChannel = event.channel let unreliableDataChannel = event.channel
lossyDataChannel.addEventListener('message', (event) => { unreliableDataChannel.addEventListener('message', (event) => {
const result: Models['OkModelingCmdResponse_type'] = JSON.parse( const result: UnreliableResponses = JSON.parse(event.data)
event.data Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
if (
result?.data?.sequence &&
result?.data.sequence > this.inSequence &&
result.type === 'highlight_set_entity'
) {
this.inSequence = result.data.sequence
callback(result)
}
}
) )
if (
result.type === 'highlight_set_entity' &&
result?.data?.sequence &&
result.data.sequence > this.inSequence
) {
this.onHoverCallback(result.data.entity_id)
this.inSequence = result.data.sequence
}
}) })
}) })
@ -418,8 +663,8 @@ export class EngineCommandManager {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.log('peer is not sending video to us') console.log('peer is not sending video to us')
this.engineConnection?.close() // this.engineConnection?.close()
this.engineConnection?.connect() // this.engineConnection?.connect()
}) })
setMediaStream(mediaStream) setMediaStream(mediaStream)
@ -433,18 +678,11 @@ export class EngineCommandManager {
return return
} }
const modelingResponse = message.data.modeling_response const modelingResponse = message.data.modeling_response
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse)
)
const command = this.artifactMap[id] const command = this.artifactMap[id]
if (modelingResponse.type === 'select_with_point') {
if (modelingResponse?.data?.entity_id) {
this.onClickCallback({
id: modelingResponse?.data?.entity_id,
type: 'default',
})
} else {
this.onClickCallback()
}
}
if (command && command.type === 'pending') { if (command && command.type === 'pending') {
const resolve = command.resolve const resolve = command.resolve
this.artifactMap[id] = { this.artifactMap[id] = {
@ -453,6 +691,7 @@ export class EngineCommandManager {
} }
resolve({ resolve({
id, id,
data: modelingResponse,
}) })
} else { } else {
this.artifactMap[id] = { this.artifactMap[id] = {
@ -468,21 +707,49 @@ export class EngineCommandManager {
this.artifactMap = {} this.artifactMap = {}
this.sourceRangeMap = {} this.sourceRangeMap = {}
} }
subscribeTo<T extends ModelTypes>({
event,
callback,
}: Subscription<T>): () => void {
const localUnsubscribeId = uuidv4()
const otherEventCallbacks = this.subscriptions[event]
if (otherEventCallbacks) {
otherEventCallbacks[localUnsubscribeId] = callback
} else {
this.subscriptions[event] = {
[localUnsubscribeId]: callback,
}
}
return () => this.unSubscribeTo(event, localUnsubscribeId)
}
private unSubscribeTo(event: ModelTypes, id: string) {
delete this.subscriptions[event][id]
}
subscribeToUnreliable<T extends UnreliableResponses['type']>({
event,
callback,
}: UnreliableSubscription<T>): () => void {
const localUnsubscribeId = uuidv4()
const otherEventCallbacks = this.unreliableSubscriptions[event]
if (otherEventCallbacks) {
otherEventCallbacks[localUnsubscribeId] = callback
} else {
this.unreliableSubscriptions[event] = {
[localUnsubscribeId]: callback,
}
}
return () => this.unSubscribeToUnreliable(event, localUnsubscribeId)
}
private unSubscribeToUnreliable(
event: UnreliableResponses['type'],
id: string
) {
delete this.unreliableSubscriptions[event][id]
}
endSession() { endSession() {
// this.websocket?.close() // this.websocket?.close()
// socket.off('command') // socket.off('command')
} }
onHover(callback: (id?: string) => void) {
// It's when the user hovers over a part in the 3d scene, and so the engine should tell the
// frontend about that (with it's id) so that the FE can highlight code associated with that id
this.onHoverCallback = callback
}
onClick(callback: (selection?: SelectionsArgs) => void) {
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
// line of code
this.onClickCallback = callback
}
cusorsSelected(selections: { cusorsSelected(selections: {
otherSelections: Selections['otherSelections'] otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[] idBasedSelections: { type: string; id: string }[]
@ -507,32 +774,38 @@ export class EngineCommandManager {
cmd_id: uuidv4(), cmd_id: uuidv4(),
}) })
} }
sendSceneCommand(command: EngineCommand) { sendSceneCommand(command: EngineCommand): Promise<any> {
if (!this.engineConnection?.isReady()) { if (!this.engineConnection?.isReady()) {
console.log('socket not ready') console.log('socket not ready')
return return Promise.resolve()
} }
if (command.type !== 'modeling_cmd_req') return if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd const cmd = command.cmd
if ( if (
cmd.type === 'camera_drag_move' && cmd.type === 'camera_drag_move' &&
this.engineConnection?.lossyDataChannel this.engineConnection?.unreliableDataChannel
) { ) {
cmd.sequence = this.outSequence cmd.sequence = this.outSequence
this.outSequence++ this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command)) this.engineConnection?.unreliableDataChannel?.send(
return JSON.stringify(command)
)
return Promise.resolve()
} else if ( } else if (
cmd.type === 'highlight_set_entity' && cmd.type === 'highlight_set_entity' &&
this.engineConnection?.lossyDataChannel this.engineConnection?.unreliableDataChannel
) { ) {
cmd.sequence = this.outSequence cmd.sequence = this.outSequence
this.outSequence++ this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command)) this.engineConnection?.unreliableDataChannel?.send(
return JSON.stringify(command)
)
return Promise.resolve()
} }
console.log('sending command', command) console.log('sending command', command)
// since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command) this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id)
} }
sendModelingCommand({ sendModelingCommand({
id, id,
@ -541,15 +814,18 @@ export class EngineCommandManager {
}: { }: {
id: string id: string
range: SourceRange range: SourceRange
command: EngineCommand command: EngineCommand | string
}): Promise<any> { }): Promise<any> {
this.sourceRangeMap[id] = range this.sourceRangeMap[id] = range
if (!this.engineConnection?.isReady()) { if (!this.engineConnection?.isReady()) {
console.log('socket not ready') console.log('socket not ready')
return new Promise(() => {}) return Promise.resolve()
} }
this.engineConnection?.send(command) this.engineConnection?.send(command)
return this.handlePendingCommand(id)
}
handlePendingCommand(id: string) {
let resolve: (val: any) => void = () => {} let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => { const promise = new Promise((_resolve, reject) => {
resolve = _resolve resolve = _resolve
@ -575,10 +851,9 @@ export class EngineCommandManager {
if (commandStr === undefined) { if (commandStr === undefined) {
throw new Error('commandStr is undefined') throw new Error('commandStr is undefined')
} }
const command: EngineCommand = JSON.parse(commandStr)
const range: SourceRange = JSON.parse(rangeStr) const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command }) return this.sendModelingCommand({ id, range, command: commandStr })
} }
commandResult(id: string): Promise<any> { commandResult(id: string): Promise<any> {
const command = this.artifactMap[id] const command = this.artifactMap[id]

View File

@ -1,8 +1,8 @@
import { lexer_js } from '../wasm-lib/pkg/wasm_lib' import { lexer_js } from '../wasm-lib/pkg/wasm_lib'
import { initPromise } from './rust' import { initPromise } from './rust'
import { Token } from '../wasm-lib/bindings/Token' import { Token } from '../wasm-lib/kcl/bindings/Token'
export type { Token } from '../wasm-lib/bindings/Token' export type { Token } from '../wasm-lib/kcl/bindings/Token'
export async function asyncLexer(str: string): Promise<Token[]> { export async function asyncLexer(str: string): Promise<Token[]> {
await initPromise await initPromise

124
src/lib/commands.ts Normal file
View File

@ -0,0 +1,124 @@
import { AnyStateMachine, EventFrom, StateFrom } from 'xstate'
import { isTauri } from './isTauri'
type InitialCommandBarMetaArg = {
name: string
type: 'string' | 'select'
description?: string
defaultValue?: string
options: string | Array<{ name: string }>
}
type Platform = 'both' | 'web' | 'desktop'
export type CommandBarMeta = {
[key: string]:
| {
displayValue: (args: string[]) => string
args: InitialCommandBarMetaArg[]
hide?: Platform
}
| {
hide?: Platform
}
}
export type Command = {
owner: string
name: string
callback: Function
meta?: {
displayValue(args: string[]): string | string
args: SubCommand[]
}
}
export type SubCommand = {
name: string
type: 'select' | 'string'
description?: string
options?: Partial<{ name: string }>[]
}
interface CommandBarArgs<T extends AnyStateMachine> {
type: EventFrom<T>['type']
state: StateFrom<T>
commandBarMeta?: CommandBarMeta
send: Function
owner: string
}
export function createMachineCommand<T extends AnyStateMachine>({
type,
state,
commandBarMeta,
send,
owner,
}: CommandBarArgs<T>): Command | null {
const lookedUpMeta = commandBarMeta && commandBarMeta[type]
if (lookedUpMeta && 'hide' in lookedUpMeta) {
const { hide } = lookedUpMeta
if (hide === 'both') return null
else if (hide === 'desktop' && isTauri()) return null
else if (hide === 'web' && !isTauri()) return null
}
let replacedArgs
if (lookedUpMeta && 'args' in lookedUpMeta) {
replacedArgs = lookedUpMeta.args.map((arg) => {
const optionsFromContext = state.context[
arg.options as keyof typeof state.context
] as { name: string }[] | string | undefined
const defaultValueFromContext = state.context[
arg.defaultValue as keyof typeof state.context
] as string | undefined
const options =
arg.options instanceof Array
? arg.options.map((o) => ({
...o,
description:
defaultValueFromContext === o.name ? '(current)' : '',
}))
: !optionsFromContext || typeof optionsFromContext === 'string'
? [
{
name: optionsFromContext,
description: arg.description || '',
},
]
: optionsFromContext.map((o) => ({
name: o.name || '',
description: arg.description || '',
}))
return {
...arg,
options,
}
}) as any[]
}
// We have to recreate this object every time,
// otherwise we'll have stale state in the CommandBar
// after completing our first action
const meta = lookedUpMeta
? {
...lookedUpMeta,
args: replacedArgs,
}
: undefined
return {
name: type,
owner,
callback: (data: EventFrom<T, typeof type>) => {
if (data !== undefined && data !== null) {
send(type, { data })
} else {
send(type)
}
},
meta: meta as any,
}
}

View File

@ -1,16 +1,13 @@
import { useAuthMachine } from '../hooks/useAuthMachine' export default function fetcher(input: RequestInfo, init: RequestInit = {}) {
const fetcherWithToken = async (token?: string): Promise<JSON> => {
const headers = { ...init.headers } as Record<string, string>
if (token) {
headers.Authorization = `Bearer ${token}`
}
export default async function fetcher<JSON = any>( const credentials = 'include' as RequestCredentials
input: RequestInfo, const res = await fetch(input, { ...init, credentials, headers })
init: RequestInit = {} return res.json()
): Promise<JSON> {
const [token] = useAuthMachine((s) => s?.context?.token)
const headers = { ...init.headers } as Record<string, string>
if (token) {
headers.Authorization = `Bearer ${token}`
} }
return fetcherWithToken
const credentials = 'include' as RequestCredentials
const res = await fetch(input, { ...init, credentials, headers })
return res.json()
} }

View File

@ -1,9 +0,0 @@
import { Themes } from '../useStore'
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' &&
'matchMedia' in window &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
: Themes.Light
}

64
src/lib/sorting.ts Normal file
View File

@ -0,0 +1,64 @@
import {
faArrowDown,
faArrowUp,
faCircleDot,
} from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata } from '../Router'
const DESC = ':desc'
export function getSortIcon(currentSort: string, newSort: string) {
if (currentSort === newSort) {
return faArrowUp
} else if (currentSort === newSort + DESC) {
return faArrowDown
}
return faCircleDot
}
export function getNextSearchParams(currentSort: string, newSort: string) {
if (currentSort === null || !currentSort)
return { sort_by: newSort + (newSort !== 'modified' ? DESC : '') }
if (currentSort.includes(newSort) && !currentSort.includes(DESC))
return { sort_by: '' }
return {
sort_by: newSort + (currentSort.includes(DESC) ? '' : DESC),
}
}
export function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
}
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (
a.entrypoint_metadata?.modifiedAt &&
b.entrypoint_metadata?.modifiedAt
) {
return !sortBy || sortBy.includes('desc')
? b.entrypoint_metadata.modifiedAt.getTime() -
a.entrypoint_metadata.modifiedAt.getTime()
: a.entrypoint_metadata.modifiedAt.getTime() -
b.entrypoint_metadata.modifiedAt.getTime()
}
return 0
}
if (sortBy?.includes('name')) {
return sortByName
} else {
return sortByModified
}
}

View File

@ -1,6 +1,11 @@
import { FileEntry, createDir, exists, writeTextFile } from '@tauri-apps/api/fs' import {
FileEntry,
createDir,
exists,
readDir,
writeTextFile,
} from '@tauri-apps/api/fs'
import { documentDir } from '@tauri-apps/api/path' import { documentDir } from '@tauri-apps/api/path'
import { useStore } from '../useStore'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router' import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
@ -12,35 +17,31 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
export const MAX_PADDING = 7 export const MAX_PADDING = 7
// Initializes the project directory and returns the path // Initializes the project directory and returns the path
export async function initializeProjectDirectory() { export async function initializeProjectDirectory(directory: string) {
if (!isTauri()) { if (!isTauri()) {
throw new Error( throw new Error(
'initializeProjectDirectory() can only be called from a Tauri app' 'initializeProjectDirectory() can only be called from a Tauri app'
) )
} }
const { defaultDir: projectDir, setDefaultDir } = useStore.getState()
if (projectDir && projectDir.dir.length > 0) { if (directory) {
const dirExists = await exists(projectDir.dir) const dirExists = await exists(directory)
if (!dirExists) { if (!dirExists) {
await createDir(projectDir.dir, { recursive: true }) await createDir(directory, { recursive: true })
} }
return projectDir return directory
} }
const appData = await documentDir() const docDirectory = await documentDir()
const INITIAL_DEFAULT_DIR = { const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
dir: appData + PROJECT_FOLDER,
}
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir) const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
if (!defaultDirExists) { if (!defaultDirExists) {
await createDir(INITIAL_DEFAULT_DIR.dir, { recursive: true }) await createDir(INITIAL_DEFAULT_DIR, { recursive: true })
} }
setDefaultDir(INITIAL_DEFAULT_DIR)
return INITIAL_DEFAULT_DIR return INITIAL_DEFAULT_DIR
} }
@ -51,6 +52,25 @@ export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
) )
} }
// Read the contents of a directory
// and return the valid projects
export async function getProjectsInDir(projectDir: string) {
const readProjects = (
await readDir(projectDir, {
recursive: true,
})
).filter(isProjectDirectory)
const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
...p,
}))
)
return projectsWithMetadata
}
// Creates a new file in the default directory with the default project name // Creates a new file in the default directory with the default project name
// Returns the path to the new file // Returns the path to the new file
export async function createNewProject( export async function createNewProject(

23
src/lib/theme.ts Normal file
View File

@ -0,0 +1,23 @@
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
// Get the theme from the system settings manually
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof window !== 'undefined' && 'matchMedia' in window
? window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.Dark
: Themes.Light
: Themes.Light
}
// Set the theme class on the body element
export function setThemeClass(theme: Themes) {
if (theme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}

View File

@ -1,6 +1,24 @@
import { createMachine, assign } from 'xstate' import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL' import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
const LOCAL_USER: Models['User_type'] = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
company: 'Test Company',
discord: 'Test User#1234',
github: 'testuser',
phone: '555-555-5555',
first_name: 'Test',
last_name: 'User',
}
export interface UserContext { export interface UserContext {
user?: Models['User_type'] user?: Models['User_type']
@ -9,16 +27,22 @@ export interface UserContext {
export type Events = export type Events =
| { | {
type: 'logout' type: 'Log out'
} }
| { | {
type: 'tryLogin' type: 'Log in'
token?: string token?: string
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
export const authCommandBarMeta: CommandBarMeta = {
'Log in': {
hide: 'both',
},
}
export const authMachine = createMachine<UserContext, Events>( export const authMachine = createMachine<UserContext, Events>(
{ {
id: 'Auth', id: 'Auth',
@ -50,7 +74,7 @@ export const authMachine = createMachine<UserContext, Events>(
loggedIn: { loggedIn: {
entry: ['goToIndexPage'], entry: ['goToIndexPage'],
on: { on: {
logout: { 'Log out': {
target: 'loggedOut', target: 'loggedOut',
}, },
}, },
@ -58,10 +82,10 @@ export const authMachine = createMachine<UserContext, Events>(
loggedOut: { loggedOut: {
entry: ['goToSignInPage'], entry: ['goToSignInPage'],
on: { on: {
tryLogin: { 'Log in': {
target: 'checkIfLoggedIn', target: 'checkIfLoggedIn',
actions: assign({ actions: assign({
token: (context, event) => { token: (_, event) => {
const token = event.token || '' const token = event.token || ''
localStorage.setItem(TOKEN_PERSIST_KEY, token) localStorage.setItem(TOKEN_PERSIST_KEY, token)
return token return token
@ -71,10 +95,12 @@ export const authMachine = createMachine<UserContext, Events>(
}, },
}, },
}, },
schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } }, schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
predictableActionArguments: true, predictableActionArguments: true,
preserveActionOrder: true, preserveActionOrder: true,
context: { token: persistedToken }, context: {
token: persistedToken,
},
}, },
{ {
actions: {}, actions: {},
@ -91,12 +117,17 @@ async function getUser(context: UserContext) {
} }
if (!context.token && '__TAURI__' in window) throw 'not log in' if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}` if (context.token) headers['Authorization'] = `Bearer ${context.token}`
const response = await fetch(url, { if (SKIP_AUTH) return LOCAL_USER
method: 'GET', try {
credentials: 'include', const response = await fetch(url, {
headers, method: 'GET',
}) credentials: 'include',
const user = await response.json() headers,
if ('error_code' in user) throw new Error(user.message) })
return user const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
} }

218
src/machines/homeMachine.ts Normal file
View File

@ -0,0 +1,218 @@
import { assign, createMachine } from 'xstate'
import { ProjectWithEntryPointMetadata } from '../Router'
import { CommandBarMeta } from '../lib/commands'
export const homeCommandMeta: CommandBarMeta = {
'Create project': {
displayValue: (args: string[]) => `Create project "${args[0]}"`,
args: [
{
name: 'name',
type: 'string',
description: '(default)',
options: 'defaultProjectName',
},
],
},
'Open project': {
displayValue: (args: string[]) => `Open project "${args[0]}"`,
args: [
{
name: 'name',
type: 'select',
options: 'projects',
},
],
},
'Delete project': {
displayValue: (args: string[]) => `Delete project "${args[0]}"`,
args: [
{
name: 'name',
type: 'select',
options: 'projects',
},
],
},
'Rename project': {
displayValue: (args: string[]) =>
`Rename project "${args[0]}" to "${args[1]}"`,
args: [
{
name: 'oldName',
type: 'select',
options: 'projects',
},
{
name: 'newName',
type: 'string',
description: '(default)',
options: 'defaultProjectName',
},
],
},
assign: {
hide: 'both',
},
}
export const homeMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
id: 'Home machine',
initial: 'Reading projects',
context: {
projects: [] as ProjectWithEntryPointMetadata[],
defaultProjectName: '',
defaultDirectory: '',
},
on: {
assign: {
actions: assign((_, event) => ({
...event.data,
})),
target: '.Reading projects',
},
},
states: {
'Has no projects': {
on: {
'Create project': {
target: 'Creating project',
},
},
},
'Has projects': {
on: {
'Rename project': {
target: 'Renaming project',
},
'Create project': {
target: 'Creating project',
},
'Delete project': {
target: 'Deleting project',
},
'Open project': {
target: 'Opening project',
},
},
},
'Creating project': {
invoke: {
id: 'create-project',
src: 'createProject',
onDone: [
{
target: 'Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: 'Reading projects',
actions: ['toastError'],
},
],
},
},
'Renaming project': {
invoke: {
id: 'rename-project',
src: 'renameProject',
onDone: [
{
target: '#Home machine.Reading projects',
actions: ['toastSuccess'],
},
],
onError: [
{
target: '#Home machine.Reading projects',
actions: ['toastError'],
},
],
},
},
'Deleting project': {
invoke: {
id: 'delete-project',
src: 'deleteProject',
onDone: [
{
actions: ['toastSuccess'],
target: '#Home machine.Reading projects',
},
],
onError: {
actions: ['toastError'],
target: '#Home machine.Has projects',
},
},
},
'Reading projects': {
invoke: {
id: 'read-projects',
src: 'readProjects',
onDone: [
{
cond: 'Has at least 1 project',
target: 'Has projects',
actions: ['setProjects'],
},
{
target: 'Has no projects',
actions: ['setProjects'],
},
],
onError: [
{
target: 'Has no projects',
actions: ['toastError'],
},
],
},
},
'Opening project': {
entry: ['navigateToProject'],
},
},
schema: {
events: {} as
| { type: 'Open project'; data: { name: string } }
| { type: 'Rename project'; data: { oldName: string; newName: string } }
| { type: 'Create project'; data: { name: string } }
| { type: 'Delete project'; data: { name: string } }
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-projects'
data: ProjectWithEntryPointMetadata[]
}
| { type: 'assign'; data: { [key: string]: any } },
},
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./homeMachine.typegen').Typegen0,
},
{
actions: {
setProjects: assign((_, event) => {
return { projects: event.data as ProjectWithEntryPointMetadata[] }
}),
},
}
)

View File

@ -0,0 +1,99 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'done.invoke.create-project': {
type: 'done.invoke.create-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.delete-project': {
type: 'done.invoke.delete-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.read-projects': {
type: 'done.invoke.read-projects'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'done.invoke.rename-project': {
type: 'done.invoke.rename-project'
data: unknown
__tip: 'See the XState TS docs to learn how to strongly type this.'
}
'error.platform.create-project': {
type: 'error.platform.create-project'
data: unknown
}
'error.platform.delete-project': {
type: 'error.platform.delete-project'
data: unknown
}
'error.platform.read-projects': {
type: 'error.platform.read-projects'
data: unknown
}
'error.platform.rename-project': {
type: 'error.platform.rename-project'
data: unknown
}
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {
createProject: 'done.invoke.create-project'
deleteProject: 'done.invoke.delete-project'
readProjects: 'done.invoke.read-projects'
renameProject: 'done.invoke.rename-project'
}
missingImplementations: {
actions: 'navigateToProject' | 'toastError' | 'toastSuccess'
delays: never
guards: 'Has at least 1 project'
services:
| 'createProject'
| 'deleteProject'
| 'readProjects'
| 'renameProject'
}
eventsCausingActions: {
navigateToProject: 'Open project'
setProjects: 'done.invoke.read-projects'
toastError:
| 'error.platform.create-project'
| 'error.platform.delete-project'
| 'error.platform.read-projects'
| 'error.platform.rename-project'
toastSuccess:
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
}
eventsCausingDelays: {}
eventsCausingGuards: {
'Has at least 1 project': 'done.invoke.read-projects'
}
eventsCausingServices: {
createProject: 'Create project'
deleteProject: 'Delete project'
readProjects:
| 'assign'
| 'done.invoke.create-project'
| 'done.invoke.delete-project'
| 'done.invoke.rename-project'
| 'error.platform.create-project'
| 'error.platform.rename-project'
| 'xstate.init'
renameProject: 'Rename project'
}
matchesStates:
| 'Creating project'
| 'Deleting project'
| 'Has no projects'
| 'Has projects'
| 'Opening project'
| 'Reading projects'
| 'Renaming project'
tags: never
}

View File

@ -0,0 +1,207 @@
import { assign, createMachine } from 'xstate'
import { BaseUnit, baseUnitsUnion } from '../useStore'
import { CommandBarMeta } from '../lib/commands'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsCommandBarMeta: CommandBarMeta = {
'Set Theme': {
displayValue: (args: string[]) => 'Change the app theme',
args: [
{
name: 'theme',
type: 'select',
defaultValue: 'theme',
options: Object.values(Themes).map((v) => ({ name: v })) as {
name: string
}[],
},
],
},
'Set Default Project Name': {
displayValue: (args: string[]) => 'Set a new default project name',
hide: 'web',
args: [
{
name: 'defaultProjectName',
type: 'string',
description: '(default)',
defaultValue: 'defaultProjectName',
options: 'defaultProjectName',
},
],
},
'Set Default Directory': {
hide: 'both',
},
'Set Unit System': {
displayValue: (args: string[]) => 'Set your default unit system',
args: [
{
name: 'unitSystem',
type: 'select',
defaultValue: 'unitSystem',
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
},
],
},
'Set Base Unit': {
displayValue: (args: string[]) => 'Set your default base unit',
args: [
{
name: 'baseUnit',
type: 'select',
defaultValue: 'baseUnit',
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
},
],
},
'Set Onboarding Status': {
hide: 'both',
},
}
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
id: 'Settings',
predictableActionArguments: true,
context: {
theme: Themes.System,
defaultProjectName: '',
unitSystem: UnitSystem.Imperial,
baseUnit: 'in' as BaseUnit,
defaultDirectory: '',
showDebugPanel: false,
onboardingStatus: '',
},
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass'],
on: {
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
actions: [
assign({
defaultProjectName: (_, event) => event.data.defaultProjectName,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Directory': {
actions: [
assign({
defaultDirectory: (_, event) => event.data.defaultDirectory,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Base Unit': {
actions: [
assign({ baseUnit: (_, event) => event.data.baseUnit }),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Toggle Debug Panel': {
actions: [
assign({
showDebugPanel: (context) => {
return !context.showDebugPanel
},
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Unit System'
data: { unitSystem: UnitSystem }
}
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Toggle Debug Panel' },
},
},
{
actions: {
persistSettings: (context) => {
try {
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
} catch (e) {
console.error(e)
}
},
setThemeClass: (context, event) => {
const currentTheme =
event.type === 'Set Theme' ? event.data.theme : context.theme
setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)
},
},
}
)

View File

@ -0,0 +1,46 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true
internalEvents: {
'xstate.init': { type: 'xstate.init' }
}
invokeSrcNameMap: {}
missingImplementations: {
actions: 'toastSuccess'
delays: never
guards: never
services: never
}
eventsCausingActions: {
persistSettings:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
setThemeClass:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Onboarding Status'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
| 'xstate.init'
toastSuccess:
| 'Set Base Unit'
| 'Set Default Directory'
| 'Set Default Project Name'
| 'Set Theme'
| 'Set Unit System'
| 'Toggle Debug Panel'
}
eventsCausingDelays: {}
eventsCausingGuards: {}
eventsCausingServices: {}
matchesStates: 'idle'
tags: never
}

View File

@ -1,93 +1,139 @@
import { FormEvent, useCallback, useEffect, useState } from 'react' import { FormEvent, useEffect } from 'react'
import { readDir, removeDir, renameFile } from '@tauri-apps/api/fs' import { removeDir, renameFile } from '@tauri-apps/api/fs'
import { import {
createNewProject, createNewProject,
getNextProjectIndex, getNextProjectIndex,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated, doesProjectNameNeedInterpolated,
isProjectDirectory, getProjectsInDir,
PROJECT_ENTRYPOINT,
} from '../lib/tauriFS' } from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton' import { ActionButton } from '../components/ActionButton'
import { import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
faArrowDown,
faArrowUp,
faCircleDot,
faPlus,
} from '@fortawesome/free-solid-svg-icons'
import { useStore } from '../useStore'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { AppHeader } from '../components/AppHeader' import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard' import ProjectCard from '../components/ProjectCard'
import { useLoaderData, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router' import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
import Loading from '../components/Loading' import Loading from '../components/Loading'
import { metadata } from 'tauri-plugin-fs-extra-api' import { useMachine } from '@xstate/react'
import { homeCommandMeta, homeMachine } from '../machines/homeMachine'
const DESC = ':desc' import { ContextFrom, EventFrom } from 'xstate'
import { paths } from '../Router'
import {
getNextSearchParams,
getSortFunction,
getSortIcon,
} from '../lib/sorting'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
// This route only opens in the Tauri desktop context for now, // This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types. // as defined in Router.tsx, so we can use the Tauri APIs and types.
const Home = () => { const Home = () => {
const [searchParams, setSearchParams] = useSearchParams() const { commands, setCommandBarOpen } = useCommandsContext()
const sort = searchParams.get('sort_by') ?? 'modified:desc' const navigate = useNavigate()
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
const [isLoading, setIsLoading] = useState(true) const {
const [projects, setProjects] = useState(loadedProjects || []) settings: {
const { defaultDir, defaultProjectName } = useStore((s) => ({ context: { defaultDirectory, defaultProjectName },
defaultDir: s.defaultDir, },
defaultProjectName: s.defaultProjectName, } = useGlobalStateContext()
}))
const modifiedSelected = sort?.includes('modified') || !sort || sort === null const [state, send] = useMachine(homeMachine, {
context: {
projects: loadedProjects,
defaultProjectName,
defaultDirectory,
},
actions: {
navigateToProject: (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine>
) => {
if (event.data && 'name' in event.data) {
setCommandBarOpen(false)
navigate(
`${paths.FILE}/${encodeURIComponent(
context.defaultDirectory + '/' + event.data.name
)}`
)
}
},
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
getProjectsInDir(context.defaultDirectory),
createProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
) => {
let name =
event.data && 'name' in event.data
? event.data.name
: defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
const refreshProjects = useCallback( await createNewProject(context.defaultDirectory + '/' + name)
async (projectDir = defaultDir) => { return `Successfully created "${name}"`
const readProjects = ( },
await readDir(projectDir.dir, { renameProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Rename project'>
) => {
const { oldName, newName } = event.data
let name = newName ? newName : context.defaultProjectName
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await renameFile(
context.defaultDirectory + '/' + oldName,
context.defaultDirectory + '/' + name
)
return `Successfully renamed "${oldName}" to "${name}"`
},
deleteProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Delete project'>
) => {
await removeDir(context.defaultDirectory + '/' + event.data.name, {
recursive: true, recursive: true,
}) })
).filter(isProjectDirectory) return `Successfully deleted "${event.data.name}"`
},
const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(
p.path + '/' + PROJECT_ENTRYPOINT
),
...p,
}))
)
setProjects(projectsWithMetadata)
}, },
[defaultDir, setProjects] guards: {
) 'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
if (event.type !== 'done.invoke.read-projects') return false
return event?.data?.length ? event.data?.length >= 1 : false
},
},
})
const { projects } = state.context
const [searchParams, setSearchParams] = useSearchParams()
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const isSortByModified = sort?.includes('modified') || !sort || sort === null
useStateMachineCommands<typeof homeMachine>({
commands,
send,
state,
commandBarMeta: homeCommandMeta,
owner: 'home',
})
useEffect(() => { useEffect(() => {
refreshProjects(defaultDir).then(() => { send({ type: 'assign', data: { defaultProjectName, defaultDirectory } })
setIsLoading(false) }, [defaultDirectory, defaultProjectName, send])
})
}, [setIsLoading, refreshProjects, defaultDir])
async function handleNewProject() {
let projectName = defaultProjectName
if (doesProjectNameNeedInterpolated(projectName)) {
const nextIndex = await getNextProjectIndex(defaultProjectName, projects)
projectName = interpolateProjectNameWithIndex(
defaultProjectName,
nextIndex
)
}
await createNewProject(defaultDir.dir + '/' + projectName).catch((err) => {
console.error('Error creating project:', err)
toast.error('Error creating project')
})
await refreshProjects()
toast.success('Project created')
}
async function handleRenameProject( async function handleRenameProject(
e: FormEvent<HTMLFormElement>, e: FormEvent<HTMLFormElement>,
@ -96,85 +142,14 @@ const Home = () => {
const { newProjectName } = Object.fromEntries( const { newProjectName } = Object.fromEntries(
new FormData(e.target as HTMLFormElement) new FormData(e.target as HTMLFormElement)
) )
if (newProjectName && project.name && newProjectName !== project.name) {
const dir = project.path?.slice(0, project.path?.lastIndexOf('/'))
await renameFile(project.path, dir + '/' + newProjectName).catch(
(err) => {
console.error('Error renaming project:', err)
toast.error('Error renaming project')
}
)
await refreshProjects() send('Rename project', {
toast.success('Project renamed') data: { oldName: project.name, newName: newProjectName },
} })
} }
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) { async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
if (project.path) { send('Delete project', { data: { name: project.name || '' } })
await removeDir(project.path, { recursive: true }).catch((err) => {
console.error('Error deleting project:', err)
toast.error('Error deleting project')
})
await refreshProjects()
toast.success('Project deleted')
}
}
function getSortIcon(sortBy: string) {
if (sort === sortBy) {
return faArrowUp
} else if (sort === sortBy + DESC) {
return faArrowDown
}
return faCircleDot
}
function getNextSearchParams(sortBy: string) {
if (sort === null || !sort)
return { sort_by: sortBy + (sortBy !== 'modified' ? DESC : '') }
if (sort.includes(sortBy) && !sort.includes(DESC)) return { sort_by: '' }
return {
sort_by: sortBy + (sort.includes(DESC) ? '' : DESC),
}
}
function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
}
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (
a.entrypoint_metadata?.modifiedAt &&
b.entrypoint_metadata?.modifiedAt
) {
return !sortBy || sortBy.includes('desc')
? b.entrypoint_metadata.modifiedAt.getTime() -
a.entrypoint_metadata.modifiedAt.getTime()
: a.entrypoint_metadata.modifiedAt.getTime() -
b.entrypoint_metadata.modifiedAt.getTime()
}
return 0
}
if (sortBy?.includes('name')) {
return sortByName
} else {
return sortByModified
}
} }
return ( return (
@ -191,9 +166,9 @@ const Home = () => {
? 'text-chalkboard-80 dark:text-chalkboard-40' ? 'text-chalkboard-80 dark:text-chalkboard-40'
: '' : ''
} }
onClick={() => setSearchParams(getNextSearchParams('name'))} onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
icon={{ icon={{
icon: getSortIcon('name'), icon: getSortIcon(sort, 'name'),
bgClassName: !sort?.includes('name') bgClassName: !sort?.includes('name')
? 'bg-liquid-50 dark:bg-liquid-70' ? 'bg-liquid-50 dark:bg-liquid-70'
: '', : '',
@ -207,17 +182,19 @@ const Home = () => {
<ActionButton <ActionButton
Element="button" Element="button"
className={ className={
!modifiedSelected !isSortByModified
? 'text-chalkboard-80 dark:text-chalkboard-40' ? 'text-chalkboard-80 dark:text-chalkboard-40'
: '' : ''
} }
onClick={() => setSearchParams(getNextSearchParams('modified'))} onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified'))
}
icon={{ icon={{
icon: sort ? getSortIcon('modified') : faArrowDown, icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
bgClassName: !modifiedSelected bgClassName: !isSortByModified
? 'bg-liquid-50 dark:bg-liquid-70' ? 'bg-liquid-50 dark:bg-liquid-70'
: '', : '',
iconClassName: !modifiedSelected iconClassName: !isSortByModified
? 'text-liquid-80 dark:text-liquid-30' ? 'text-liquid-80 dark:text-liquid-30'
: '', : '',
}} }}
@ -230,11 +207,11 @@ const Home = () => {
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30"> <p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
Are being saved at{' '} Are being saved at{' '}
<code className="text-liquid-80 dark:text-liquid-30"> <code className="text-liquid-80 dark:text-liquid-30">
{defaultDir.dir} {defaultDirectory}
</code> </code>
, which you can change in your <Link to="settings">Settings</Link>. , which you can change in your <Link to="settings">Settings</Link>.
</p> </p>
{isLoading ? ( {state.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading> <Loading>Loading your Projects...</Loading>
) : ( ) : (
<> <>
@ -256,7 +233,7 @@ const Home = () => {
)} )}
<ActionButton <ActionButton
Element="button" Element="button"
onClick={handleNewProject} onClick={() => send('Create project')}
icon={{ icon: faPlus }} icon={{ icon: faPlus }}
> >
New file New file

View File

@ -20,9 +20,25 @@ export default function Units() {
> >
<h1 className="text-2xl font-bold">Camera</h1> <h1 className="text-2xl font-bold">Camera</h1>
<p className="mt-6"> <p className="mt-6">
Moving the camera is easy. Just click and drag anywhere in the scene Moving the camera is easy! The controls are as you might expect:
to rotate the camera, or hold down the <kbd>Ctrl</kbd> key and drag to </p>
pan the camera. <ul className="list-disc list-outside ms-8 mb-4">
<li>Click and drag anywhere in the scene to rotate the camera</li>
<li>
Hold down the <kbd>Shift</kbd> key while clicking and dragging to
pan the camera
</li>
<li>
Hold down the <kbd>Ctrl</kbd> key while dragging to zoom. You can
also use the scroll wheel to zoom in and out.
</li>
</ul>
<p>
What you're seeing here is just a video, and your interactions are
being sent to our Geometry Engine API, which sends back video frames
in real time. How cool is that? It means that you can use KittyCAD
Modeling App (or whatever you want to build) on any device, even a
cheap laptop with no graphics card!
</p> </p>
<div className="flex justify-between mt-6"> <div className="flex justify-between mt-6">
<ActionButton <ActionButton

View File

@ -1,34 +1,21 @@
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
import { baseUnits, useStore } from '../../useStore' import { BaseUnit, baseUnits } from '../../useStore'
import { ActionButton } from '../../components/ActionButton' import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings' import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle' import { Toggle } from '../../components/Toggle/Toggle'
import { useState } from 'react'
import { onboardingPaths, useDismiss, useNextClick } from '.' import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export default function Units() { export default function Units() {
const dismiss = useDismiss() const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA) const next = useNextClick(onboardingPaths.CAMERA)
const { const {
defaultUnitSystem: ogDefaultUnitSystem, settings: {
setDefaultUnitSystem: saveDefaultUnitSystem, send,
defaultBaseUnit: ogDefaultBaseUnit, context: { unitSystem, baseUnit },
setDefaultBaseUnit: saveDefaultBaseUnit, },
} = useStore((s) => ({ } = useGlobalStateContext()
defaultUnitSystem: s.defaultUnitSystem,
setDefaultUnitSystem: s.setDefaultUnitSystem,
defaultBaseUnit: s.defaultBaseUnit,
setDefaultBaseUnit: s.setDefaultBaseUnit,
}))
const [defaultUnitSystem, setDefaultUnitSystem] =
useState(ogDefaultUnitSystem)
const [defaultBaseUnit, setDefaultBaseUnit] = useState(ogDefaultBaseUnit)
function handleNextClick() {
saveDefaultUnitSystem(defaultUnitSystem)
saveDefaultBaseUnit(defaultBaseUnit)
next()
}
return ( return (
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50"> <div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
@ -42,10 +29,16 @@ export default function Units() {
offLabel="Imperial" offLabel="Imperial"
onLabel="Metric" onLabel="Metric"
name="settings-units" name="settings-units"
checked={defaultUnitSystem === 'metric'} checked={unitSystem === UnitSystem.Metric}
onChange={(e) => onChange={(e) => {
setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial') const newUnitSystem = e.target.checked
} ? UnitSystem.Metric
: UnitSystem.Imperial
send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
})
}}
/> />
</SettingsSection> </SettingsSection>
<SettingsSection <SettingsSection
@ -55,10 +48,15 @@ export default function Units() {
<select <select
id="base-unit" id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultBaseUnit} value={baseUnit}
onChange={(e) => setDefaultBaseUnit(e.target.value)} onChange={(e) => {
send({
type: 'Set Base Unit',
data: { baseUnit: e.target.value as BaseUnit },
})
}}
> >
{baseUnits[defaultUnitSystem].map((unit) => ( {baseUnits[unitSystem].map((unit) => (
<option key={unit} value={unit}> <option key={unit} value={unit}>
{unit} {unit}
</option> </option>
@ -81,7 +79,7 @@ export default function Units() {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
onClick={handleNextClick} onClick={next}
icon={{ icon: faArrowRight }} icon={{ icon: faArrowRight }}
> >
Next: Camera Next: Camera

View File

@ -1,13 +1,12 @@
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Outlet, useNavigate } from 'react-router-dom' import { Outlet, useNavigate } from 'react-router-dom'
import { useStore } from '../../useStore'
import Introduction from './Introduction' import Introduction from './Introduction'
import Units from './Units' import Units from './Units'
import Camera from './Camera' import Camera from './Camera'
import Sketching from './Sketching' import Sketching from './Sketching'
import { useCallback } from 'react' import { useCallback } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative' import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export const onboardingPaths = { export const onboardingPaths = {
INDEX: '/', INDEX: '/',
@ -36,29 +35,35 @@ export const onboardingRoutes = [
] ]
export function useNextClick(newStatus: string) { export function useNextClick(newStatus: string) {
const { setOnboardingStatus } = useStore((s) => ({ const {
setOnboardingStatus: s.setOnboardingStatus, settings: { send },
})) } = useGlobalStateContext()
const navigate = useNavigate() const navigate = useNavigate()
return useCallback(() => { return useCallback(() => {
setOnboardingStatus(newStatus) send({
type: 'Set Onboarding Status',
data: { onboardingStatus: newStatus },
})
navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus) navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus)
}, [newStatus, setOnboardingStatus, navigate]) }, [newStatus, send, navigate])
} }
export function useDismiss() { export function useDismiss() {
const { setOnboardingStatus } = useStore((s) => ({ const {
setOnboardingStatus: s.setOnboardingStatus, settings: { send },
})) } = useGlobalStateContext()
const navigate = useNavigate() const navigate = useNavigate()
return useCallback( return useCallback(
(path: string) => { (path: string) => {
setOnboardingStatus('dismissed') send({
type: 'Set Onboarding Status',
data: { onboardingStatus: 'dismissed' },
})
navigate(path) navigate(path)
}, },
[setOnboardingStatus, navigate] [send, navigate]
) )
} }

View File

@ -6,59 +6,48 @@ import {
import { ActionButton } from '../components/ActionButton' import { ActionButton } from '../components/ActionButton'
import { AppHeader } from '../components/AppHeader' import { AppHeader } from '../components/AppHeader'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import { Themes, baseUnits, useStore } from '../useStore' import { BaseUnit, baseUnits } from '../useStore'
import { useRef } from 'react'
import { toast } from 'react-hot-toast'
import { Toggle } from '../components/Toggle/Toggle' import { Toggle } from '../components/Toggle/Toggle'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { IndexLoaderData, paths } from '../Router' import { IndexLoaderData, paths } from '../Router'
import { Themes } from '../lib/theme'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export const Settings = () => { export const Settings = () => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
useHotkeys('esc', () => navigate('../')) useHotkeys('esc', () => navigate('../'))
const { const {
defaultDir, settings: {
setDefaultDir, send,
defaultProjectName, state: {
setDefaultProjectName, context: {
defaultUnitSystem, defaultProjectName,
setDefaultUnitSystem, showDebugPanel,
defaultBaseUnit, defaultDirectory,
setDefaultBaseUnit, unitSystem,
setDebugPanel, baseUnit,
debugPanel, theme,
setOnboardingStatus, },
theme, },
setTheme, },
} = useStore((s) => ({ } = useGlobalStateContext()
defaultDir: s.defaultDir,
setDefaultDir: s.setDefaultDir,
defaultProjectName: s.defaultProjectName,
setDefaultProjectName: s.setDefaultProjectName,
defaultUnitSystem: s.defaultUnitSystem,
setDefaultUnitSystem: s.setDefaultUnitSystem,
defaultBaseUnit: s.defaultBaseUnit,
setDefaultBaseUnit: s.setDefaultBaseUnit,
setDebugPanel: s.setDebugPanel,
debugPanel: s.debugPanel,
setOnboardingStatus: s.setOnboardingStatus,
theme: s.theme,
setTheme: s.setTheme,
}))
const ogDefaultDir = useRef(defaultDir)
const ogDefaultProjectName = useRef(defaultProjectName)
async function handleDirectorySelection() { async function handleDirectorySelection() {
const newDirectory = await open({ const newDirectory = await open({
directory: true, directory: true,
defaultPath: (defaultDir.base || '') + (defaultDir.dir || paths.INDEX), defaultPath: defaultDirectory || paths.INDEX,
title: 'Choose a new default directory', title: 'Choose a new default directory',
}) })
if (newDirectory && newDirectory !== null && !Array.isArray(newDirectory)) { if (newDirectory && newDirectory !== null && !Array.isArray(newDirectory)) {
setDefaultDir({ base: defaultDir.base, dir: newDirectory }) send({
type: 'Set Default Directory',
data: { defaultDirectory: newDirectory },
})
} }
} }
@ -102,18 +91,8 @@ export const Settings = () => {
<div className="w-full flex gap-4 p-1 rounded border border-chalkboard-30"> <div className="w-full flex gap-4 p-1 rounded border border-chalkboard-30">
<input <input
className="flex-1 px-2 bg-transparent" className="flex-1 px-2 bg-transparent"
value={defaultDir.dir} value={defaultDirectory}
onChange={(e) => { disabled
setDefaultDir({
base: defaultDir.base,
dir: e.target.value,
})
}}
onBlur={() => {
ogDefaultDir.current.dir !== defaultDir.dir &&
toast.success('Default directory updated')
ogDefaultDir.current.dir = defaultDir.dir
}}
/> />
<ActionButton <ActionButton
Element="button" Element="button"
@ -137,15 +116,15 @@ export const Settings = () => {
> >
<input <input
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultProjectName} defaultValue={defaultProjectName}
onChange={(e) => { onBlur={(e) => {
setDefaultProjectName(e.target.value) send({
}} type: 'Set Default Project Name',
onBlur={() => { data: { defaultProjectName: e.target.value },
ogDefaultProjectName.current !== defaultProjectName && })
toast.success('Default project name updated')
ogDefaultProjectName.current = defaultProjectName
}} }}
autoCapitalize="off"
autoComplete="off"
/> />
</SettingsSection> </SettingsSection>
</> </>
@ -158,12 +137,15 @@ export const Settings = () => {
offLabel="Imperial" offLabel="Imperial"
onLabel="Metric" onLabel="Metric"
name="settings-units" name="settings-units"
checked={defaultUnitSystem === 'metric'} checked={unitSystem === UnitSystem.Metric}
onChange={(e) => { onChange={(e) => {
const newUnitSystem = e.target.checked ? 'metric' : 'imperial' const newUnitSystem = e.target.checked
setDefaultUnitSystem(newUnitSystem) ? UnitSystem.Metric
setDefaultBaseUnit(baseUnits[newUnitSystem][0]) : UnitSystem.Imperial
toast.success('Unit system set to ' + newUnitSystem) send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
})
}} }}
/> />
</SettingsSection> </SettingsSection>
@ -174,13 +156,15 @@ export const Settings = () => {
<select <select
id="base-unit" id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultBaseUnit} value={baseUnit}
onChange={(e) => { onChange={(e) => {
setDefaultBaseUnit(e.target.value) send({
toast.success('Base unit changed to ' + e.target.value) type: 'Set Base Unit',
data: { baseUnit: e.target.value as BaseUnit },
})
}} }}
> >
{baseUnits[defaultUnitSystem].map((unit) => ( {baseUnits[unitSystem as keyof typeof baseUnits].map((unit) => (
<option key={unit} value={unit}> <option key={unit} value={unit}>
{unit} {unit}
</option> </option>
@ -193,12 +177,9 @@ export const Settings = () => {
> >
<Toggle <Toggle
name="settings-debug-panel" name="settings-debug-panel"
checked={debugPanel} checked={showDebugPanel}
onChange={(e) => { onChange={(e) => {
setDebugPanel(e.target.checked) send('Toggle Debug Panel')
toast.success(
'Debug panel toggled ' + (e.target.checked ? 'on' : 'off')
)
}} }}
/> />
</SettingsSection> </SettingsSection>
@ -211,12 +192,10 @@ export const Settings = () => {
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={theme} value={theme}
onChange={(e) => { onChange={(e) => {
setTheme(e.target.value as Themes) send({
toast.success( type: 'Set Theme',
'Theme changed to ' + data: { theme: e.target.value as Themes },
e.target.value.slice(0, 1).toLocaleUpperCase() + })
e.target.value.slice(1)
)
}} }}
> >
{Object.entries(Themes).map(([label, value]) => ( {Object.entries(Themes).map(([label, value]) => (
@ -226,21 +205,26 @@ export const Settings = () => {
))} ))}
</select> </select>
</SettingsSection> </SettingsSection>
<SettingsSection {location.pathname.includes(paths.FILE) && (
title="Onboarding" <SettingsSection
description="Replay the onboarding process" title="Onboarding"
> description="Replay the onboarding process"
<ActionButton
Element="button"
onClick={() => {
setOnboardingStatus('')
navigate('..' + paths.ONBOARDING.INDEX)
}}
icon={{ icon: faArrowRotateBack }}
> >
Replay Onboarding <ActionButton
</ActionButton> Element="button"
</SettingsSection> onClick={() => {
send({
type: 'Set Onboarding Status',
data: { onboardingStatus: '' },
})
navigate('..' + paths.ONBOARDING.INDEX)
}}
icon={{ icon: faArrowRotateBack }}
>
Replay Onboarding
</ActionButton>
</SettingsSection>
)}
</div> </div>
</div> </div>
) )

View File

@ -1,20 +1,22 @@
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons' import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from '../components/ActionButton' import { ActionButton } from '../components/ActionButton'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Themes, useStore } from '../useStore'
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import { useNavigate } from 'react-router-dom'
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
import { getSystemTheme } from '../lib/getSystemTheme' import { Themes, getSystemTheme } from '../lib/theme'
import { paths } from '../Router' import { paths } from '../Router'
import { useAuthMachine } from '../hooks/useAuthMachine' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
const SignIn = () => { const SignIn = () => {
const navigate = useNavigate() const {
const { theme } = useStore((s) => ({ auth: { send },
theme: s.theme, settings: {
})) state: {
const [_, send] = useAuthMachine() context: { theme },
},
},
} = useGlobalStateContext()
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
const signInTauri = async () => { const signInTauri = async () => {
// We want to invoke our command to login via device auth. // We want to invoke our command to login via device auth.
@ -22,7 +24,7 @@ const SignIn = () => {
const token: string = await invoke('login', { const token: string = await invoke('login', {
host: VITE_KC_API_BASE_URL, host: VITE_KC_API_BASE_URL,
}) })
send({ type: 'tryLogin', token }) send({ type: 'Log in', token })
} catch (error) { } catch (error) {
console.error('login button', error) console.error('login button', error)
} }

View File

@ -13,7 +13,6 @@ import {
} from './lang/executor' } from './lang/executor'
import { recast } from './lang/recast' import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
import { BaseDirectory } from '@tauri-apps/api/fs'
import { import {
ArtifactMap, ArtifactMap,
SourceRangeMap, SourceRangeMap,
@ -95,22 +94,14 @@ export type GuiModes =
position: Position position: Position
} }
type UnitSystem = 'imperial' | 'metric' export const baseUnits = {
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export const baseUnits: Record<UnitSystem, string[]> = {
imperial: ['in', 'ft'], imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'], metric: ['mm', 'cm', 'm'],
} } as const
interface DefaultDir { export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
base?: BaseDirectory
dir: string export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
}
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs' export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
@ -181,21 +172,8 @@ export interface StoreState {
streamHeight: number streamHeight: number
}) => void }) => void
// tauri specific app settings
defaultDir: DefaultDir
setDefaultDir: (dir: DefaultDir) => void
defaultProjectName: string
setDefaultProjectName: (defaultProjectName: string) => void
defaultUnitSystem: UnitSystem
setDefaultUnitSystem: (defaultUnitSystem: UnitSystem) => void
defaultBaseUnit: string
setDefaultBaseUnit: (defaultBaseUnit: string) => void
showHomeMenu: boolean showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void setHomeShowMenu: (showMenu: boolean) => void
onboardingStatus: string
setOnboardingStatus: (status: string) => void
theme: Themes
setTheme: (theme: Themes) => void
isBannerDismissed: boolean isBannerDismissed: boolean
setBannerDismissed: (isBannerDismissed: boolean) => void setBannerDismissed: (isBannerDismissed: boolean) => void
openPanes: PaneType[] openPanes: PaneType[]
@ -205,8 +183,6 @@ export interface StoreState {
path: string path: string
}[] }[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void setHomeMenuItems: (items: { name: string; path: string }[]) => void
debugPanel: boolean
setDebugPanel: (debugPanel: boolean) => void
} }
let pendingAstUpdates: number[] = [] let pendingAstUpdates: number[] = []
@ -385,18 +361,6 @@ export const useStore = create<StoreState>()(
defaultDir: { defaultDir: {
dir: '', dir: '',
}, },
setDefaultDir: (dir) => set({ defaultDir: dir }),
defaultProjectName: 'new-project-$nnn',
setDefaultProjectName: (defaultProjectName) =>
set({ defaultProjectName }),
defaultUnitSystem: 'imperial',
setDefaultUnitSystem: (defaultUnitSystem) => set({ defaultUnitSystem }),
defaultBaseUnit: 'in',
setDefaultBaseUnit: (defaultBaseUnit) => set({ defaultBaseUnit }),
onboardingStatus: '',
setOnboardingStatus: (onboardingStatus) => set({ onboardingStatus }),
theme: Themes.System,
setTheme: (theme) => set({ theme }),
isBannerDismissed: false, isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }), setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'], openPanes: ['code'],
@ -405,25 +369,13 @@ export const useStore = create<StoreState>()(
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }), setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [], homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }), setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
debugPanel: false,
setDebugPanel: (debugPanel) => set({ debugPanel }),
}), }),
{ {
name: 'store', name: 'store',
partialize: (state) => partialize: (state) =>
Object.fromEntries( Object.fromEntries(
Object.entries(state).filter(([key]) => Object.entries(state).filter(([key]) =>
[ ['code', 'openPanes'].includes(key)
'code',
'defaultDir',
'defaultProjectName',
'defaultUnitSystem',
'defaultBaseUnit',
'debugPanel',
'onboardingStatus',
'theme',
'openPanes',
].includes(key)
) )
), ),
} }

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

@ -27,6 +27,18 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.0.4" version = "1.0.4"
@ -109,6 +121,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.2" version = "0.21.2"
@ -147,6 +165,18 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -156,6 +186,28 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bson"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58da0ae1e701ea752cc46c1bb9f39d5ecefc7395c3ecd526261a566d4f16e0c2"
dependencies = [
"ahash",
"base64 0.13.1",
"bitvec",
"chrono",
"hex",
"indexmap 1.9.3",
"js-sys",
"once_cell",
"rand",
"serde",
"serde_bytes",
"serde_json",
"time 0.3.27",
"uuid",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.13.0" version = "3.13.0"
@ -281,6 +333,16 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.4" version = "0.8.4"
@ -352,6 +414,20 @@ dependencies = [
"syn 2.0.29", "syn 2.0.29",
] ]
[[package]]
name = "derive-docs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "075291fd1d6d70a886078f7b1c132a160559ceb9a0fe143177872d40ea587906"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"serde",
"serde_tokenstream",
"syn 2.0.29",
]
[[package]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@ -488,6 +564,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.28" version = "0.3.28"
@ -606,28 +688,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "gloo-events"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
dependencies = [
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.2.0" version = "0.2.0"
@ -693,6 +753,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.5" version = "0.5.5"
@ -881,13 +947,41 @@ dependencies = [
] ]
[[package]] [[package]]
name = "kittycad" name = "kcl-lib"
version = "0.2.19" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0443a9f76cee80d5a43d076028d3ce39d2f6f6b66fc5c1a0ce24f8d7caf733b9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "bson",
"derive-docs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate",
"futures",
"js-sys",
"kittycad",
"lazy_static",
"parse-display",
"pretty_assertions",
"regex",
"reqwest",
"schemars",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-tungstenite",
"ts-rs-json-value",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
]
[[package]]
name = "kittycad"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
dependencies = [
"anyhow",
"base64 0.21.2",
"bytes", "bytes",
"chrono", "chrono",
"data-encoding", "data-encoding",
@ -896,6 +990,7 @@ dependencies = [
"phonenumber", "phonenumber",
"schemars", "schemars",
"serde", "serde",
"serde_bytes",
"serde_json", "serde_json",
"thiserror", "thiserror",
"url", "url",
@ -1151,6 +1246,12 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.5.1" version = "6.5.1"
@ -1244,9 +1345,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1321,6 +1422,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1433,7 +1540,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [ dependencies = [
"base64", "base64 0.21.2",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -1519,9 +1626,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.8" version = "0.38.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
@ -1542,13 +1649,25 @@ dependencies = [
"sct", "sct",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.3" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [ dependencies = [
"base64", "base64 0.21.2",
] ]
[[package]] [[package]]
@ -1583,10 +1702,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "schemars" name = "schannel"
version = "0.8.12" version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "schemars"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
dependencies = [ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
@ -1601,9 +1729,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "0.8.12" version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1627,6 +1755,29 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "0.11.0" version = "0.11.0"
@ -1647,18 +1798,27 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.186" version = "1.0.187"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" checksum = "30a7fe14252655bd1e578af19f5fa00fe02fd0013b100ca6b49fde31c41bae4c"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_bytes"
version = "1.0.186" version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.187"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46b2a6ca578b3f1d4501b12f78ed4692006d79d82a1a7c561c12dbc3d625eb8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1682,6 +1842,7 @@ version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [ dependencies = [
"indexmap 2.0.0",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
@ -1925,6 +2086,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "task-local-extensions" name = "task-local-extensions"
version = "0.1.4" version = "0.1.4"
@ -2110,7 +2277,10 @@ checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"rustls",
"rustls-native-certs",
"tokio", "tokio",
"tokio-rustls",
"tungstenite", "tungstenite",
] ]
@ -2192,9 +2362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs-json-value"
version = "7.0.0" version = "7.0.0"
source = "git+https://github.com/kittycad/ts-rs.git?branch=serde_json#94e2a19c41194e47009fafc7b5a2c28ae544a6e8" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66d07e64e1e39d693819307757ad16878ff2be1f26d6fc2137c4e23bc0c0545"
dependencies = [ dependencies = [
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -2205,7 +2376,8 @@ dependencies = [
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "7.0.0" version = "7.0.0"
source = "git+https://github.com/kittycad/ts-rs.git?branch=serde_json#94e2a19c41194e47009fafc7b5a2c28ae544a6e8" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
@ -2227,6 +2399,7 @@ dependencies = [
"httparse", "httparse",
"log", "log",
"rand", "rand",
"rustls",
"sha1", "sha1",
"thiserror", "thiserror",
"url", "url",
@ -2435,30 +2608,11 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
name = "wasm-lib" name = "wasm-lib"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "bson",
"backtrace",
"bincode",
"derive-docs",
"expectorate",
"futures",
"gloo-file",
"gloo-utils", "gloo-utils",
"http", "kcl-lib",
"httparse",
"js-sys",
"kittycad", "kittycad",
"lazy_static",
"parse-display",
"pretty_assertions",
"regex",
"schemars",
"serde",
"serde_json", "serde_json",
"thiserror",
"tokio",
"tokio-tungstenite",
"ts-rs",
"uuid",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
] ]
@ -2661,6 +2815,15 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View File

@ -8,28 +8,11 @@ edition = "2021"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
anyhow = "1.0.75" bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
backtrace = "0.3"
bincode = "1.3.3"
derive-docs = { path = "derive-docs" }
futures = { version = "0.3.28", optional = true }
gloo-file = { version = "0.3.0", optional = true }
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
http = "0.2.9" kcl-lib = { path = "kcl" }
httparse = { version = "1.8.0", optional = true } kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
js-sys = { version = "0.3.64", optional = true }
kittycad = { version = "0.2.15", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
schemars = { version = "0.8", features = ["url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93" serde_json = "1.0.93"
thiserror = "1.0.47"
tokio = { version = "1.32.0", features = ["full"], optional = true }
tokio-tungstenite = { version = "0.20.0", optional = true }
ts-rs = { git = "https://github.com/kittycad/ts-rs.git", branch = "serde_json", features = ["serde-json-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87" wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37" wasm-bindgen-futures = "0.4.37"
@ -37,17 +20,8 @@ wasm-bindgen-futures = "0.4.37"
panic = "abort" panic = "abort"
debug = true debug = true
[dev-dependencies]
expectorate = "1.0.7"
pretty_assertions = "1.4.0"
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
[features]
default = ["web"]
web = ["dep:gloo-file", "dep:js-sys"]
noweb = ["dep:futures", "dep:httparse", "dep:tokio", "dep:tokio-tungstenite"]
[workspace] [workspace]
members = [ members = [
"derive-docs" "derive-docs",
"kcl"
] ]

View File

@ -3,6 +3,7 @@ name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -35,10 +35,7 @@ struct StdlibMetadata {
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn stdlib( pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into())) do_output(do_stdlib(attr.into(), item.into()))
} }
@ -50,9 +47,7 @@ fn do_stdlib(
do_stdlib_inner(metadata, attr, item) do_stdlib_inner(metadata, attr, item)
} }
fn do_output( fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>,
) -> proc_macro::TokenStream {
match res { match res {
Err(err) => err.to_compile_error().into(), Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => { Ok((stdlib_docs, errors)) => {
@ -207,11 +202,7 @@ fn do_stdlib_inner(
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(), syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
}; };
let ty_string = ty let ty_string = ty.to_string().replace('&', "").replace("mut", "").replace(' ', "");
.to_string()
.replace('&', "")
.replace("mut", "")
.replace(' ', "");
let ty_string = ty_string.trim().to_string(); let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") { let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>'); let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
@ -370,8 +361,7 @@ fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> (Option<String>, Option<S
if let syn::Meta::NameValue(nv) = &attr.meta { if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) { if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit { if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s), lit: syn::Lit::Str(s), ..
..
}) = &nv.value }) = &nv.value
{ {
return normalize_comment_string(s.value()); return normalize_comment_string(s.value());
@ -508,10 +498,7 @@ mod tests {
let _expected = quote! {}; let _expected = quote! {};
assert!(errors.is_empty()); assert!(errors.is_empty());
expectorate::assert_contents( expectorate::assert_contents("tests/lineTo.gen", &openapitor::types::get_text_fmt(&item).unwrap());
"tests/lineTo.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
} }
#[test] #[test]
@ -540,9 +527,6 @@ mod tests {
let _expected = quote! {}; let _expected = quote! {};
assert!(errors.is_empty()); assert!(errors.is_empty());
expectorate::assert_contents( expectorate::assert_contents("tests/min.gen", &openapitor::types::get_text_fmt(&item).unwrap());
"tests/min.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
} }
} }

View File

@ -0,0 +1,47 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language"
version = "0.1.10"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
derive-docs = { version = "0.1.0" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
lazy_static = "1.4.0"
parse-display = "0.8.2"
regex = "1.7.1"
schemars = { version = "0.8", features = ["url", "uuid1"] }
serde = {version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.47"
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "uuid-impl"] }
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.64" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
futures = { version = "0.3.28" }
reqwest = { version = "0.11.20", default-features = false }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
[features]
default = ["engine"]
engine = []
[profile.release]
panic = "abort"
debug = true
[dev-dependencies]
expectorate = "1.0.7"
pretty_assertions = "1.4.0"
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }

View File

@ -2,6 +2,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Map; use serde_json::Map;
@ -56,7 +57,7 @@ pub trait ValueMeta {
macro_rules! impl_value_meta { macro_rules! impl_value_meta {
{$name:ident} => { {$name:ident} => {
impl ValueMeta for $name { impl crate::abstract_syntax_tree_types::ValueMeta for $name {
fn start(&self) -> usize { fn start(&self) -> usize {
self.start self.start
} }
@ -86,6 +87,8 @@ macro_rules! impl_value_meta {
}; };
} }
pub(crate) use impl_value_meta;
impl Value { impl Value {
pub fn start(&self) -> usize { pub fn start(&self) -> usize {
match self { match self {
@ -192,16 +195,11 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => { BinaryPart::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine) binary_expression.get_result(memory, pipe_info, stdlib, engine)
} }
BinaryPart::CallExpression(call_expression) => { BinaryPart::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, stdlib, engine),
call_expression.execute(memory, pipe_info, stdlib, engine)
}
BinaryPart::UnaryExpression(unary_expression) => { BinaryPart::UnaryExpression(unary_expression) => {
// Return an error this should not happen. // Return an error this should not happen.
Err(KclError::Semantic(KclErrorDetails { Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("UnaryExpression should not be a BinaryPart: {:?}", unary_expression),
"UnaryExpression should not be a BinaryPart: {:?}",
unary_expression
),
source_ranges: vec![unary_expression.into()], source_ranges: vec![unary_expression.into()],
})) }))
} }
@ -313,10 +311,7 @@ impl CallExpression {
} }
Value::PipeExpression(pipe_expression) => { Value::PipeExpression(pipe_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("PipeExpression not implemented here: {:?}", pipe_expression),
"PipeExpression not implemented here: {:?}",
pipe_expression
),
source_ranges: vec![pipe_expression.into()], source_ranges: vec![pipe_expression.into()],
})); }));
} }
@ -325,29 +320,20 @@ impl CallExpression {
.get(&pipe_info.index - 1) .get(&pipe_info.index - 1)
.ok_or_else(|| { .ok_or_else(|| {
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: format!( message: format!("PipeSubstitution index out of bounds: {:?}", pipe_info),
"PipeSubstitution index out of bounds: {:?}",
pipe_info
),
source_ranges: vec![pipe_substitution.into()], source_ranges: vec![pipe_substitution.into()],
}) })
})? })?
.clone(), .clone(),
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("MemberExpression not implemented here: {:?}", member_expression),
"MemberExpression not implemented here: {:?}",
member_expression
),
source_ranges: vec![member_expression.into()], source_ranges: vec![member_expression.into()],
})); }));
} }
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("FunctionExpression not implemented here: {:?}", function_expression),
"FunctionExpression not implemented here: {:?}",
function_expression
),
source_ranges: vec![function_expression.into()], source_ranges: vec![function_expression.into()],
})); }));
} }
@ -363,14 +349,7 @@ impl CallExpression {
if pipe_info.is_in_pipe { if pipe_info.is_in_pipe {
pipe_info.index += 1; pipe_info.index += 1;
pipe_info.previous_results.push(result); pipe_info.previous_results.push(result);
execute_pipe_body( execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
} else { } else {
Ok(result) Ok(result)
} }
@ -390,14 +369,7 @@ impl CallExpression {
pipe_info.index += 1; pipe_info.index += 1;
pipe_info.previous_results.push(result); pipe_info.previous_results.push(result);
execute_pipe_body( execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
} else { } else {
Ok(result) Ok(result)
} }
@ -412,11 +384,22 @@ pub struct VariableDeclaration {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
pub declarations: Vec<VariableDeclarator>, pub declarations: Vec<VariableDeclarator>,
pub kind: String, // Change to enum if there are specific values pub kind: VariableKind, // Change to enum if there are specific values
} }
impl_value_meta!(VariableDeclaration); impl_value_meta!(VariableDeclaration);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum VariableKind {
Let,
Const,
Fn,
Var,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -533,28 +516,19 @@ impl ArrayExpression {
} }
Value::PipeSubstitution(pipe_substitution) => { Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
source_ranges: vec![pipe_substitution.into()], source_ranges: vec![pipe_substitution.into()],
})); }));
} }
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("MemberExpression not implemented here: {:?}", member_expression),
"MemberExpression not implemented here: {:?}",
member_expression
),
source_ranges: vec![member_expression.into()], source_ranges: vec![member_expression.into()],
})); }));
} }
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("FunctionExpression not implemented here: {:?}", function_expression),
"FunctionExpression not implemented here: {:?}",
function_expression
),
source_ranges: vec![function_expression.into()], source_ranges: vec![function_expression.into()],
})); }));
} }
@ -619,28 +593,19 @@ impl ObjectExpression {
} }
Value::PipeSubstitution(pipe_substitution) => { Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
source_ranges: vec![pipe_substitution.into()], source_ranges: vec![pipe_substitution.into()],
})); }));
} }
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("MemberExpression not implemented here: {:?}", member_expression),
"MemberExpression not implemented here: {:?}",
member_expression
),
source_ranges: vec![member_expression.into()], source_ranges: vec![member_expression.into()],
})); }));
} }
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("FunctionExpression not implemented here: {:?}", function_expression),
"FunctionExpression not implemented here: {:?}",
function_expression
),
source_ranges: vec![function_expression.into()], source_ranges: vec![function_expression.into()],
})); }));
} }
@ -712,10 +677,7 @@ impl MemberExpression {
string string
} else { } else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!( message: format!("Expected string literal for property name, found {:?}", value),
"Expected string literal for property name, found {:?}",
value
),
source_ranges: vec![literal.into()], source_ranges: vec![literal.into()],
})); }));
} }
@ -837,10 +799,7 @@ impl BinaryExpression {
} }
} }
pub fn parse_json_number_as_f64( pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
j: &serde_json::Value,
source_range: SourceRange,
) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j { if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| { n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails { KclError::Syntax(KclErrorDetails {

View File

@ -44,6 +44,11 @@ impl StdLibFnArg {
get_type_string_from_schema(&self.schema) get_type_string_from_schema(&self.schema)
} }
#[allow(dead_code)]
pub fn get_autocomplete_string(&self) -> Result<String> {
get_autocomplete_string_from_schema(&self.schema)
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn description(&self) -> Option<String> { pub fn description(&self) -> Option<String> {
get_description_string_from_schema(&self.schema) get_description_string_from_schema(&self.schema)
@ -93,9 +98,24 @@ pub trait StdLibFn {
deprecated: self.deprecated(), deprecated: self.deprecated(),
}) })
} }
fn fn_signature(&self) -> String {
let mut signature = String::new();
signature.push_str(&format!("{}(", self.name()));
for (i, arg) in self.args().iter().enumerate() {
if i > 0 {
signature.push_str(", ");
}
signature.push_str(&format!("{}: {}", arg.name, arg.type_));
}
signature.push_str(") -> ");
signature.push_str(&self.return_value().type_);
signature
}
} }
fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> { pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
if let schemars::schema::Schema::Object(o) = schema { if let schemars::schema::Schema::Object(o) = schema {
if let Some(metadata) = &o.metadata { if let Some(metadata) = &o.metadata {
if let Some(description) = &metadata.description { if let Some(description) = &metadata.description {
@ -107,7 +127,7 @@ fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Opti
None None
} }
fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> { pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
match schema { match schema {
schemars::schema::Schema::Object(o) => { schemars::schema::Schema::Object(o) => {
if let Some(format) = &o.format { if let Some(format) = &o.format {
@ -147,15 +167,9 @@ fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(Str
if let Some(array_val) = &o.array { if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items { if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties. // Let's print out the object's properties.
return Ok(( return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
} else if let Some(items) = &array_val.contains { } else if let Some(items) = &array_val.contains {
return Ok(( return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
} }
} }
@ -193,3 +207,78 @@ fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(Str
schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)), schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)),
} }
} }
pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) -> Result<String> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(format) = &o.format {
if format == "uuid" {
return Ok(Primitive::Uuid.to_string());
} else if format == "double" || format == "uint" {
return Ok(Primitive::Number.to_string());
} else {
anyhow::bail!("unknown format: {}", format);
}
}
if let Some(obj_val) = &o.object {
let mut fn_docs = String::new();
fn_docs.push_str("{\n");
// Let's print out the object's properties.
for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') {
continue;
}
if let Some(description) = get_description_string_from_schema(prop) {
fn_docs.push_str(&format!("\t// {}\n", description));
}
fn_docs.push_str(&format!(
"\t\"{}\": {},\n",
prop_name,
get_autocomplete_string_from_schema(prop)?,
));
}
fn_docs.push('}');
return Ok(fn_docs);
}
if let Some(array_val) = &o.array {
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties.
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
} else if let Some(items) = &array_val.contains {
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
}
}
if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new();
if let Some(items) = &subschemas.one_of {
if let Some(item) = items.iter().next() {
// Let's print out the object's properties.
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
}
} else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() {
// Let's print out the object's properties.
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
}
} else {
anyhow::bail!("unknown subschemas: {:#?}", subschemas);
}
return Ok(fn_docs);
}
if let Some(schemars::schema::SingleOrVec::Single(_string)) = &o.instance_type {
return Ok(Primitive::String.to_string());
}
anyhow::bail!("unknown type: {:#?}", o)
}
schemars::schema::Schema::Bool(_) => Ok(Primitive::Bool.to_string()),
}
}

View File

@ -0,0 +1,175 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use std::sync::Arc;
use anyhow::Result;
use futures::{SinkExt, StreamExt};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use crate::errors::{KclError, KclErrorDetails};
#[derive(Debug)]
pub struct EngineConnection {
tcp_write: futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>,
tcp_read_handle: tokio::task::JoinHandle<Result<()>>,
export_notifier: Arc<tokio::sync::Notify>,
snapshot_notifier: Arc<tokio::sync::Notify>,
}
impl Drop for EngineConnection {
fn drop(&mut self) {
// Drop the read handle.
self.tcp_read_handle.abort();
}
}
pub struct TcpRead {
stream: futures::stream::SplitStream<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>>,
}
impl TcpRead {
pub async fn read(&mut self) -> Result<WebSocketResponse> {
let msg = self.stream.next().await.unwrap()?;
let msg: WebSocketResponse = match msg {
WsMsg::Text(text) => serde_json::from_str(&text)?,
WsMsg::Binary(bin) => bson::from_slice(&bin)?,
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
};
Ok(msg)
}
}
impl EngineConnection {
pub async fn new(ws: reqwest::Upgraded, export_dir: &str, snapshot_file: &str) -> Result<EngineConnection> {
// Make sure the export directory exists and that it is a directory.
let export_dir = std::path::Path::new(export_dir).to_owned();
if !export_dir.exists() {
anyhow::bail!("Export directory does not exist: {}", export_dir.display());
}
// Make sure it is a directory.
if !export_dir.is_dir() {
anyhow::bail!("Export directory is not a directory: {}", export_dir.display());
}
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws,
tokio_tungstenite::tungstenite::protocol::Role::Client,
None,
)
.await;
let (tcp_write, tcp_read) = ws_stream.split();
let mut tcp_read = TcpRead { stream: tcp_read };
let export_notifier = Arc::new(tokio::sync::Notify::new());
let export_notifier_clone = export_notifier.clone();
let snapshot_notifier = Arc::new(tokio::sync::Notify::new());
let snapshot_notifier_clone = snapshot_notifier.clone();
let snapshot_file = snapshot_file.to_owned();
let tcp_read_handle = tokio::spawn(async move {
// Get Websocket messages from API server
loop {
match tcp_read.read().await {
Ok(ws_resp) => {
if let Some(success) = ws_resp.success {
if !success {
println!("got ws errors: {:?}", ws_resp.errors);
export_notifier.notify_one();
continue;
}
}
if let Some(msg) = ws_resp.resp {
match msg {
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
println!("got ice server info: {:?}", ice_servers);
}
OkWebSocketResponseData::SdpAnswer { answer } => {
println!("got sdp answer: {:?}", answer);
}
OkWebSocketResponseData::TrickleIce { candidate } => {
println!("got trickle ice: {:?}", candidate);
}
OkWebSocketResponseData::Modeling { modeling_response } => {
if let kittycad::types::OkModelingCmdResponse::TakeSnapshot { data } =
modeling_response
{
if snapshot_file.is_empty() {
println!("Got snapshot, but no snapshot file specified.");
continue;
}
// Save the snapshot locally.
std::fs::write(&snapshot_file, data.contents)?;
snapshot_notifier.notify_one();
}
}
OkWebSocketResponseData::Export { files } => {
// Save the files to our export directory.
for file in files {
let path = export_dir.join(file.name);
std::fs::write(&path, file.contents)?;
println!("Wrote file: {}", path.display());
}
// Tell the export notifier that we have new files.
export_notifier.notify_one();
}
}
}
}
Err(e) => {
println!("got ws error: {:?}", e);
export_notifier.notify_one();
continue;
}
}
}
});
Ok(EngineConnection {
tcp_write,
tcp_read_handle,
export_notifier: export_notifier_clone,
snapshot_notifier: snapshot_notifier_clone,
})
}
pub async fn wait_for_export(&self) {
self.export_notifier.notified().await;
}
pub async fn wait_for_snapshot(&self) {
self.snapshot_notifier.notified().await;
}
pub async fn tcp_send(&mut self, msg: WebSocketRequest) -> Result<()> {
let msg = serde_json::to_string(&msg)?;
self.tcp_write.send(WsMsg::Text(msg)).await?;
Ok(())
}
pub fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
futures::executor::block_on(self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id })).map_err(
|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
},
)?;
Ok(())
}
}

View File

@ -9,11 +9,7 @@ use crate::errors::KclError;
pub struct EngineConnection {} pub struct EngineConnection {}
impl EngineConnection { impl EngineConnection {
pub async fn new( pub async fn new() -> Result<EngineConnection> {
_conn_str: &str,
_auth_token: &str,
_origin: &str,
) -> Result<EngineConnection> {
Ok(EngineConnection {}) Ok(EngineConnection {})
} }

View File

@ -7,7 +7,7 @@ use wasm_bindgen::prelude::*;
use crate::errors::{KclError, KclErrorDetails}; use crate::errors::{KclError, KclErrorDetails};
#[wasm_bindgen(module = "/../lang/std/engineConnection.ts")] #[wasm_bindgen(module = "/../../lang/std/engineConnection.ts")]
extern "C" { extern "C" {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub type EngineCommandManager; pub type EngineCommandManager;

View File

@ -2,25 +2,36 @@
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[cfg(feature = "noweb")] #[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))] #[cfg(not(test))]
pub mod conn_noweb; #[cfg(feature = "engine")]
#[cfg(feature = "noweb")] pub mod conn;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))] #[cfg(not(test))]
pub use conn_noweb::EngineConnection; #[cfg(feature = "engine")]
pub use conn::EngineConnection;
#[cfg(feature = "web")] #[cfg(target_arch = "wasm32")]
#[cfg(not(test))] #[cfg(not(test))]
pub mod conn_web; #[cfg(feature = "engine")]
#[cfg(feature = "web")] pub mod conn_wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))] #[cfg(not(test))]
pub use conn_web::EngineConnection; #[cfg(feature = "engine")]
pub use conn_wasm::EngineConnection;
#[cfg(test)] #[cfg(test)]
pub mod conn_mock; pub mod conn_mock;
#[cfg(test)] #[cfg(test)]
pub use conn_mock::EngineConnection; pub use conn_mock::EngineConnection;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub mod conn_mock;
#[cfg(not(feature = "engine"))]
#[cfg(not(test))]
pub use conn_mock::EngineConnection;
use crate::executor::SourceRange; use crate::executor::SourceRange;
#[derive(Debug)] #[derive(Debug)]
@ -31,27 +42,17 @@ pub struct EngineManager {
#[wasm_bindgen] #[wasm_bindgen]
impl EngineManager { impl EngineManager {
#[cfg(feature = "web")] #[cfg(target_arch = "wasm32")]
#[cfg(not(test))] #[cfg(not(test))]
#[cfg(feature = "engine")]
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub async fn new(manager: conn_web::EngineCommandManager) -> EngineManager { pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
EngineManager { EngineManager {
// This unwrap is safe because the connection is always created. // This unwrap is safe because the connection is always created.
connection: EngineConnection::new(manager).await.unwrap(), connection: EngineConnection::new(manager).await.unwrap(),
} }
} }
#[cfg(not(feature = "web"))]
#[wasm_bindgen(constructor)]
pub async fn new(conn_str: &str, auth_token: &str, origin: &str) -> EngineManager {
EngineManager {
// TODO: fix unwrap.
connection: EngineConnection::new(conn_str, auth_token, origin)
.await
.unwrap(),
}
}
pub fn send_modeling_cmd(&mut self, id_str: &str, cmd_str: &str) -> Result<(), String> { pub fn send_modeling_cmd(&mut self, id_str: &str, cmd_str: &str) -> Result<(), String> {
let id = uuid::Uuid::parse_str(id_str).map_err(|e| e.to_string())?; let id = uuid::Uuid::parse_str(id_str).map_err(|e| e.to_string())?;
let cmd = serde_json::from_str(cmd_str).map_err(|e| e.to_string())?; let cmd = serde_json::from_str(cmd_str).map_err(|e| e.to_string())?;

View File

@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError {
#[error("syntax: {0:?}")]
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
Unimplemented(KclErrorDetails),
#[error("unexpected: {0:?}")]
Unexpected(KclErrorDetails),
#[error("value already defined: {0:?}")]
ValueAlreadyDefined(KclErrorDetails),
#[error("undefined value: {0:?}")]
UndefinedValue(KclErrorDetails),
#[error("invalid expression: {0:?}")]
InvalidExpression(KclErrorDetails),
#[error("engine: {0:?}")]
Engine(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
pub source_ranges: Vec<crate::executor::SourceRange>,
#[serde(rename = "msg")]
pub message: String,
}
impl KclError {
/// Get the error message, line and column from the error and input code.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
let (type_, source_range, message) = match &self {
KclError::Syntax(e) => ("syntax", e.source_ranges.clone(), e.message.clone()),
KclError::Semantic(e) => ("semantic", e.source_ranges.clone(), e.message.clone()),
KclError::Type(e) => ("type", e.source_ranges.clone(), e.message.clone()),
KclError::Unimplemented(e) => ("unimplemented", e.source_ranges.clone(), e.message.clone()),
KclError::Unexpected(e) => ("unexpected", e.source_ranges.clone(), e.message.clone()),
KclError::ValueAlreadyDefined(e) => ("value already defined", e.source_ranges.clone(), e.message.clone()),
KclError::UndefinedValue(e) => ("undefined value", e.source_ranges.clone(), e.message.clone()),
KclError::InvalidExpression(e) => ("invalid expression", e.source_ranges.clone(), e.message.clone()),
KclError::Engine(e) => ("engine", e.source_ranges.clone(), e.message.clone()),
};
// Calculate the line and column of the error from the source range.
let (line, column) = if let Some(range) = source_range.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!("{}: {}", type_, message), line, column)
}
}
/// 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.
impl From<KclError> for String {
fn from(error: KclError) -> Self {
serde_json::to_string(&error).unwrap()
}
}
impl From<String> for KclError {
fn from(error: String) -> Self {
serde_json::from_str(&error).unwrap()
}
}

View File

@ -6,9 +6,6 @@ use anyhow::Result;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(test))]
use wasm_bindgen::prelude::*;
use crate::{ use crate::{
abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value}, abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value},
engine::EngineConnection, engine::EngineConnection,
@ -33,12 +30,7 @@ impl ProgramMemory {
} }
/// Add to the program memory. /// Add to the program memory.
pub fn add( pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
&mut self,
key: &str,
value: MemoryItem,
source_range: SourceRange,
) -> Result<(), KclError> {
if self.root.get(key).is_some() { if self.root.get(key).is_some() {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails { return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine {}", key), message: format!("Cannot redefine {}", key),
@ -172,12 +164,7 @@ impl MemoryItem {
memory: &ProgramMemory, memory: &ProgramMemory,
engine: &mut EngineConnection, engine: &mut EngineConnection,
) -> Result<Option<ProgramReturn>, KclError> { ) -> Result<Option<ProgramReturn>, KclError> {
if let MemoryItem::Function { if let MemoryItem::Function { func, expression, meta } = self {
func,
expression,
meta,
} = self
{
if let Some(func) = func { if let Some(func) = func {
func(args, memory, expression, meta, engine) func(args, memory, expression, meta, engine)
} else { } else {
@ -228,10 +215,7 @@ impl SketchGroup {
if self.start.name == name { if self.start.name == name {
Some(&self.start) Some(&self.start)
} else { } else {
self.value self.value.iter().find(|p| p.get_name() == name).map(|p| p.get_base())
.iter()
.find(|p| p.get_name() == name)
.map(|p| p.get_base())
} }
} }
@ -314,12 +298,24 @@ impl From<[f64; 2]> for Point2d {
} }
} }
impl From<&[f64; 2]> for Point2d {
fn from(p: &[f64; 2]) -> Self {
Self { x: p[0], y: p[1] }
}
}
impl From<Point2d> for [f64; 2] { impl From<Point2d> for [f64; 2] {
fn from(p: Point2d) -> Self { fn from(p: Point2d) -> Self {
[p.x, p.y] [p.x, p.y]
} }
} }
impl From<Point2d> for kittycad::types::Point2D {
fn from(p: Point2d) -> Self {
Self { x: p.x, y: p.y }
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
pub struct Point3d { pub struct Point3d {
@ -506,7 +502,7 @@ impl Default for PipeInfo {
} }
/// Execute a AST's program. /// Execute a AST's program.
fn execute( pub fn execute(
program: crate::abstract_syntax_tree_types::Program, program: crate::abstract_syntax_tree_types::Program,
memory: &mut ProgramMemory, memory: &mut ProgramMemory,
options: BodyType, options: BodyType,
@ -526,8 +522,7 @@ fn execute(
match arg { match arg {
Value::Literal(literal) => args.push(literal.into()), Value::Literal(literal) => args.push(literal.into()),
Value::Identifier(identifier) => { Value::Identifier(identifier) => {
let memory_item = let memory_item = memory.get(&identifier.name, identifier.into())?;
memory.get(&identifier.name, identifier.into())?;
args.push(memory_item.clone()); args.push(memory_item.clone());
} }
// We do nothing for the rest. // We do nothing for the rest.
@ -542,8 +537,7 @@ fn execute(
})); }));
} }
memory.return_ = memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) { } else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?; func.call_fn(&args, memory, engine)?;
} else { } else {
@ -569,12 +563,7 @@ fn execute(
memory.add(&var_name, value.clone(), source_range)?; memory.add(&var_name, value.clone(), source_range)?;
} }
Value::BinaryExpression(binary_expression) => { Value::BinaryExpression(binary_expression) => {
let result = binary_expression.get_result( let result = binary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::FunctionExpression(function_expression) => { Value::FunctionExpression(function_expression) => {
@ -611,41 +600,28 @@ fn execute(
)?; )?;
} }
Value::CallExpression(call_expression) => { Value::CallExpression(call_expression) => {
let result = let result = call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::PipeExpression(pipe_expression) => { Value::PipeExpression(pipe_expression) => {
let result = pipe_expression.get_result( let result = pipe_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::PipeSubstitution(pipe_substitution) => { Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: format!("pipe substitution not implemented for declaration of variable {}", var_name), message: format!(
"pipe substitution not implemented for declaration of variable {}",
var_name
),
source_ranges: vec![pipe_substitution.into()], source_ranges: vec![pipe_substitution.into()],
})); }));
} }
Value::ArrayExpression(array_expression) => { Value::ArrayExpression(array_expression) => {
let result = array_expression.execute( let result = array_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::ObjectExpression(object_expression) => { Value::ObjectExpression(object_expression) => {
let result = object_expression.execute( let result = object_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::MemberExpression(member_expression) => { Value::MemberExpression(member_expression) => {
@ -653,12 +629,7 @@ fn execute(
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
Value::UnaryExpression(unary_expression) => { Value::UnaryExpression(unary_expression) => {
let result = unary_expression.get_result( let result = unary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
memory.add(&var_name, result, source_range)?; memory.add(&var_name, result, source_range)?;
} }
} }
@ -681,63 +652,17 @@ fn execute(
Ok(memory.clone()) Ok(memory.clone())
} }
// wasm_bindgen wrapper for execute
#[cfg(feature = "web")]
#[cfg(not(test))]
#[wasm_bindgen]
pub async fn execute_wasm(
program_str: &str,
memory_str: &str,
manager: crate::engine::conn_web::EngineCommandManager,
) -> Result<JsValue, String> {
use gloo_utils::format::JsValueSerdeExt;
// deserialize the ast from a stringified json
let program: crate::abstract_syntax_tree_types::Program =
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut engine = EngineConnection::new(manager)
.await
.map_err(|e| format!("{:?}", e))?;
let memory = execute(program, &mut mem, BodyType::Root, &mut engine).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&memory).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for execute
#[cfg(not(feature = "web"))]
#[wasm_bindgen]
pub async fn execute_wasm(program_str: &str, memory_str: &str) -> Result<JsValue, String> {
use gloo_utils::format::JsValueSerdeExt;
// deserialize the ast from a stringified json
let program: crate::abstract_syntax_tree_types::Program =
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut engine = EngineConnection::new("dev.kittycad.io", "some-token", "")
.await
.map_err(|e| format!("{:?}", e))?;
let memory = execute(program, &mut mem, BodyType::Root, &mut engine).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&memory).map_err(|e| e.to_string())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> { pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let tokens = crate::tokeniser::lexer(code); let tokens = crate::tokeniser::lexer(code);
let program = crate::parser::abstract_syntax_tree(&tokens)?; let program = crate::parser::abstract_syntax_tree(&tokens)?;
let mut mem: ProgramMemory = Default::default(); let mut mem: ProgramMemory = Default::default();
let mut engine = EngineConnection::new("dev.kittycad.io", "some-token", "").await?; let mut engine = EngineConnection::new().await?;
let memory = execute(program, &mut mem, BodyType::Root, &mut engine)?; let memory = execute(program, &mut mem, BodyType::Root, &mut engine)?;
Ok(memory) Ok(memory)
@ -780,23 +705,13 @@ show(part001)"#,
let memory = parse_execute(&ast_fn("-1")).await.unwrap(); let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()), serde_json::json!(1.0 + 2.0f64.sqrt()),
memory memory.root.get("intersect").unwrap().get_json_value().unwrap()
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
); );
let memory = parse_execute(&ast_fn("0")).await.unwrap(); let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!( assert_eq!(
serde_json::json!(1.0000000000000002), serde_json::json!(1.0000000000000002),
memory memory.root.get("intersect").unwrap().get_json_value().unwrap()
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
); );
} }

View File

@ -0,0 +1,10 @@
pub mod abstract_syntax_tree_types;
pub mod docs;
pub mod engine;
pub mod errors;
pub mod executor;
pub mod math_parser;
pub mod parser;
pub mod recast;
pub mod std;
pub mod tokeniser;

View File

@ -1,12 +1,13 @@
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::abstract_syntax_tree_types::{ use crate::{
BinaryExpression, BinaryPart, CallExpression, Identifier, Literal, abstract_syntax_tree_types::{BinaryExpression, BinaryPart, CallExpression, Identifier, Literal, ValueMeta},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
parser::{find_closing_brace, is_not_code_token, make_call_expression},
tokeniser::{Token, TokenType},
}; };
use crate::errors::{KclError, KclErrorDetails};
use crate::parser::{find_closing_brace, is_not_code_token, make_call_expression};
use crate::tokeniser::{Token, TokenType};
pub fn precedence(operator: &str) -> u8 { pub fn precedence(operator: &str) -> u8 {
// might be useful for reference to make it match // might be useful for reference to make it match
@ -182,6 +183,8 @@ pub struct ParenthesisToken {
pub end: usize, pub end: usize,
} }
crate::abstract_syntax_tree_types::impl_value_meta!(ParenthesisToken);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -195,10 +198,12 @@ pub struct ExtendedBinaryExpression {
pub end_extended: Option<usize>, pub end_extended: Option<usize>,
} }
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedBinaryExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub struct ExntendedLiteral { pub struct ExtendedLiteral {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
pub value: serde_json::Value, pub value: serde_json::Value,
@ -207,11 +212,13 @@ pub struct ExntendedLiteral {
pub end_extended: Option<usize>, pub end_extended: Option<usize>,
} }
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedLiteral);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum MathExpression { pub enum MathExpression {
ExntendedLiteral(Box<ExntendedLiteral>), ExtendedLiteral(Box<ExtendedLiteral>),
Identifier(Box<Identifier>), Identifier(Box<Identifier>),
CallExpression(Box<CallExpression>), CallExpression(Box<CallExpression>),
BinaryExpression(Box<BinaryExpression>), BinaryExpression(Box<BinaryExpression>),
@ -219,6 +226,30 @@ pub enum MathExpression {
ParenthesisToken(Box<ParenthesisToken>), ParenthesisToken(Box<ParenthesisToken>),
} }
impl MathExpression {
pub fn start(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.start(),
MathExpression::Identifier(identifier) => identifier.start(),
MathExpression::CallExpression(call_expression) => call_expression.start(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.start(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.start(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.start(),
}
}
pub fn end(&self) -> usize {
match self {
MathExpression::ExtendedLiteral(literal) => literal.end(),
MathExpression::Identifier(identifier) => identifier.end(),
MathExpression::CallExpression(call_expression) => call_expression.end(),
MathExpression::BinaryExpression(binary_expression) => binary_expression.end(),
MathExpression::ExtendedBinaryExpression(extended_binary_expression) => extended_binary_expression.end(),
MathExpression::ParenthesisToken(parenthesis_token) => parenthesis_token.end(),
}
}
}
fn build_tree( fn build_tree(
reverse_polish_notation_tokens: &[Token], reverse_polish_notation_tokens: &[Token],
stack: Vec<MathExpression>, stack: Vec<MathExpression>,
@ -241,80 +272,76 @@ fn build_tree(
}), }),
a => { a => {
return Err(KclError::InvalidExpression(a.clone())); return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![SourceRange([a.start(), a.end()])],
message: format!("{:?}", a),
}))
} }
}; };
} }
let current_token = &reverse_polish_notation_tokens[0]; let current_token = &reverse_polish_notation_tokens[0];
if current_token.token_type == TokenType::Number if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
|| current_token.token_type == TokenType::String
{
let mut new_stack = stack; let mut new_stack = stack;
new_stack.push(MathExpression::ExntendedLiteral(Box::new( new_stack.push(MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
ExntendedLiteral { value: if current_token.token_type == TokenType::Number {
value: if current_token.token_type == TokenType::Number { if let Ok(value) = current_token.value.parse::<i64>() {
if let Ok(value) = current_token.value.parse::<i64>() { serde_json::Value::Number(value.into())
serde_json::Value::Number(value.into()) } else if let Ok(value) = current_token.value.parse::<f64>() {
} else if let Ok(value) = current_token.value.parse::<f64>() { if let Some(n) = serde_json::Number::from_f64(value) {
if let Some(n) = serde_json::Number::from_f64(value) { serde_json::Value::Number(n)
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid float: {}", current_token.value),
}));
}
} else { } else {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value), message: format!("Invalid float: {}", current_token.value),
})); }));
} }
} else { } else {
let mut str_val = current_token.value.clone(); return Err(KclError::Syntax(KclErrorDetails {
str_val.remove(0); source_ranges: vec![current_token.into()],
str_val.pop(); message: format!("Invalid integer: {}", current_token.value),
serde_json::Value::String(str_val) }));
}, }
start: current_token.start, } else {
end: current_token.end, let mut str_val = current_token.value.clone();
raw: current_token.value.clone(), str_val.remove(0);
end_extended: None, str_val.pop();
start_extended: None, serde_json::Value::String(str_val)
}, },
)));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "("
{
let closing_brace = find_closing_brace(reverse_polish_notation_tokens, 1, 0, "")?;
let mut new_stack = stack;
new_stack.push(MathExpression::CallExpression(Box::new(
make_call_expression(reverse_polish_notation_tokens, 0)?.expression,
)));
return build_tree(
&reverse_polish_notation_tokens[closing_brace + 1..],
new_stack,
);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
start: current_token.start, start: current_token.start,
end: current_token.end, end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
}))); })));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack); return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == "(" { } else if current_token.token_type == TokenType::Word {
let mut new_stack = stack; if reverse_polish_notation_tokens.len() > 1 {
new_stack.push(MathExpression::ParenthesisToken(Box::new( if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
ParenthesisToken { && reverse_polish_notation_tokens[1].value == "("
value: "(".to_string(), {
let closing_brace = find_closing_brace(reverse_polish_notation_tokens, 1, 0, "")?;
let mut new_stack = stack;
new_stack.push(MathExpression::CallExpression(Box::new(
make_call_expression(reverse_polish_notation_tokens, 0)?.expression,
)));
return build_tree(&reverse_polish_notation_tokens[closing_brace + 1..], new_stack);
}
let mut new_stack = stack;
new_stack.push(MathExpression::Identifier(Box::new(Identifier {
name: current_token.value.clone(),
start: current_token.start, start: current_token.start,
end: current_token.end, end: current_token.end,
token_type: MathTokenType::Parenthesis, })));
}, return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
))); }
} else if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let mut new_stack = stack;
new_stack.push(MathExpression::ParenthesisToken(Box::new(ParenthesisToken {
value: "(".to_string(),
start: current_token.start,
end: current_token.end,
token_type: MathTokenType::Parenthesis,
})));
return build_tree(&reverse_polish_notation_tokens[1..], new_stack); return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} else if current_token.token_type == TokenType::Brace && current_token.value == ")" { } else if current_token.token_type == TokenType::Brace && current_token.value == ")" {
let inner_node: MathExpression = match &stack[stack.len() - 1] { let inner_node: MathExpression = match &stack[stack.len() - 1] {
@ -340,14 +367,22 @@ fn build_tree(
end_extended: None, end_extended: None,
})) }))
} }
MathExpression::ExntendedLiteral(literal) => { MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(literal.clone()),
MathExpression::ExntendedLiteral(literal.clone()) a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
} }
a => return Err(KclError::InvalidExpression(a.clone())),
}; };
let paran = match &stack[stack.len() - 2] { let paran = match &stack[stack.len() - 2] {
MathExpression::ParenthesisToken(paran) => paran.clone(), MathExpression::ParenthesisToken(paran) => paran.clone(),
a => return Err(KclError::InvalidExpression(a.clone())), a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
}
}; };
let expression = match inner_node { let expression = match inner_node {
MathExpression::ExtendedBinaryExpression(bin_exp) => { MathExpression::ExtendedBinaryExpression(bin_exp) => {
@ -372,22 +407,33 @@ fn build_tree(
end_extended: Some(current_token.end), end_extended: Some(current_token.end),
})) }))
} }
MathExpression::ExntendedLiteral(literal) => { MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
MathExpression::ExntendedLiteral(Box::new(ExntendedLiteral { value: literal.value.clone(),
value: literal.value.clone(), start: literal.start,
start: literal.start, end: literal.end,
end: literal.end, raw: literal.raw.clone(),
raw: literal.raw.clone(), end_extended: Some(current_token.end),
end_extended: Some(current_token.end), start_extended: Some(paran.start),
start_extended: Some(paran.start), })),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
})) }))
} }
a => return Err(KclError::InvalidExpression(a.clone())),
}; };
let mut new_stack = stack[0..stack.len() - 2].to_vec(); let mut new_stack = stack[0..stack.len() - 2].to_vec();
new_stack.push(expression); new_stack.push(expression);
return build_tree(&reverse_polish_notation_tokens[1..], new_stack); return build_tree(&reverse_polish_notation_tokens[1..], new_stack);
} }
if stack.len() < 2 {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: "unexpected end of expression".to_string(),
}));
}
let left: (BinaryPart, usize) = match &stack[stack.len() - 2] { let left: (BinaryPart, usize) = match &stack[stack.len() - 2] {
MathExpression::ExtendedBinaryExpression(bin_exp) => ( MathExpression::ExtendedBinaryExpression(bin_exp) => (
BinaryPart::BinaryExpression(Box::new(BinaryExpression { BinaryPart::BinaryExpression(Box::new(BinaryExpression {
@ -399,7 +445,7 @@ fn build_tree(
})), })),
bin_exp.start_extended.unwrap_or(bin_exp.start), bin_exp.start_extended.unwrap_or(bin_exp.start),
), ),
MathExpression::ExntendedLiteral(lit) => ( MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal { BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(), value: lit.value.clone(),
start: lit.start, start: lit.start,
@ -409,13 +455,14 @@ fn build_tree(
lit.start_extended.unwrap_or(lit.start), lit.start_extended.unwrap_or(lit.start),
), ),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start), MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => { MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.start),
(BinaryPart::CallExpression(call.clone()), call.start) MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
} }
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.start)
}
a => return Err(KclError::InvalidExpression(a.clone())),
}; };
let right = match &stack[stack.len() - 1] { let right = match &stack[stack.len() - 1] {
MathExpression::ExtendedBinaryExpression(bin_exp) => ( MathExpression::ExtendedBinaryExpression(bin_exp) => (
@ -428,7 +475,7 @@ fn build_tree(
})), })),
bin_exp.end_extended.unwrap_or(bin_exp.end), bin_exp.end_extended.unwrap_or(bin_exp.end),
), ),
MathExpression::ExntendedLiteral(lit) => ( MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal { BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(), value: lit.value.clone(),
start: lit.start, start: lit.start,
@ -438,13 +485,14 @@ fn build_tree(
lit.end_extended.unwrap_or(lit.end), lit.end_extended.unwrap_or(lit.end),
), ),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end), MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => { MathExpression::CallExpression(call) => (BinaryPart::CallExpression(call.clone()), call.end),
(BinaryPart::CallExpression(call.clone()), call.end) MathExpression::BinaryExpression(bin_exp) => (BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end),
a => {
return Err(KclError::InvalidExpression(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("{:?}", a),
}))
} }
MathExpression::BinaryExpression(bin_exp) => {
(BinaryPart::BinaryExpression(bin_exp.clone()), bin_exp.end)
}
a => return Err(KclError::InvalidExpression(a.clone())),
}; };
let right_end = match right.0.clone() { let right_end = match right.0.clone() {
@ -458,11 +506,7 @@ fn build_tree(
let tree = BinaryExpression { let tree = BinaryExpression {
operator: current_token.value.clone(), operator: current_token.value.clone(),
start: left.1, start: left.1,
end: if right.1 > right_end { end: if right.1 > right_end { right.1 } else { right_end },
right.1
} else {
right_end
},
left: left.0, left: left.0,
right: right.0, right: right.0,
}; };
@ -510,9 +554,10 @@ pub fn parse_expression(tokens: &[Token]) -> Result<BinaryExpression, KclError>
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_parse_expression() { fn test_parse_expression() {
let tokens = crate::tokeniser::lexer("1 + 2"); let tokens = crate::tokeniser::lexer("1 + 2");
@ -833,8 +878,7 @@ mod test {
#[test] #[test]
fn test_reverse_polish_notation_complex() { fn test_reverse_polish_notation_complex() {
let result = let result = reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
assert_eq!( assert_eq!(
result, result,
vec![ vec![
@ -874,8 +918,7 @@ mod test {
#[test] #[test]
fn test_reverse_polish_notation_complex_with_parentheses() { fn test_reverse_polish_notation_complex_with_parentheses() {
let result = let result = reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
assert_eq!( assert_eq!(
result, result,
vec![ vec![

View File

@ -1,19 +1,16 @@
use std::collections::HashMap; use std::{collections::HashMap, str::FromStr};
use crate::abstract_syntax_tree_types::{ use crate::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement, abstract_syntax_tree_types::{
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
NoneCodeMeta, NoneCodeNode, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NoneCodeMeta,
PipeSubstitution, Program, ReturnStatement, UnaryExpression, Value, VariableDeclaration, NoneCodeNode, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution, Program,
VariableDeclarator, ReturnStatement, UnaryExpression, Value, VariableDeclaration, VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
math_parser::parse_expression,
tokeniser::{Token, TokenType},
}; };
use crate::errors::{KclError, KclErrorDetails};
use crate::math_parser::parse_expression;
use crate::tokeniser::lexer;
use crate::tokeniser::{Token, TokenType};
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::prelude::*;
fn make_identifier(tokens: &[Token], index: usize) -> Identifier { fn make_identifier(tokens: &[Token], index: usize) -> Identifier {
let current_token = &tokens[index]; let current_token = &tokens[index];
@ -83,10 +80,7 @@ fn make_none_code_node(tokens: &[Token], index: usize) -> (Option<NoneCodeNode>,
find_end_of_non_code_node(tokens, index) find_end_of_non_code_node(tokens, index)
}; };
let non_code_tokens = tokens[index..end_index].to_vec(); let non_code_tokens = tokens[index..end_index].to_vec();
let value = non_code_tokens let value = non_code_tokens.iter().map(|t| t.value.clone()).collect::<String>();
.iter()
.map(|t| t.value.clone())
.collect::<String>();
let node = NoneCodeNode { let node = NoneCodeNode {
start: current_token.start, start: current_token.start,
@ -109,11 +103,7 @@ struct TokenReturnWithNonCode {
non_code_node: Option<NoneCodeNode>, non_code_node: Option<NoneCodeNode>,
} }
fn next_meaningful_token( fn next_meaningful_token(tokens: &[Token], index: usize, offset: Option<usize>) -> TokenReturnWithNonCode {
tokens: &[Token],
index: usize,
offset: Option<usize>,
) -> TokenReturnWithNonCode {
let new_index = index + offset.unwrap_or(1); let new_index = index + offset.unwrap_or(1);
let _token = tokens.get(new_index); let _token = tokens.get(new_index);
let token = if let Some(token) = _token { let token = if let Some(token) = _token {
@ -154,11 +144,13 @@ pub fn find_closing_brace(
brace_count: usize, brace_count: usize,
search_opening_brace: &str, search_opening_brace: &str,
) -> Result<usize, KclError> { ) -> Result<usize, KclError> {
let closing_brace_map: HashMap<&str, &str> = [("(", ")"), ("{", "}"), ("[", "]")] let closing_brace_map: HashMap<&str, &str> = [("(", ")"), ("{", "}"), ("[", "]")].iter().cloned().collect();
.iter() let Some(current_token) = tokens.get(index) else {
.cloned() return Err(KclError::Syntax(KclErrorDetails {
.collect(); source_ranges: vec![tokens.last().unwrap().into()],
let current_token = &tokens[index]; message: "unexpected end".to_string(),
}));
};
let mut search_opening_brace = search_opening_brace; let mut search_opening_brace = search_opening_brace;
let is_first_call = search_opening_brace.is_empty() && brace_count == 0; let is_first_call = search_opening_brace.is_empty() && brace_count == 0;
if is_first_call { if is_first_call {
@ -173,11 +165,9 @@ pub fn find_closing_brace(
})); }));
} }
} }
let found_closing_brace = let found_closing_brace = brace_count == 1 && current_token.value == closing_brace_map[search_opening_brace];
brace_count == 1 && current_token.value == closing_brace_map[search_opening_brace];
let found_another_opening_brace = current_token.value == search_opening_brace; let found_another_opening_brace = current_token.value == search_opening_brace;
let found_another_closing_brace = let found_another_closing_brace = current_token.value == closing_brace_map[search_opening_brace];
current_token.value == closing_brace_map[search_opening_brace];
if found_closing_brace { if found_closing_brace {
return Ok(index); return Ok(index);
} }
@ -222,9 +212,7 @@ fn find_next_declaration_keyword(tokens: &[Token], index: usize) -> Result<Token
}); });
} }
if let Some(token_val) = next_token.token { if let Some(token_val) = next_token.token {
if token_val.token_type == TokenType::Word if token_val.token_type == TokenType::Word && (token_val.value == "const" || token_val.value == "fn") {
&& (token_val.value == "const" || token_val.value == "fn")
{
return Ok(TokenReturn { return Ok(TokenReturn {
token: Some(token_val), token: Some(token_val),
index: next_token.index, index: next_token.index,
@ -279,8 +267,7 @@ fn has_pipe_operator(
let current_token = &tokens[index]; let current_token = &tokens[index];
if current_token.token_type == TokenType::Brace && current_token.value == "{" { if current_token.token_type == TokenType::Brace && current_token.value == "{" {
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?; let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
let token_after_closing_brace = let token_after_closing_brace = next_meaningful_token(tokens, closing_brace_index, None);
next_meaningful_token(tokens, closing_brace_index, None);
if let Some(token_after_closing_brace_val) = token_after_closing_brace.token { if let Some(token_after_closing_brace_val) = token_after_closing_brace.token {
if token_after_closing_brace_val.token_type == TokenType::Operator if token_after_closing_brace_val.token_type == TokenType::Operator
&& token_after_closing_brace_val.value == "|>" && token_after_closing_brace_val.value == "|>"
@ -397,10 +384,7 @@ pub struct MemberExpressionReturn {
pub last_index: usize, pub last_index: usize,
} }
fn make_member_expression( fn make_member_expression(tokens: &[Token], index: usize) -> Result<MemberExpressionReturn, KclError> {
tokens: &[Token],
index: usize,
) -> Result<MemberExpressionReturn, KclError> {
let current_token = tokens[index].clone(); let current_token = tokens[index].clone();
let mut keys_info = collect_object_keys(tokens, index, None)?; let mut keys_info = collect_object_keys(tokens, index, None)?;
let last_key = keys_info[keys_info.len() - 1].clone(); let last_key = keys_info[keys_info.len() - 1].clone();
@ -468,9 +452,7 @@ fn find_end_of_binary_expression(tokens: &[Token], index: usize) -> Result<usize
} }
let maybe_operator = next_meaningful_token(tokens, index, None); let maybe_operator = next_meaningful_token(tokens, index, None);
if let Some(maybe_operator_token) = maybe_operator.token { if let Some(maybe_operator_token) = maybe_operator.token {
if maybe_operator_token.token_type != TokenType::Operator if maybe_operator_token.token_type != TokenType::Operator || maybe_operator_token.value == "|>" {
|| maybe_operator_token.value == "|>"
{
return Ok(index); return Ok(index);
} }
let next_right = next_meaningful_token(tokens, maybe_operator.index, None); let next_right = next_meaningful_token(tokens, maybe_operator.index, None);
@ -506,10 +488,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
} else { } else {
return Err(KclError::Unimplemented(KclErrorDetails { return Err(KclError::Unimplemented(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
message: format!( message: format!("expression with token type {:?}", current_token.token_type),
"expression with token type {:?}",
current_token.token_type
),
})); }));
} }
} }
@ -568,9 +547,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
last_index: index, last_index: index,
}); });
} }
if current_token.token_type == TokenType::Number if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
|| current_token.token_type == TokenType::String
{
let literal = make_literal(tokens, index)?; let literal = make_literal(tokens, index)?;
return Ok(ValueReturn { return Ok(ValueReturn {
value: Value::Literal(Box::new(literal)), value: Value::Literal(Box::new(literal)),
@ -580,9 +557,7 @@ fn make_value(tokens: &[Token], index: usize) -> Result<ValueReturn, KclError> {
if current_token.token_type == TokenType::Brace && current_token.value == "(" { if current_token.token_type == TokenType::Brace && current_token.value == "(" {
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?; let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
return if let Some(arrow_token) = return if let Some(arrow_token) = next_meaningful_token(tokens, closing_brace_index, None).token {
next_meaningful_token(tokens, closing_brace_index, None).token
{
if arrow_token.token_type == TokenType::Operator && arrow_token.value == "=>" { if arrow_token.token_type == TokenType::Operator && arrow_token.value == "=>" {
let function_expression = make_function_expression(tokens, index)?; let function_expression = make_function_expression(tokens, index)?;
Ok(ValueReturn { Ok(ValueReturn {
@ -637,16 +612,12 @@ fn make_array_elements(
let current_element = make_value(tokens, index)?; let current_element = make_value(tokens, index)?;
let next_token = next_meaningful_token(tokens, current_element.last_index, None); let next_token = next_meaningful_token(tokens, current_element.last_index, None);
if let Some(next_token_token) = next_token.token { if let Some(next_token_token) = next_token.token {
let is_closing_brace = let is_closing_brace = next_token_token.token_type == TokenType::Brace && next_token_token.value == "]";
next_token_token.token_type == TokenType::Brace && next_token_token.value == "]";
let is_comma = next_token_token.token_type == TokenType::Comma; let is_comma = next_token_token.token_type == TokenType::Comma;
if !is_closing_brace && !is_comma { if !is_closing_brace && !is_comma {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![next_token_token.clone().into()], source_ranges: vec![next_token_token.clone().into()],
message: format!( message: format!("Expected a comma or closing brace, found {:?}", next_token_token.value),
"Expected a comma or closing brace, found {:?}",
next_token_token.value
),
})); }));
} }
let next_call_index = if is_closing_brace { let next_call_index = if is_closing_brace {
@ -715,10 +686,7 @@ fn make_pipe_body(
} else { } else {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
message: format!( message: format!("Expected a pipe value, found {:?}", current_token.token_type),
"Expected a pipe value, found {:?}",
current_token.token_type
),
})); }));
} }
let next_pipe = has_pipe_operator(tokens, index, None)?; let next_pipe = has_pipe_operator(tokens, index, None)?;
@ -734,20 +702,13 @@ fn make_pipe_body(
let mut _non_code_meta: NoneCodeMeta; let mut _non_code_meta: NoneCodeMeta;
if let Some(node) = next_pipe.non_code_node { if let Some(node) = next_pipe.non_code_node {
_non_code_meta = non_code_meta; _non_code_meta = non_code_meta;
_non_code_meta _non_code_meta.none_code_nodes.insert(previous_values.len(), node);
.none_code_nodes
.insert(previous_values.len(), node);
} else { } else {
_non_code_meta = non_code_meta; _non_code_meta = non_code_meta;
} }
let mut _previous_values = previous_values; let mut _previous_values = previous_values;
_previous_values.push(value); _previous_values.push(value);
make_pipe_body( make_pipe_body(tokens, next_pipe.index, _previous_values, Some(_non_code_meta))
tokens,
next_pipe.index,
_previous_values,
Some(_non_code_meta),
)
} }
struct BinaryExpressionReturn { struct BinaryExpressionReturn {
@ -755,10 +716,7 @@ struct BinaryExpressionReturn {
last_index: usize, last_index: usize,
} }
fn make_binary_expression( fn make_binary_expression(tokens: &[Token], index: usize) -> Result<BinaryExpressionReturn, KclError> {
tokens: &[Token],
index: usize,
) -> Result<BinaryExpressionReturn, KclError> {
let end_index = find_end_of_binary_expression(tokens, index)?; let end_index = find_end_of_binary_expression(tokens, index)?;
let expression = parse_expression(&tokens[index..end_index + 1])?; let expression = parse_expression(&tokens[index..end_index + 1])?;
Ok(BinaryExpressionReturn { Ok(BinaryExpressionReturn {
@ -772,11 +730,7 @@ struct ArgumentsReturn {
last_index: usize, last_index: usize,
} }
fn make_arguments( fn make_arguments(tokens: &[Token], index: usize, previous_args: Vec<Value>) -> Result<ArgumentsReturn, KclError> {
tokens: &[Token],
index: usize,
previous_args: Vec<Value>,
) -> Result<ArgumentsReturn, KclError> {
let brace_or_comma_token = &tokens[index]; let brace_or_comma_token = &tokens[index];
let should_finish_recursion = let should_finish_recursion =
brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")"; brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")";
@ -792,40 +746,28 @@ fn make_arguments(
if let Some(next_brace_or_comma_token) = next_brace_or_comma.token { if let Some(next_brace_or_comma_token) = next_brace_or_comma.token {
let is_identifier_or_literal = next_brace_or_comma_token.token_type == TokenType::Comma let is_identifier_or_literal = next_brace_or_comma_token.token_type == TokenType::Comma
|| next_brace_or_comma_token.token_type == TokenType::Brace; || next_brace_or_comma_token.token_type == TokenType::Brace;
if argument_token_token.token_type == TokenType::Brace if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "[" {
&& argument_token_token.value == "["
{
let array_expression = make_array_expression(tokens, argument_token.index)?; let array_expression = make_array_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, array_expression.last_index, None).index; next_meaningful_token(tokens, array_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::ArrayExpression(Box::new( _previous_args.push(Value::ArrayExpression(Box::new(array_expression.expression)));
array_expression.expression,
)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args); return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Operator if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "-" {
&& argument_token_token.value == "-"
{
let unary_expression = make_unary_expression(tokens, argument_token.index)?; let unary_expression = make_unary_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, unary_expression.last_index, None).index; next_meaningful_token(tokens, unary_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::UnaryExpression(Box::new( _previous_args.push(Value::UnaryExpression(Box::new(unary_expression.expression)));
unary_expression.expression,
)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args); return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Brace if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == "{" {
&& argument_token_token.value == "{"
{
let object_expression = make_object_expression(tokens, argument_token.index)?; let object_expression = make_object_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, object_expression.last_index, None).index; next_meaningful_token(tokens, object_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::ObjectExpression(Box::new( _previous_args.push(Value::ObjectExpression(Box::new(object_expression.expression)));
object_expression.expression,
)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args); return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
} }
if (argument_token_token.token_type == TokenType::Word if (argument_token_token.token_type == TokenType::Word
@ -837,23 +779,17 @@ fn make_arguments(
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, binary_expression.last_index, None).index; next_meaningful_token(tokens, binary_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new( _previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
binary_expression.expression,
)));
return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args); return make_arguments(tokens, next_comma_or_brace_token_index, _previous_args);
} }
if !is_identifier_or_literal { if !is_identifier_or_literal {
let binary_expression = make_binary_expression(tokens, next_brace_or_comma.index)?; let binary_expression = make_binary_expression(tokens, next_brace_or_comma.index)?;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new( _previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
binary_expression.expression,
)));
return make_arguments(tokens, binary_expression.last_index, _previous_args); return make_arguments(tokens, binary_expression.last_index, _previous_args);
} }
if argument_token_token.token_type == TokenType::Operator if argument_token_token.token_type == TokenType::Operator && argument_token_token.value == "%" {
&& argument_token_token.value == "%"
{
let value = Value::PipeSubstitution(Box::new(PipeSubstitution { let value = Value::PipeSubstitution(Box::new(PipeSubstitution {
start: argument_token_token.start, start: argument_token_token.start,
end: argument_token_token.end, end: argument_token_token.end,
@ -868,28 +804,23 @@ fn make_arguments(
&& next_brace_or_comma_token.value == "(" && next_brace_or_comma_token.value == "("
{ {
let closing_brace = find_closing_brace(tokens, next_brace_or_comma.index, 0, "")?; let closing_brace = find_closing_brace(tokens, next_brace_or_comma.index, 0, "")?;
return if let Some(token_after_closing_brace) = return if let Some(token_after_closing_brace) = next_meaningful_token(tokens, closing_brace, None).token
next_meaningful_token(tokens, closing_brace, None).token
{ {
if token_after_closing_brace.token_type == TokenType::Operator if token_after_closing_brace.token_type == TokenType::Operator
&& token_after_closing_brace.value != "|>" && token_after_closing_brace.value != "|>"
{ {
let binary_expression = let binary_expression = make_binary_expression(tokens, argument_token.index)?;
make_binary_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, binary_expression.last_index, None).index; next_meaningful_token(tokens, binary_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(Value::BinaryExpression(Box::new( _previous_args.push(Value::BinaryExpression(Box::new(binary_expression.expression)));
binary_expression.expression,
)));
make_arguments(tokens, next_comma_or_brace_token_index, _previous_args) make_arguments(tokens, next_comma_or_brace_token_index, _previous_args)
} else { } else {
let call_expression = make_call_expression(tokens, argument_token.index)?; let call_expression = make_call_expression(tokens, argument_token.index)?;
let next_comma_or_brace_token_index = let next_comma_or_brace_token_index =
next_meaningful_token(tokens, call_expression.last_index, None).index; next_meaningful_token(tokens, call_expression.last_index, None).index;
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args _previous_args.push(Value::CallExpression(Box::new(call_expression.expression)));
.push(Value::CallExpression(Box::new(call_expression.expression)));
make_arguments(tokens, next_comma_or_brace_token_index, _previous_args) make_arguments(tokens, next_comma_or_brace_token_index, _previous_args)
} }
} else { } else {
@ -901,8 +832,7 @@ fn make_arguments(
} }
if argument_token_token.token_type == TokenType::Word { if argument_token_token.token_type == TokenType::Word {
let identifier = let identifier = Value::Identifier(Box::new(make_identifier(tokens, argument_token.index)));
Value::Identifier(Box::new(make_identifier(tokens, argument_token.index)));
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(identifier); _previous_args.push(identifier);
return make_arguments(tokens, next_brace_or_comma.index, _previous_args); return make_arguments(tokens, next_brace_or_comma.index, _previous_args);
@ -913,9 +843,7 @@ fn make_arguments(
let mut _previous_args = previous_args; let mut _previous_args = previous_args;
_previous_args.push(literal); _previous_args.push(literal);
return make_arguments(tokens, next_brace_or_comma.index, _previous_args); return make_arguments(tokens, next_brace_or_comma.index, _previous_args);
} else if argument_token_token.token_type == TokenType::Brace } else if argument_token_token.token_type == TokenType::Brace && argument_token_token.value == ")" {
&& argument_token_token.value == ")"
{
return make_arguments(tokens, argument_token.index, previous_args); return make_arguments(tokens, argument_token.index, previous_args);
} }
@ -942,10 +870,7 @@ pub struct CallExpressionResult {
last_index: usize, last_index: usize,
} }
pub fn make_call_expression( pub fn make_call_expression(tokens: &[Token], index: usize) -> Result<CallExpressionResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<CallExpressionResult, KclError> {
let current_token = tokens[index].clone(); let current_token = tokens[index].clone();
let brace_token = next_meaningful_token(tokens, index, None); let brace_token = next_meaningful_token(tokens, index, None);
let callee = make_identifier(tokens, index); let callee = make_identifier(tokens, index);
@ -1038,27 +963,20 @@ struct VariableDeclarationResult {
last_index: usize, last_index: usize,
} }
fn make_variable_declaration( fn make_variable_declaration(tokens: &[Token], index: usize) -> Result<VariableDeclarationResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<VariableDeclarationResult, KclError> {
let current_token = tokens[index].clone(); let current_token = tokens[index].clone();
let declaration_start_token = next_meaningful_token(tokens, index, None); let declaration_start_token = next_meaningful_token(tokens, index, None);
let variable_declarators_result = let variable_declarators_result = make_variable_declarators(tokens, declaration_start_token.index, vec![])?;
make_variable_declarators(tokens, declaration_start_token.index, vec![])?;
Ok(VariableDeclarationResult { Ok(VariableDeclarationResult {
declaration: VariableDeclaration { declaration: VariableDeclaration {
start: current_token.start, start: current_token.start,
end: variable_declarators_result.declarations end: variable_declarators_result.declarations[variable_declarators_result.declarations.len() - 1].end,
[variable_declarators_result.declarations.len() - 1] kind: VariableKind::from_str(&current_token.value).map_err(|_| {
.end, KclError::Syntax(KclErrorDetails {
kind: if current_token.value == "const" { source_ranges: vec![current_token.into()],
"const".to_string() message: "Unexpected token".to_string(),
} else if current_token.value == "fn" { })
"fn".to_string() })?,
} else {
"unkown".to_string()
},
declarations: variable_declarators_result.declarations, declarations: variable_declarators_result.declarations,
}, },
last_index: variable_declarators_result.last_index, last_index: variable_declarators_result.last_index,
@ -1070,18 +988,12 @@ pub struct ParamsResult {
pub last_index: usize, pub last_index: usize,
} }
fn make_params( fn make_params(tokens: &[Token], index: usize, previous_params: Vec<Identifier>) -> Result<ParamsResult, KclError> {
tokens: &[Token],
index: usize,
previous_params: Vec<Identifier>,
) -> Result<ParamsResult, KclError> {
let brace_or_comma_token = &tokens[index]; let brace_or_comma_token = &tokens[index];
let argument = next_meaningful_token(tokens, index, None); let argument = next_meaningful_token(tokens, index, None);
if let Some(argument_token) = argument.token { if let Some(argument_token) = argument.token {
let should_finish_recursion = (argument_token.token_type == TokenType::Brace let should_finish_recursion = (argument_token.token_type == TokenType::Brace && argument_token.value == ")")
&& argument_token.value == ")") || (brace_or_comma_token.token_type == TokenType::Brace && brace_or_comma_token.value == ")");
|| (brace_or_comma_token.token_type == TokenType::Brace
&& brace_or_comma_token.value == ")");
if should_finish_recursion { if should_finish_recursion {
return Ok(ParamsResult { return Ok(ParamsResult {
params: previous_params, params: previous_params,
@ -1106,10 +1018,7 @@ struct UnaryExpressionResult {
last_index: usize, last_index: usize,
} }
fn make_unary_expression( fn make_unary_expression(tokens: &[Token], index: usize) -> Result<UnaryExpressionResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<UnaryExpressionResult, KclError> {
let current_token = &tokens[index]; let current_token = &tokens[index];
let next_token = next_meaningful_token(tokens, index, None); let next_token = next_meaningful_token(tokens, index, None);
let argument = make_value(tokens, next_token.index)?; let argument = make_value(tokens, next_token.index)?;
@ -1124,17 +1033,11 @@ fn make_unary_expression(
start: current_token.start, start: current_token.start,
end: argument_token.end, end: argument_token.end,
argument: match argument.value { argument: match argument.value {
Value::BinaryExpression(binary_expression) => { Value::BinaryExpression(binary_expression) => BinaryPart::BinaryExpression(binary_expression),
BinaryPart::BinaryExpression(binary_expression)
}
Value::Identifier(identifier) => BinaryPart::Identifier(identifier), Value::Identifier(identifier) => BinaryPart::Identifier(identifier),
Value::Literal(literal) => BinaryPart::Literal(literal), Value::Literal(literal) => BinaryPart::Literal(literal),
Value::UnaryExpression(unary_expression) => { Value::UnaryExpression(unary_expression) => BinaryPart::UnaryExpression(unary_expression),
BinaryPart::UnaryExpression(unary_expression) Value::CallExpression(call_expression) => BinaryPart::CallExpression(call_expression),
}
Value::CallExpression(call_expression) => {
BinaryPart::CallExpression(call_expression)
}
_ => { _ => {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()], source_ranges: vec![current_token.into()],
@ -1153,10 +1056,7 @@ struct ExpressionStatementResult {
last_index: usize, last_index: usize,
} }
fn make_expression_statement( fn make_expression_statement(tokens: &[Token], index: usize) -> Result<ExpressionStatementResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<ExpressionStatementResult, KclError> {
let current_token = &tokens[index]; let current_token = &tokens[index];
let next = next_meaningful_token(tokens, index, None); let next = next_meaningful_token(tokens, index, None);
if let Some(next_token) = &next.token { if let Some(next_token) = &next.token {
@ -1256,10 +1156,7 @@ struct ObjectExpressionResult {
last_index: usize, last_index: usize,
} }
fn make_object_expression( fn make_object_expression(tokens: &[Token], index: usize) -> Result<ObjectExpressionResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<ObjectExpressionResult, KclError> {
let opening_brace_token = &tokens[index]; let opening_brace_token = &tokens[index];
let first_property_token = next_meaningful_token(tokens, index, None); let first_property_token = next_meaningful_token(tokens, index, None);
let object_properties = make_object_properties(tokens, first_property_token.index, vec![])?; let object_properties = make_object_properties(tokens, first_property_token.index, vec![])?;
@ -1278,10 +1175,7 @@ struct ReturnStatementResult {
last_index: usize, last_index: usize,
} }
fn make_return_statement( fn make_return_statement(tokens: &[Token], index: usize) -> Result<ReturnStatementResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<ReturnStatementResult, KclError> {
let current_token = &tokens[index]; let current_token = &tokens[index];
let next_token = next_meaningful_token(tokens, index, None); let next_token = next_meaningful_token(tokens, index, None);
let val = make_value(tokens, next_token.index)?; let val = make_value(tokens, next_token.index)?;
@ -1333,9 +1227,7 @@ fn make_body(
if previous_body.is_empty() { if previous_body.is_empty() {
non_code_meta.start = next_token.non_code_node; non_code_meta.start = next_token.non_code_node;
} else { } else {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
} }
return make_body(tokens, next_token.index, previous_body, non_code_meta); return make_body(tokens, next_token.index, previous_body, non_code_meta);
@ -1343,18 +1235,14 @@ fn make_body(
let next = next_meaningful_token(tokens, token_index, None); let next = next_meaningful_token(tokens, token_index, None);
if let Some(node) = &next.non_code_node { if let Some(node) = &next.non_code_node {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
if token.token_type == TokenType::Word && (token.value == *"const" || token.value == "fn") { if token.token_type == TokenType::Word && (token.value == *"const" || token.value == "fn") {
let declaration = make_variable_declaration(tokens, token_index)?; let declaration = make_variable_declaration(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, declaration.last_index, None); let next_thing = next_meaningful_token(tokens, declaration.last_index, None);
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration { _previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
@ -1375,9 +1263,7 @@ fn make_body(
let statement = make_return_statement(tokens, token_index)?; let statement = make_return_statement(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, statement.last_index, None); let next_thing = next_meaningful_token(tokens, statement.last_index, None);
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ReturnStatement(ReturnStatement { _previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
@ -1394,16 +1280,11 @@ fn make_body(
} }
if let Some(next_token) = next.token { if let Some(next_token) = next.token {
if token.token_type == TokenType::Word if token.token_type == TokenType::Word && next_token.token_type == TokenType::Brace && next_token.value == "(" {
&& next_token.token_type == TokenType::Brace
&& next_token.value == "("
{
let expression = make_expression_statement(tokens, token_index)?; let expression = make_expression_statement(tokens, token_index)?;
let next_thing = next_meaningful_token(tokens, expression.last_index, None); let next_thing = next_meaningful_token(tokens, expression.last_index, None);
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
_previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement { _previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
@ -1426,9 +1307,7 @@ fn make_body(
&& next_thing_token.token_type == TokenType::Operator && next_thing_token.token_type == TokenType::Operator
{ {
if let Some(node) = &next_thing.non_code_node { if let Some(node) = &next_thing.non_code_node {
non_code_meta non_code_meta.none_code_nodes.insert(previous_body.len(), node.clone());
.none_code_nodes
.insert(previous_body.len(), node.clone());
} }
let expression = make_expression_statement(tokens, token_index)?; let expression = make_expression_statement(tokens, token_index)?;
let mut _previous_body = previous_body; let mut _previous_body = previous_body;
@ -1496,10 +1375,7 @@ struct FunctionExpressionResult {
last_index: usize, last_index: usize,
} }
fn make_function_expression( fn make_function_expression(tokens: &[Token], index: usize) -> Result<FunctionExpressionResult, KclError> {
tokens: &[Token],
index: usize,
) -> Result<FunctionExpressionResult, KclError> {
let current_token = &tokens[index]; let current_token = &tokens[index];
let closing_brace_index = find_closing_brace(tokens, index, 0, "")?; let closing_brace_index = find_closing_brace(tokens, index, 0, "")?;
let arrow_token = next_meaningful_token(tokens, closing_brace_index, None); let arrow_token = next_meaningful_token(tokens, closing_brace_index, None);
@ -1540,23 +1416,15 @@ pub fn abstract_syntax_tree(tokens: &[Token]) -> Result<Program, KclError> {
}) })
} }
#[wasm_bindgen]
pub fn parse_js(js: &str) -> Result<JsValue, String> {
let tokens = lexer(js);
let program = abstract_syntax_tree(&tokens).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&program).map_err(|e| e.to_string())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn test_make_identifier() { fn test_make_identifier() {
let tokens = lexer("a"); let tokens = crate::tokeniser::lexer("a");
let identifier = make_identifier(&tokens, 0); let identifier = make_identifier(&tokens, 0);
assert_eq!( assert_eq!(
Identifier { Identifier {
@ -1570,7 +1438,7 @@ mod tests {
#[test] #[test]
fn test_make_identifier_with_const_myvar_equals_5_and_index_2() { fn test_make_identifier_with_const_myvar_equals_5_and_index_2() {
let tokens = lexer("const myVar = 5"); let tokens = crate::tokeniser::lexer("const myVar = 5");
let identifier = make_identifier(&tokens, 2); let identifier = make_identifier(&tokens, 2);
assert_eq!( assert_eq!(
Identifier { Identifier {
@ -1584,7 +1452,7 @@ mod tests {
#[test] #[test]
fn test_make_identifier_multiline() { fn test_make_identifier_multiline() {
let tokens = lexer("const myVar = 5\nconst newVar = myVar + 1"); let tokens = crate::tokeniser::lexer("const myVar = 5\nconst newVar = myVar + 1");
let identifier = make_identifier(&tokens, 2); let identifier = make_identifier(&tokens, 2);
assert_eq!( assert_eq!(
Identifier { Identifier {
@ -1607,7 +1475,7 @@ mod tests {
#[test] #[test]
fn test_make_identifier_call_expression() { fn test_make_identifier_call_expression() {
let tokens = lexer("log(5, \"hello\", aIdentifier)"); let tokens = crate::tokeniser::lexer("log(5, \"hello\", aIdentifier)");
let identifier = make_identifier(&tokens, 0); let identifier = make_identifier(&tokens, 0);
assert_eq!( assert_eq!(
Identifier { Identifier {
@ -1629,7 +1497,7 @@ mod tests {
} }
#[test] #[test]
fn test_make_none_code_node() { fn test_make_none_code_node() {
let tokens = lexer("log(5, \"hello\", aIdentifier)"); let tokens = crate::tokeniser::lexer("log(5, \"hello\", aIdentifier)");
let index = 4; let index = 4;
let expected_output = ( let expected_output = (
Some(NoneCodeNode { Some(NoneCodeNode {
@ -1651,7 +1519,7 @@ mod tests {
7, 7,
); );
assert_eq!(make_none_code_node(&tokens, index), expected_output); assert_eq!(make_none_code_node(&tokens, index), expected_output);
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#" r#"
const yo = { a: { b: { c: '123' } } } const yo = { a: { b: { c: '123' } } }
// this is a comment // this is a comment
@ -1700,7 +1568,7 @@ const key = 'c'"#,
31, 31,
); );
assert_eq!(make_none_code_node(&tokens, index), expected_output); assert_eq!(make_none_code_node(&tokens, index), expected_output);
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const mySketch = startSketchAt([0,0]) r#"const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %) |> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -1714,8 +1582,7 @@ const key = 'c'"#,
Some(NoneCodeNode { Some(NoneCodeNode {
start: 106, start: 106,
end: 166, end: 166,
value: " /* this is\n a comment\n spanning a few lines */\n " value: " /* this is\n a comment\n spanning a few lines */\n ".to_string(),
.to_string(),
}), }),
59, 59,
); );
@ -1724,7 +1591,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_collect_object_keys() { fn test_collect_object_keys() {
let tokens = lexer("const prop = yo.one[\"two\"]"); let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]");
let keys_info = collect_object_keys(&tokens, 6, None).unwrap(); let keys_info = collect_object_keys(&tokens, 6, None).unwrap();
assert_eq!(keys_info.len(), 2); assert_eq!(keys_info.len(), 2);
let first_key = match keys_info[0].key.clone() { let first_key = match keys_info[0].key.clone() {
@ -1743,7 +1610,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_make_literal_call_expression() { fn test_make_literal_call_expression() {
let tokens = lexer("log(5, \"hello\", aIdentifier)"); let tokens = crate::tokeniser::lexer("log(5, \"hello\", aIdentifier)");
let literal = make_literal(&tokens, 2).unwrap(); let literal = make_literal(&tokens, 2).unwrap();
assert_eq!( assert_eq!(
Literal { Literal {
@ -1833,7 +1700,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_next_meaningful_token() { fn test_next_meaningful_token() {
let _offset = 1; let _offset = 1;
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const mySketch = startSketchAt([0,0]) r#"const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %) |> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -2218,7 +2085,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_find_closing_brace() { fn test_find_closing_brace() {
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const mySketch = startSketchAt([0,0]) r#"const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %) |> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -2234,22 +2101,24 @@ const key = 'c'"#,
assert_eq!(find_closing_brace(&tokens, 90, 0, "").unwrap(), 92); assert_eq!(find_closing_brace(&tokens, 90, 0, "").unwrap(), 92);
let basic = "( hey )"; let basic = "( hey )";
assert_eq!(find_closing_brace(&lexer(basic), 0, 0, "").unwrap(), 4);
let handles_non_zero_index =
"(indexForBracketToRightOfThisIsTwo(shouldBeFour)AndNotThisSix)";
assert_eq!( assert_eq!(
find_closing_brace(&lexer(handles_non_zero_index), 2, 0, "").unwrap(), find_closing_brace(&crate::tokeniser::lexer(basic), 0, 0, "").unwrap(),
4
);
let handles_non_zero_index = "(indexForBracketToRightOfThisIsTwo(shouldBeFour)AndNotThisSix)";
assert_eq!(
find_closing_brace(&crate::tokeniser::lexer(handles_non_zero_index), 2, 0, "").unwrap(),
4 4
); );
assert_eq!( assert_eq!(
find_closing_brace(&lexer(handles_non_zero_index), 0, 0, "").unwrap(), find_closing_brace(&crate::tokeniser::lexer(handles_non_zero_index), 0, 0, "").unwrap(),
6 6
); );
let handles_nested = "{a{b{c(}d]}eathou athoeu tah u} thatOneToTheLeftIsLast }"; let handles_nested = "{a{b{c(}d]}eathou athoeu tah u} thatOneToTheLeftIsLast }";
assert_eq!( assert_eq!(
find_closing_brace(&lexer(handles_nested), 0, 0, "").unwrap(), find_closing_brace(&crate::tokeniser::lexer(handles_nested), 0, 0, "").unwrap(),
18 18
); );
@ -2258,7 +2127,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_is_call_expression() { fn test_is_call_expression() {
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const mySketch = startSketchAt([0,0]) r#"const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %) |> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -2278,7 +2147,7 @@ const key = 'c'"#,
#[test] #[test]
fn test_find_next_declaration_keyword() { fn test_find_next_declaration_keyword() {
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const mySketch = startSketchAt([0,0]) r#"const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %) |> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is |> lineTo([1, 1], %) /* this is
@ -2289,13 +2158,10 @@ const key = 'c'"#,
); );
assert_eq!( assert_eq!(
find_next_declaration_keyword(&tokens, 4).unwrap(), find_next_declaration_keyword(&tokens, 4).unwrap(),
TokenReturn { TokenReturn { token: None, index: 92 }
token: None,
index: 92,
}
); );
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const myVar = 5 r#"const myVar = 5
const newVar = myVar + 1 const newVar = myVar + 1
"#, "#,
@ -2314,10 +2180,7 @@ const newVar = myVar + 1
); );
assert_eq!( assert_eq!(
find_next_declaration_keyword(&tokens, 14).unwrap(), find_next_declaration_keyword(&tokens, 14).unwrap(),
TokenReturn { TokenReturn { token: None, index: 19 }
token: None,
index: 19,
}
); );
} }
@ -2327,7 +2190,7 @@ const newVar = myVar + 1
lineTo(2, 3) lineTo(2, 3)
} |> rx(45, %) } |> rx(45, %)
"#; "#;
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
assert_eq!( assert_eq!(
has_pipe_operator(&tokens, 0, None).unwrap(), has_pipe_operator(&tokens, 0, None).unwrap(),
TokenReturnWithNonCode { TokenReturnWithNonCode {
@ -2349,7 +2212,7 @@ const newVar = myVar + 1
lineTo(2, 3) lineTo(2, 3)
} |> rx(45, %) |> rx(45, %) } |> rx(45, %) |> rx(45, %)
"#; "#;
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
assert_eq!( assert_eq!(
has_pipe_operator(&tokens, 0, None).unwrap(), has_pipe_operator(&tokens, 0, None).unwrap(),
TokenReturnWithNonCode { TokenReturnWithNonCode {
@ -2374,7 +2237,7 @@ const newVar = myVar + 1
const yo = myFunc(9() const yo = myFunc(9()
|> rx(45, %) |> rx(45, %)
"#; "#;
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
assert_eq!( assert_eq!(
has_pipe_operator(&tokens, 0, None).unwrap(), has_pipe_operator(&tokens, 0, None).unwrap(),
TokenReturnWithNonCode { TokenReturnWithNonCode {
@ -2385,7 +2248,7 @@ const yo = myFunc(9()
); );
let code = "const myVar2 = 5 + 1 |> myFn(%)"; let code = "const myVar2 = 5 + 1 |> myFn(%)";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
assert_eq!( assert_eq!(
has_pipe_operator(&tokens, 1, None).unwrap(), has_pipe_operator(&tokens, 1, None).unwrap(),
TokenReturnWithNonCode { TokenReturnWithNonCode {
@ -2410,11 +2273,8 @@ const yo = myFunc(9()
lineTo(1,1) lineTo(1,1)
} |> rx(90, %) } |> rx(90, %)
show(mySk1)"#; show(mySk1)"#;
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let token_with_my_path_index = tokens let token_with_my_path_index = tokens.iter().position(|token| token.value == "myPath").unwrap();
.iter()
.position(|token| token.value == "myPath")
.unwrap();
// loop through getting the token and it's index // loop through getting the token and it's index
let token_with_line_to_index_for_var_dec_index = tokens let token_with_line_to_index_for_var_dec_index = tokens
.iter() .iter()
@ -2454,7 +2314,7 @@ show(mySk1)"#;
#[test] #[test]
fn test_make_member_expression() { fn test_make_member_expression() {
let tokens = lexer("const prop = yo.one[\"two\"]"); let tokens = crate::tokeniser::lexer("const prop = yo.one[\"two\"]");
let member_expression_return = make_member_expression(&tokens, 6).unwrap(); let member_expression_return = make_member_expression(&tokens, 6).unwrap();
let member_expression = member_expression_return.expression; let member_expression = member_expression_return.expression;
let last_index = member_expression_return.last_index; let last_index = member_expression_return.last_index;
@ -2495,12 +2355,12 @@ show(mySk1)"#;
#[test] #[test]
fn test_find_end_of_binary_expression() { fn test_find_end_of_binary_expression() {
let code = "1 + 2 * 3\nconst yo = 5"; let code = "1 + 2 * 3\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].value, "3"); assert_eq!(tokens[end].value, "3");
let code = "(1 + 25) / 5 - 3\nconst yo = 5"; let code = "(1 + 25) / 5 - 3\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].value, "3"); assert_eq!(tokens[end].value, "3");
let index_of_5 = code.find('5').unwrap(); let index_of_5 = code.find('5').unwrap();
@ -2508,44 +2368,44 @@ show(mySk1)"#;
assert_eq!(end_starting_at_the_5, end); assert_eq!(end_starting_at_the_5, end);
// whole thing wraped // whole thing wraped
let code = "((1 + 2) / 5 - 3)\nconst yo = 5"; let code = "((1 + 2) / 5 - 3)\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].end, code.find("3)").unwrap() + 2); assert_eq!(tokens[end].end, code.find("3)").unwrap() + 2);
// whole thing wraped but given index after the first brace // whole thing wraped but given index after the first brace
let code = "((1 + 2) / 5 - 3)\nconst yo = 5"; let code = "((1 + 2) / 5 - 3)\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 1).unwrap(); let end = find_end_of_binary_expression(&tokens, 1).unwrap();
assert_eq!(tokens[end].value, "3"); assert_eq!(tokens[end].value, "3");
// given the index of a small wrapped section i.e. `1 + 2` in ((1 + 2) / 5 - 3)' // given the index of a small wrapped section i.e. `1 + 2` in ((1 + 2) / 5 - 3)'
let code = "((1 + 2) / 5 - 3)\nconst yo = 5"; let code = "((1 + 2) / 5 - 3)\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 2).unwrap(); let end = find_end_of_binary_expression(&tokens, 2).unwrap();
assert_eq!(tokens[end].value, "2"); assert_eq!(tokens[end].value, "2");
// lots of silly nesting // lots of silly nesting
let code = "(1 + 2) / (5 - (3))\nconst yo = 5"; let code = "(1 + 2) / (5 - (3))\nconst yo = 5";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].end, code.find("))").unwrap() + 2); assert_eq!(tokens[end].end, code.find("))").unwrap() + 2);
// with pipe operator at the end // with pipe operator at the end
let code = "(1 + 2) / (5 - (3))\n |> fn(%)"; let code = "(1 + 2) / (5 - (3))\n |> fn(%)";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].end, code.find("))").unwrap() + 2); assert_eq!(tokens[end].end, code.find("))").unwrap() + 2);
// with call expression at the start of binary expression // with call expression at the start of binary expression
let code = "yo(2) + 3\n |> fn(%)"; let code = "yo(2) + 3\n |> fn(%)";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let end = find_end_of_binary_expression(&tokens, 0).unwrap(); let end = find_end_of_binary_expression(&tokens, 0).unwrap();
assert_eq!(tokens[end].value, "3"); assert_eq!(tokens[end].value, "3");
// with call expression at the end of binary expression // with call expression at the end of binary expression
let code = "3 + yo(2)\n |> fn(%)"; let code = "3 + yo(2)\n |> fn(%)";
let tokens = lexer(code); let tokens = crate::tokeniser::lexer(code);
let _end = find_end_of_binary_expression(&tokens, 0).unwrap(); let _end = find_end_of_binary_expression(&tokens, 0).unwrap();
} }
#[test] #[test]
fn test_make_array_expression() { fn test_make_array_expression() {
// input_index: 6, output_index: 14, output: {"type":"ArrayExpression","start":11,"end":26,"elements":[{"type":"Literal","start":12,"end":15,"value":"1","raw":"\"1\""},{"type":"Literal","start":17,"end":18,"value":2,"raw":"2"},{"type":"Identifier","start":20,"end":25,"name":"three"}]} // input_index: 6, output_index: 14, output: {"type":"ArrayExpression","start":11,"end":26,"elements":[{"type":"Literal","start":12,"end":15,"value":"1","raw":"\"1\""},{"type":"Literal","start":17,"end":18,"value":2,"raw":"2"},{"type":"Identifier","start":20,"end":25,"name":"three"}]}
let tokens = lexer("const yo = [\"1\", 2, three]"); let tokens = crate::tokeniser::lexer("const yo = [\"1\", 2, three]");
let array_expression = make_array_expression(&tokens, 6).unwrap(); let array_expression = make_array_expression(&tokens, 6).unwrap();
let expression = array_expression.expression; let expression = array_expression.expression;
assert_eq!(array_expression.last_index, 14); assert_eq!(array_expression.last_index, 14);
@ -2583,7 +2443,7 @@ show(mySk1)"#;
#[test] #[test]
fn test_make_call_expression() { fn test_make_call_expression() {
let tokens = lexer("foo(\"a\", a, 3)"); let tokens = crate::tokeniser::lexer("foo(\"a\", a, 3)");
let result = make_call_expression(&tokens, 0).unwrap(); let result = make_call_expression(&tokens, 0).unwrap();
assert_eq!(result.last_index, 9); assert_eq!(result.last_index, 9);
assert_eq!(result.expression.start, 0); assert_eq!(result.expression.start, 0);
@ -2616,14 +2476,14 @@ show(mySk1)"#;
#[test] #[test]
fn test_make_variable_declaration() { fn test_make_variable_declaration() {
let tokens = lexer( let tokens = crate::tokeniser::lexer(
r#"const yo = startSketch([0, 0]) r#"const yo = startSketch([0, 0])
|> lineTo([1, myVar], %) |> lineTo([1, myVar], %)
|> foo(myVar2, %) |> foo(myVar2, %)
|> close(%)"#, |> close(%)"#,
); );
let result = make_variable_declaration(&tokens, 0).unwrap(); let result = make_variable_declaration(&tokens, 0).unwrap();
assert_eq!(result.declaration.kind, "const"); assert_eq!(result.declaration.kind.to_string(), "const");
assert_eq!(result.declaration.declarations.len(), 1); assert_eq!(result.declaration.declarations.len(), 1);
assert_eq!(result.declaration.declarations[0].id.name, "yo"); assert_eq!(result.declaration.declarations[0].id.name, "yo");
let declaration = result.declaration.declarations[0].clone(); let declaration = result.declaration.declarations[0].clone();
@ -2685,7 +2545,7 @@ show(mySk1)"#;
#[test] #[test]
fn test_make_body() { fn test_make_body() {
let tokens = lexer("const myVar = 5"); let tokens = crate::tokeniser::lexer("const myVar = 5");
let body = make_body( let body = make_body(
&tokens, &tokens,
0, 0,
@ -2702,7 +2562,7 @@ show(mySk1)"#;
#[test] #[test]
fn test_abstract_syntax_tree() { fn test_abstract_syntax_tree() {
let code = "5 +6"; let code = "5 +6";
let result = abstract_syntax_tree(&lexer(code)).unwrap(); let result = abstract_syntax_tree(&crate::tokeniser::lexer(code)).unwrap();
let expected_result = Program { let expected_result = Program {
start: 0, start: 0,
end: 4, end: 4,

View File

@ -1,21 +1,15 @@
//! Generates source code from the AST. //! Generates source code from the AST.
//! The inverse of parsing (which generates an AST from the source code) //! The inverse of parsing (which generates an AST from the source code)
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::prelude::*;
use crate::abstract_syntax_tree_types::{ use crate::abstract_syntax_tree_types::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, Literal,
Literal, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, Program, UnaryExpression,
Program, UnaryExpression, Value, Value,
}; };
fn recast_literal(literal: Literal) -> String { fn recast_literal(literal: Literal) -> String {
if let serde_json::Value::String(value) = literal.value { if let serde_json::Value::String(value) = literal.value {
let quote = if literal.raw.trim().starts_with('"') { let quote = if literal.raw.trim().starts_with('"') { '"' } else { '\'' };
'"'
} else {
'\''
};
format!("{}{}{}", quote, value, quote) format!("{}{}{}", quote, value, quote)
} else { } else {
literal.value.to_string() literal.value.to_string()
@ -41,16 +35,13 @@ fn recast_binary_expression(expression: BinaryExpression) -> String {
let should_wrap_right = match expression.right.clone() { let should_wrap_right = match expression.right.clone() {
BinaryPart::BinaryExpression(bin_exp) => { BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator) precedence(&expression.operator) > precedence(&bin_exp.operator) || expression.operator == "-"
|| expression.operator == "-"
} }
_ => false, _ => false,
}; };
let should_wrap_left = match expression.left.clone() { let should_wrap_left = match expression.left.clone() {
BinaryPart::BinaryExpression(bin_exp) => { BinaryPart::BinaryExpression(bin_exp) => precedence(&expression.operator) > precedence(&bin_exp.operator),
precedence(&expression.operator) > precedence(&bin_exp.operator)
}
_ => false, _ => false,
}; };
@ -66,12 +57,8 @@ fn recast_binary_part(part: BinaryPart) -> String {
match part { match part {
BinaryPart::Literal(literal) => recast_literal(*literal), BinaryPart::Literal(literal) => recast_literal(*literal),
BinaryPart::Identifier(identifier) => identifier.name, BinaryPart::Identifier(identifier) => identifier.name,
BinaryPart::BinaryExpression(binary_expression) => { BinaryPart::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
recast_binary_expression(*binary_expression) BinaryPart::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
}
BinaryPart::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
_ => String::new(), _ => String::new(),
} }
} }
@ -81,15 +68,11 @@ fn recast_value(node: Value, _indentation: String, is_in_pipe_expression: bool)
match node { match node {
Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp), Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp),
Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation), Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation),
Value::ObjectExpression(ref obj_exp) => { Value::ObjectExpression(ref obj_exp) => recast_object_expression(obj_exp, &indentation, is_in_pipe_expression),
recast_object_expression(obj_exp, &indentation, is_in_pipe_expression)
}
Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp), Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp),
Value::Literal(literal) => recast_literal(*literal), Value::Literal(literal) => recast_literal(*literal),
Value::FunctionExpression(func_exp) => recast_function(*func_exp), Value::FunctionExpression(func_exp) => recast_function(*func_exp),
Value::CallExpression(call_exp) => { Value::CallExpression(call_exp) => recast_call_expression(&call_exp, &indentation, is_in_pipe_expression),
recast_call_expression(&call_exp, &indentation, is_in_pipe_expression)
}
Value::Identifier(ident) => ident.name, Value::Identifier(ident) => ident.name,
Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_exp), Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_exp),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp), Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -126,11 +109,7 @@ fn recast_array_expression(expression: &ArrayExpression, indentation: &str) -> S
} }
} }
fn recast_object_expression( fn recast_object_expression(expression: &ObjectExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
expression: &ObjectExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
let flat_recast = format!( let flat_recast = format!(
"{{ {} }}", "{{ {} }}",
expression expression
@ -159,11 +138,7 @@ fn recast_object_expression(
format!( format!(
"{}: {}", "{}: {}",
prop.key.name, prop.key.name,
recast_value( recast_value(prop.value.clone(), _indentation.clone(), is_in_pipe_expression)
prop.value.clone(),
_indentation.clone(),
is_in_pipe_expression
)
) )
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
@ -175,11 +150,7 @@ fn recast_object_expression(
} }
} }
fn recast_call_expression( fn recast_call_expression(expression: &CallExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
expression: &CallExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
format!( format!(
"{}({})", "{}({})",
expression.callee.name, expression.callee.name,
@ -201,9 +172,7 @@ fn recast_argument(argument: Value, indentation: &str, is_in_pipe_expression: bo
Value::ObjectExpression(object_exp) => { Value::ObjectExpression(object_exp) => {
recast_object_expression(&object_exp, indentation, is_in_pipe_expression) recast_object_expression(&object_exp, indentation, is_in_pipe_expression)
} }
Value::CallExpression(call_exp) => { Value::CallExpression(call_exp) => recast_call_expression(&call_exp, indentation, is_in_pipe_expression),
recast_call_expression(&call_exp, indentation, is_in_pipe_expression)
}
Value::FunctionExpression(function_exp) => recast_function(*function_exp), Value::FunctionExpression(function_exp) => recast_function(*function_exp),
Value::PipeSubstitution(_) => "%".to_string(), Value::PipeSubstitution(_) => "%".to_string(),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp), Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -224,9 +193,7 @@ fn recast_member_expression(expression: MemberExpression) -> String {
}; };
match expression.object { match expression.object {
MemberObject::MemberExpression(member_exp) => { MemberObject::MemberExpression(member_exp) => recast_member_expression(*member_exp) + key_str.as_str(),
recast_member_expression(*member_exp) + key_str.as_str()
}
MemberObject::Identifier(identifier) => identifier.name + key_str.as_str(), MemberObject::Identifier(identifier) => identifier.name + key_str.as_str(),
} }
} }
@ -263,9 +230,7 @@ fn recast_unary_expression(expression: UnaryExpression) -> String {
let bin_part_val = match expression.argument { let bin_part_val = match expression.argument {
BinaryPart::Literal(literal) => Value::Literal(literal), BinaryPart::Literal(literal) => Value::Literal(literal),
BinaryPart::Identifier(identifier) => Value::Identifier(identifier), BinaryPart::Identifier(identifier) => Value::Identifier(identifier),
BinaryPart::BinaryExpression(binary_expression) => { BinaryPart::BinaryExpression(binary_expression) => Value::BinaryExpression(binary_expression),
Value::BinaryExpression(binary_expression)
}
BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_expression), BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_expression),
BinaryPart::UnaryExpression(unary_expression) => Value::UnaryExpression(unary_expression), BinaryPart::UnaryExpression(unary_expression) => Value::UnaryExpression(unary_expression),
}; };
@ -280,23 +245,13 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
ast.body ast.body
.iter() .iter()
.map(|statement| match statement.clone() { .map(|statement| match statement.clone() {
BodyItem::ExpressionStatement(expression_statement) => { BodyItem::ExpressionStatement(expression_statement) => match expression_statement.expression {
match expression_statement.expression { Value::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
Value::BinaryExpression(binary_expression) => { Value::ArrayExpression(array_expression) => recast_array_expression(&array_expression, ""),
recast_binary_expression(*binary_expression) Value::ObjectExpression(object_expression) => recast_object_expression(&object_expression, "", false),
} Value::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
Value::ArrayExpression(array_expression) => { _ => "Expression".to_string(),
recast_array_expression(&array_expression, "") },
}
Value::ObjectExpression(object_expression) => {
recast_object_expression(&object_expression, "", false)
}
Value::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
_ => "Expression".to_string(),
}
}
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration BodyItem::VariableDeclaration(variable_declaration) => variable_declaration
.declarations .declarations
.iter() .iter()
@ -310,22 +265,16 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
}) })
.collect::<String>(), .collect::<String>(),
BodyItem::ReturnStatement(return_statement) => { BodyItem::ReturnStatement(return_statement) => {
format!( format!("return {}", recast_argument(return_statement.argument, "", false))
"return {}",
recast_argument(return_statement.argument, "", false)
)
} }
}) })
.enumerate() .enumerate()
.map(|(index, recast_str)| { .map(|(index, recast_str)| {
let is_legit_custom_whitespace_or_comment = let is_legit_custom_whitespace_or_comment = |str: String| str != " " && str != "\n" && str != " ";
|str: String| str != " " && str != "\n" && str != " ";
// determine the value of startString // determine the value of startString
let last_white_space_or_comment = if index > 0 { let last_white_space_or_comment = if index > 0 {
let tmp = if let Some(non_code_node) = let tmp = if let Some(non_code_node) = ast.non_code_meta.none_code_nodes.get(&(index - 1)) {
ast.non_code_meta.none_code_nodes.get(&(index - 1))
{
non_code_node.value.clone() non_code_node.value.clone()
} else { } else {
" ".to_string() " ".to_string()
@ -335,12 +284,11 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
" ".to_string() " ".to_string()
}; };
// indentation of this line will be covered by the previous if we're using a custom whitespace or comment // indentation of this line will be covered by the previous if we're using a custom whitespace or comment
let mut start_string = let mut start_string = if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) { String::new()
String::new() } else {
} else { indentation.to_owned()
indentation.to_owned() };
};
if index == 0 { if index == 0 {
if let Some(start) = ast.non_code_meta.start.clone() { if let Some(start) = ast.non_code_meta.start.clone() {
start_string = start.value; start_string = start.value;
@ -358,13 +306,10 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
} else { } else {
"\n".to_string() "\n".to_string()
}; };
let mut custom_white_space_or_comment = let mut custom_white_space_or_comment = match ast.non_code_meta.none_code_nodes.get(&index) {
match ast.non_code_meta.none_code_nodes.get(&index) { Some(custom_white_space_or_comment) => custom_white_space_or_comment.value.clone(),
Some(custom_white_space_or_comment) => { None => String::new(),
custom_white_space_or_comment.value.clone() };
}
None => String::new(),
};
if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) { if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) {
custom_white_space_or_comment = String::new(); custom_white_space_or_comment = String::new();
} }
@ -400,14 +345,3 @@ pub fn recast_function(expression: FunctionExpression) -> String {
) )
) )
} }
// wasm_bindgen wrapper for recast
// test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
#[wasm_bindgen]
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
// deserialize the ast from a stringified json
let program: Program = serde_json::from_str(json_str).map_err(JsError::from)?;
let result = recast(&program, "", false);
Ok(JsValue::from_serde(&result)?)
}

View File

@ -1,15 +1,15 @@
//! Functions related to extruding. //! Functions related to extruding.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup}, executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup},
std::Args, std::Args,
}; };
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
/// Extrudes by a given amount. /// Extrudes by a given amount.
pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> { pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
let (length, sketch_group) = args.get_number_sketch_group()?; let (length, sketch_group) = args.get_number_sketch_group()?;
@ -23,11 +23,7 @@ pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "extrude" name = "extrude"
}] }]
fn inner_extrude( fn inner_extrude(length: f64, sketch_group: SketchGroup, args: &mut Args) -> Result<ExtrudeGroup, KclError> {
length: f64,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<ExtrudeGroup, KclError> {
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
let cmd = kittycad::types::ModelingCmd::Extrude { let cmd = kittycad::types::ModelingCmd::Extrude {
@ -65,17 +61,15 @@ fn inner_get_extrude_wall_transform(
extrude_group: ExtrudeGroup, extrude_group: ExtrudeGroup,
args: &mut Args, args: &mut Args,
) -> Result<ExtrudeTransform, KclError> { ) -> Result<ExtrudeTransform, KclError> {
let surface = extrude_group let surface = extrude_group.get_path_by_name(surface_name).ok_or_else(|| {
.get_path_by_name(surface_name) KclError::Type(KclErrorDetails {
.ok_or_else(|| { message: format!(
KclError::Type(KclErrorDetails { "Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
message: format!( surface_name
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`", ),
surface_name source_ranges: vec![args.source_range],
), })
source_ranges: vec![args.source_range], })?;
})
})?;
Ok(ExtrudeTransform { Ok(ExtrudeTransform {
position: surface.get_position(), position: surface.get_position(),

View File

@ -1,9 +1,9 @@
//! Functions implemented for language execution. //! Functions implemented for language execution.
mod extrude; pub mod extrude;
mod segment; pub mod segment;
mod sketch; pub mod sketch;
mod utils; pub mod utils;
// TODO: Something that would be nice is if we could generate docs for Kcl based on the // TODO: Something that would be nice is if we could generate docs for Kcl based on the
// actual stdlib functions below. // actual stdlib functions below.
@ -27,8 +27,7 @@ pub type FnMap = HashMap<String, StdFn>;
pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>; pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>;
pub struct StdLib { pub struct StdLib {
#[allow(dead_code)] pub internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
pub fns: FnMap, pub fns: FnMap,
} }
@ -64,20 +63,16 @@ impl StdLib {
Box::new(crate::std::sketch::AngledLineThatIntersects), Box::new(crate::std::sketch::AngledLineThatIntersects),
Box::new(crate::std::sketch::StartSketchAt), Box::new(crate::std::sketch::StartSketchAt),
Box::new(crate::std::sketch::Close), Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::BezierCurve),
]; ];
let mut fns = HashMap::new(); let mut fns = HashMap::new();
for internal_fn_name in &internal_fn_names { for internal_fn_name in &internal_fn_names {
fns.insert( fns.insert(internal_fn_name.name().to_string(), internal_fn_name.std_lib_fn());
internal_fn_name.name().to_string(),
internal_fn_name.std_lib_fn(),
);
} }
Self { Self { internal_fn_names, fns }
internal_fn_names,
fns,
}
} }
} }
@ -95,22 +90,15 @@ pub struct Args<'a> {
} }
impl<'a> Args<'a> { impl<'a> Args<'a> {
pub fn new( pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, engine: &'a mut EngineConnection) -> Self {
args: Vec<MemoryItem>,
source_range: SourceRange,
engine: &'a mut EngineConnection,
) -> Self {
Self { Self {
args, args,
source_range, source_range,
engine, engine,
} }
} }
pub fn send_modeling_cmd(
&mut self, pub fn send_modeling_cmd(&mut self, id: uuid::Uuid, cmd: kittycad::types::ModelingCmd) -> Result<(), KclError> {
id: uuid::Uuid,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
self.engine.send_modeling_cmd(id, self.source_range, cmd) self.engine.send_modeling_cmd(id, self.source_range, cmd)
} }
@ -124,14 +112,14 @@ impl<'a> Args<'a> {
} }
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> { fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
self.make_user_val_from_json(serde_json::Value::Number( self.make_user_val_from_json(serde_json::Value::Number(serde_json::Number::from_f64(f).ok_or_else(
serde_json::Number::from_f64(f).ok_or_else(|| { || {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!("Failed to convert `{}` to a number", f), message: format!("Failed to convert `{}` to a number", f),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?, },
)) )?))
} }
fn get_number_array(&self) -> Result<Vec<f64>, KclError> { fn get_number_array(&self) -> Result<Vec<f64>, KclError> {
@ -164,10 +152,7 @@ impl<'a> Args<'a> {
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -177,20 +162,14 @@ impl<'a> Args<'a> {
s.to_string() s.to_string()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
let second_value = self.args.get(1).ok_or_else(|| { let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?; })?;
@ -199,10 +178,7 @@ impl<'a> Args<'a> {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -213,10 +189,7 @@ impl<'a> Args<'a> {
fn get_sketch_group(&self) -> Result<SketchGroup, KclError> { fn get_sketch_group(&self) -> Result<SketchGroup, KclError> {
let first_value = self.args.first().ok_or_else(|| { let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
"Expected a SketchGroup as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?; })?;
@ -225,10 +198,7 @@ impl<'a> Args<'a> {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
"Expected a SketchGroup as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -242,10 +212,7 @@ impl<'a> Args<'a> {
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
"Expected a struct as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -261,18 +228,13 @@ impl<'a> Args<'a> {
Ok(data) Ok(data)
} }
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>( fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, SketchGroup), KclError> {
&self,
) -> Result<(T, SketchGroup), KclError> {
let first_value = self let first_value = self
.args .args
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
"Expected a struct as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -287,10 +249,7 @@ impl<'a> Args<'a> {
let second_value = self.args.get(1).ok_or_else(|| { let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?; })?;
@ -299,10 +258,7 @@ impl<'a> Args<'a> {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -310,9 +266,7 @@ impl<'a> Args<'a> {
Ok((data, sketch_group)) Ok((data, sketch_group))
} }
fn get_segment_name_to_number_sketch_group( fn get_segment_name_to_number_sketch_group(&self) -> Result<(String, f64, SketchGroup), KclError> {
&self,
) -> Result<(String, f64, SketchGroup), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value. // Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a number. // The second argument should be a number.
// The third argument should be a SketchGroup. // The third argument should be a SketchGroup.
@ -321,10 +275,7 @@ impl<'a> Args<'a> {
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -334,10 +285,7 @@ impl<'a> Args<'a> {
s.to_string() s.to_string()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -347,10 +295,7 @@ impl<'a> Args<'a> {
.get(1) .get(1)
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a number as the second argument, found `{:?}`", self.args),
"Expected a number as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -360,10 +305,7 @@ impl<'a> Args<'a> {
let third_value = self.args.get(2).ok_or_else(|| { let third_value = self.args.get(2).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
"Expected a SketchGroup as the third argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?; })?;
@ -372,10 +314,7 @@ impl<'a> Args<'a> {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the third argument, found `{:?}`", self.args),
"Expected a SketchGroup as the third argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -391,10 +330,7 @@ impl<'a> Args<'a> {
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a number as the first argument, found `{:?}`", self.args),
"Expected a number as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -404,10 +340,7 @@ impl<'a> Args<'a> {
let second_value = self.args.get(1).ok_or_else(|| { let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})?; })?;
@ -416,10 +349,7 @@ impl<'a> Args<'a> {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
"Expected a SketchGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -435,10 +365,7 @@ impl<'a> Args<'a> {
.first() .first()
.ok_or_else(|| { .ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
}) })
})? })?
@ -448,10 +375,7 @@ impl<'a> Args<'a> {
s.to_string() s.to_string()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!( message: format!("Expected a string as the first argument, found `{:?}`", self.args),
"Expected a string as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -589,11 +513,7 @@ mod tests {
continue; continue;
} }
buf.push_str(&format!( buf.push_str(&format!("\t* [`{}`](#{})\n", internal_fn.name(), internal_fn.name()));
"\t* [`{}`](#{})\n",
internal_fn.name(),
internal_fn.name()
));
} }
buf.push_str("\n\n"); buf.push_str("\n\n");
@ -617,25 +537,15 @@ mod tests {
fn_docs.push_str(&format!("{}\n\n", internal_fn.description())); fn_docs.push_str(&format!("{}\n\n", internal_fn.description()));
fn_docs.push_str("```\n"); fn_docs.push_str("```\n");
fn_docs.push_str(&format!("{}(", internal_fn.name())); let signature = internal_fn.fn_signature();
for (i, arg) in internal_fn.args().iter().enumerate() { fn_docs.push_str(&signature);
if i > 0 {
fn_docs.push_str(", ");
}
fn_docs.push_str(&format!("{}: {}", arg.name, arg.type_));
}
fn_docs.push_str(") -> ");
fn_docs.push_str(&internal_fn.return_value().type_);
fn_docs.push_str("\n```\n\n"); fn_docs.push_str("\n```\n\n");
fn_docs.push_str("#### Arguments\n\n"); fn_docs.push_str("#### Arguments\n\n");
for arg in internal_fn.args() { for arg in internal_fn.args() {
let (format, should_be_indented) = arg.get_type_string().unwrap(); let (format, should_be_indented) = arg.get_type_string().unwrap();
if let Some(description) = arg.description() { if let Some(description) = arg.description() {
fn_docs.push_str(&format!( fn_docs.push_str(&format!("* `{}`: `{}` - {}\n", arg.name, arg.type_, description));
"* `{}`: `{}` - {}\n",
arg.name, arg.type_, description
));
} else { } else {
fn_docs.push_str(&format!("* `{}`: `{}`\n", arg.name, arg.type_)); fn_docs.push_str(&format!("* `{}`: `{}`\n", arg.name, arg.type_));
} }
@ -663,7 +573,7 @@ mod tests {
buf.push_str(&fn_docs); buf.push_str(&fn_docs);
} }
expectorate::assert_contents("../../docs/kcl.md", &buf); expectorate::assert_contents("../../../docs/kcl.md", &buf);
} }
#[test] #[test]
@ -677,7 +587,7 @@ mod tests {
} }
expectorate::assert_contents( expectorate::assert_contents(
"../../docs/kcl.json", "../../../docs/kcl.json",
&serde_json::to_string_pretty(&json_data).unwrap(), &serde_json::to_string_pretty(&json_data).unwrap(),
); );
} }

View File

@ -1,15 +1,15 @@
//! Functions related to line segments. //! Functions related to line segments.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup}, executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args}, std::{utils::get_angle, Args},
}; };
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
/// Returns the segment end of x. /// Returns the segment end of x.
pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> { pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?; let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
@ -22,22 +22,16 @@ pub fn segment_end_x(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "segEndX", name = "segEndX",
}] }]
fn inner_segment_end_x( fn inner_segment_end_x(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
segment_name: &str, let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
sketch_group: SketchGroup, KclError::Type(KclErrorDetails {
args: &mut Args, message: format!(
) -> Result<f64, KclError> { "Expected a segment name that exists in the given SketchGroup, found `{}`",
let line = sketch_group segment_name
.get_base_by_name_or_start(segment_name) ),
.ok_or_else(|| { source_ranges: vec![args.source_range],
KclError::Type(KclErrorDetails { })
message: format!( })?;
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[0]) Ok(line.to[0])
} }
@ -54,22 +48,16 @@ pub fn segment_end_y(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "segEndY", name = "segEndY",
}] }]
fn inner_segment_end_y( fn inner_segment_end_y(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
segment_name: &str, let line = sketch_group.get_base_by_name_or_start(segment_name).ok_or_else(|| {
sketch_group: SketchGroup, KclError::Type(KclErrorDetails {
args: &mut Args, message: format!(
) -> Result<f64, KclError> { "Expected a segment name that exists in the given SketchGroup, found `{}`",
let line = sketch_group segment_name
.get_base_by_name_or_start(segment_name) ),
.ok_or_else(|| { source_ranges: vec![args.source_range],
KclError::Type(KclErrorDetails { })
message: format!( })?;
"Expected a segment name that exists in the given SketchGroup, found `{}`",
segment_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(line.to[1]) Ok(line.to[1])
} }
@ -145,11 +133,7 @@ pub fn segment_length(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "segLen", name = "segLen",
}] }]
fn inner_segment_length( fn inner_segment_length(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| { let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!(
@ -178,11 +162,7 @@ pub fn segment_angle(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "segAng", name = "segAng",
}] }]
fn inner_segment_angle( fn inner_segment_angle(segment_name: &str, sketch_group: SketchGroup, args: &mut Args) -> Result<f64, KclError> {
segment_name: &str,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<f64, KclError> {
let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| { let path = sketch_group.get_path_by_name(segment_name).ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!( message: format!(

View File

@ -1,5 +1,6 @@
//! Functions related to sketching. //! Functions related to sketching.
use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kittycad::types::{ModelingCmd, Point3D}; use kittycad::types::{ModelingCmd, Point3D};
use schemars::JsonSchema; use schemars::JsonSchema;
@ -9,13 +10,11 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup}, executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
std::{ std::{
utils::{get_x_component, get_y_component, intersection_with_parallel_line}, utils::{arc_angles, arc_center_and_end, get_x_component, get_y_component, intersection_with_parallel_line},
Args, Args,
}, },
}; };
use anyhow::Result;
/// Data to draw a line to a point. /// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
@ -44,11 +43,7 @@ pub fn line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "lineTo", name = "lineTo",
}] }]
fn inner_line_to( fn inner_line_to(data: LineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let to = match data { let to = match data {
LineToData::PointWithTag { to, .. } => to, LineToData::PointWithTag { to, .. } => to,
@ -56,6 +51,21 @@ fn inner_line_to(
}; };
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Line {
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let current_path = Path::ToPoint { let current_path = Path::ToPoint {
base: BasePath { base: BasePath {
from: from.into(), from: from.into(),
@ -106,18 +116,11 @@ pub fn x_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "xLineTo", name = "xLineTo",
}] }]
fn inner_x_line_to( fn inner_x_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data { let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [to, from.y], tag },
to: [to, from.y],
tag,
},
AxisLineToData::Point(data) => LineToData::Point([data, from.y]), AxisLineToData::Point(data) => LineToData::Point([data, from.y]),
}; };
@ -138,18 +141,11 @@ pub fn y_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "yLineTo", name = "yLineTo",
}] }]
fn inner_y_line_to( fn inner_y_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data { let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [from.x, to], tag },
to: [from.x, to],
tag,
},
AxisLineToData::Point(data) => LineToData::Point([from.x, data]), AxisLineToData::Point(data) => LineToData::Point([from.x, data]),
}; };
@ -207,11 +203,7 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "line", name = "line",
}] }]
fn inner_line( fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: LineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0]; let default = [0.2, 1.0];
@ -289,11 +281,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "xLine", name = "xLine",
}] }]
fn inner_x_line( fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let line_data = match data { let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([length, 0.0]), to: PointOrDefault::Point([length, 0.0]),
@ -318,11 +306,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib { #[stdlib {
name = "yLine", name = "yLine",
}] }]
fn inner_y_line( fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
let line_data = match data { let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag { AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([0.0, length]), to: PointOrDefault::Point([0.0, length]),
@ -373,9 +357,7 @@ fn inner_angled_line(
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let (angle, length) = match &data { let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length), AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => { AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
(angle_and_length[0], angle_and_length[1])
}
}; };
let to: [f64; 2] = [ let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0), from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
@ -424,9 +406,7 @@ fn inner_angled_line_of_x_length(
) -> Result<SketchGroup, KclError> { ) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data { let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length), AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => { AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
(angle_and_length[0], angle_and_length[1])
}
}; };
let to = get_y_component(angle, length); let to = get_y_component(angle, length);
@ -494,10 +474,7 @@ fn inner_angled_line_to_x(
let new_sketch_group = inner_line_to( let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data { if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag { LineToData::PointWithTag { to: [x_to, y_to], tag }
to: [x_to, y_to],
tag,
}
} else { } else {
LineToData::Point([x_to, y_to]) LineToData::Point([x_to, y_to])
}, },
@ -527,9 +504,7 @@ fn inner_angled_line_of_y_length(
) -> Result<SketchGroup, KclError> { ) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data { let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length), AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => { AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
(angle_and_length[0], angle_and_length[1])
}
}; };
let to = get_x_component(angle, length); let to = get_x_component(angle, length);
@ -579,10 +554,7 @@ fn inner_angled_line_to_y(
let new_sketch_group = inner_line_to( let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data { if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag { LineToData::PointWithTag { to: [x_to, y_to], tag }
to: [x_to, y_to],
tag,
}
} else { } else {
LineToData::Point([x_to, y_to]) LineToData::Point([x_to, y_to])
}, },
@ -610,8 +582,7 @@ pub struct AngeledLineThatIntersectsData {
/// Draw an angled line that intersects with a given line. /// Draw an angled line that intersects with a given line.
pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> { pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) = let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) = args.get_data_and_sketch_group()?;
args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?; let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group)) Ok(MemoryItem::SketchGroup(new_sketch_group))
} }
@ -760,13 +731,255 @@ fn inner_close(sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup
Ok(new_sketch_group) Ok(new_sketch_group)
} }
/// Data to draw an arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum ArcData {
/// Angles and radius with a tag.
AnglesAndRadiusWithTag {
/// The start angle.
angle_start: f64,
/// The end angle.
angle_end: f64,
/// The radius.
radius: f64,
/// The tag.
tag: String,
},
/// Angles and radius.
AnglesAndRadius {
/// The start angle.
angle_start: f64,
/// The end angle.
angle_end: f64,
/// The radius.
radius: f64,
},
/// Center, to and radius with a tag.
CenterToRadiusWithTag {
/// The center.
center: [f64; 2],
/// The to point.
to: [f64; 2],
/// The radius.
radius: f64,
/// The tag.
tag: String,
},
/// Center, to and radius.
CenterToRadius {
/// The center.
center: [f64; 2],
/// The to point.
to: [f64; 2],
/// The radius.
radius: f64,
},
}
/// Draw an arc.
pub fn arc(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (ArcData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_arc(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw an arc.
#[stdlib {
name = "arc",
}]
fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadiusWithTag {
angle_start,
angle_end,
radius,
..
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
}
ArcData::AnglesAndRadius {
angle_start,
angle_end,
radius,
} => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius);
(center, *angle_start, *angle_end, *radius, end)
}
ArcData::CenterToRadiusWithTag { center, to, radius, .. } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
ArcData::CenterToRadius { center, to, radius } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into())
}
};
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc {
angle_start,
angle_end,
center: center.into(),
radius,
},
},
)?;
// Move the path pen to the end of the arc.
// Since that is where we want to draw the next path.
// TODO: the engine should automatically move the pen to the end of the arc.
// This just seems inefficient.
args.send_modeling_cmd(
id,
ModelingCmd::MovePathPen {
path: sketch_group.id,
to: Point3D {
x: end.x,
y: end.y,
z: 0.0,
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: end.into(),
name: match data {
ArcData::AnglesAndRadiusWithTag { tag, .. } => tag.to_string(),
ArcData::AnglesAndRadius { .. } => "".to_string(),
ArcData::CenterToRadiusWithTag { tag, .. } => tag.to_string(),
ArcData::CenterToRadius { .. } => "".to_string(),
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
/// Data to draw a bezier curve.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum BezierData {
/// Points with a tag.
PointsWithTag {
/// The to point.
to: [f64; 2],
/// The first control point.
control1: [f64; 2],
/// The second control point.
control2: [f64; 2],
/// The tag.
tag: String,
},
/// Points.
Points {
/// The to point.
to: [f64; 2],
/// The first control point.
control1: [f64; 2],
/// The second control point.
control2: [f64; 2],
},
}
/// Draw a bezier curve.
pub fn bezier_curve(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (BezierData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_bezier_curve(data, sketch_group, args)?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a bezier curve.
#[stdlib {
name = "bezierCurve",
}]
fn inner_bezier_curve(data: BezierData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let (to, control1, control2) = match &data {
BezierData::PointsWithTag {
to, control1, control2, ..
} => (to, control1, control2),
BezierData::Points { to, control1, control2 } => (to, control1, control2),
};
let to = [from.x + to[0], from.y + to[1]];
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(
id,
ModelingCmd::ExtendPath {
path: sketch_group.id,
segment: kittycad::types::PathSegment::Bezier {
control1: Point3D {
x: from.x + control1[0],
y: from.y + control1[1],
z: 0.0,
},
control2: Point3D {
x: from.x + control2[0],
y: from.y + control2[1],
z: 0.0,
},
end: Point3D {
x: to[0],
y: to[1],
z: 0.0,
},
},
},
)?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to,
name: if let BezierData::PointsWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.value.push(current_path);
Ok(new_sketch_group)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::std::sketch::{LineData, PointOrDefault};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
#[test] #[test]
fn test_deserialize_line_data() { fn test_deserialize_line_data() {
let mut str_json = "\"default\"".to_string(); let mut str_json = "\"default\"".to_string();

View File

@ -1,3 +1,8 @@
use crate::{
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
};
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 { pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let x = b[0] - a[0]; let x = b[0] - a[0];
let y = b[1] - a[1]; let y = b[1] - a[1];
@ -44,7 +49,10 @@ pub fn normalize_rad(angle: f64) -> f64 {
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// assert_eq!(delta_angle(std::f64::consts::PI/8.0, std::f64::consts::PI/4.0), std::f64::consts::PI/8.0); /// assert_eq!(
/// kcl_lib::std::utils::delta_angle(std::f64::consts::PI / 8.0, std::f64::consts::PI / 4.0),
/// std::f64::consts::PI / 8.0
/// );
/// ``` /// ```
#[allow(dead_code)] #[allow(dead_code)]
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 { pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
@ -69,8 +77,14 @@ pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[0.0, 5.0]), 5.0); /// assert_eq!(
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[3.0, 4.0]), 5.0); /// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]),
/// 5.0
/// );
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]),
/// 5.0
/// );
/// ``` /// ```
#[allow(dead_code)] #[allow(dead_code)]
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 { pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
@ -82,11 +96,7 @@ pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 {
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt() ((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
} }
pub fn calculate_intersection_of_two_lines( pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
line1: &[[f64; 2]; 2],
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
let line2_point_b = [ let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0, line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0, line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
@ -117,27 +127,17 @@ pub fn intersection_with_parallel_line(
line2_angle: f64, line2_angle: f64,
line2_point: [f64; 2], line2_point: [f64; 2],
) -> [f64; 2] { ) -> [f64; 2] {
calculate_intersection_of_two_lines( calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
&offset_line(line1_offset, line1[0], line1[1]),
line2_angle,
line2_point,
)
} }
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] { fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] {
if p1[0] == p2[0] { if p1[0] == p2[0] {
let direction = (p1[1] - p2[1]).signum(); let direction = (p1[1] - p2[1]).signum();
return [ return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
[p1[0] + offset * direction, p1[1]],
[p2[0] + offset * direction, p2[1]],
];
} }
if p1[1] == p2[1] { if p1[1] == p2[1] {
let direction = (p2[0] - p1[0]).signum(); let direction = (p2[0] - p1[0]).signum();
return [ return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]];
[p1[0], p1[1] + offset * direction],
[p2[0], p2[1] + offset * direction],
];
} }
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0])); let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0]));
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]] [[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
@ -165,12 +165,81 @@ pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] {
[sign * x_component, sign * y_component] [sign * x_component, sign * y_component]
} }
pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f64, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle_deg * (std::f64::consts::PI / 180.0);
let end_angle = end_angle_deg * (std::f64::consts::PI / 180.0);
let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x),
y: -1.0 * (radius * start_angle.sin() - from.y),
};
let end = Point2d {
x: center.x + radius * end_angle.cos(),
y: center.y + radius * end_angle.sin(),
};
(center, end)
}
pub fn arc_angles(
from: &Point2d,
to: &Point2d,
center: &Point2d,
radius: f64,
source_range: SourceRange,
) -> Result<(f64, f64), KclError> {
// First make sure that the points are on the circumference of the circle.
// If not, we'll return an error.
if !is_on_circumference(center, from, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
from, center, radius
),
source_ranges: vec![source_range],
}));
}
if !is_on_circumference(center, to, radius) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
to, center, radius
),
source_ranges: vec![source_range],
}));
}
let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x);
let start_angle_deg = start_angle * (180.0 / std::f64::consts::PI);
let end_angle_deg = end_angle * (180.0 / std::f64::consts::PI);
Ok((start_angle_deg, end_angle_deg))
}
pub fn is_on_circumference(center: &Point2d, point: &Point2d, radius: f64) -> bool {
let dx = point.x - center.x;
let dy = point.y - center.y;
let distance_squared = dx.powi(2) + dy.powi(2);
// We'll check if the distance squared is approximately equal to radius squared.
// Due to potential floating point inaccuracies, we'll check if the difference
// is very small (e.g., 1e-9) rather than checking for strict equality.
(distance_squared - radius.powi(2)).abs() < 1e-9
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// Here you can bring your functions into scope // Here you can bring your functions into scope
use super::{get_x_component, get_y_component};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component};
use crate::executor::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [ static EACH_QUAD: [(i32, [i32; 2]); 12] = [
(-315, [1, 1]), (-315, [1, 1]),
(-225, [-1, 1]), (-225, [-1, 1]),
@ -245,4 +314,77 @@ mod tests {
assert!((result[0] - 0.0).abs() < f64::EPSILON); assert!((result[0] - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, -1); assert_eq!(result[1] as i32, -1);
} }
#[test]
fn test_arc_center_and_end() {
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 90.0, 1.0);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -1.0);
assert_eq!(end.y, 1.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 1.0);
assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -2.0);
assert_eq!(end.y.round(), 0.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 10.0);
assert_eq!(center.x.round(), -10.0);
assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -20.0);
assert_eq!(end.y.round(), 0.0);
}
#[test]
fn test_arc_angles() {
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 1.0 },
&super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 90.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -2.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 0.0 },
1.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -20.0, y: 0.0 },
&super::Point2d { x: -10.0, y: 0.0 },
10.0,
SourceRange(Default::default()),
)
.unwrap();
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
let result = super::arc_angles(
&super::Point2d { x: 0.0, y: 5.0 },
&super::Point2d { x: 5.0, y: 5.0 },
&super::Point2d { x: 10.0, y: -10.0 },
10.0,
SourceRange(Default::default()),
);
if let Err(err) = result {
assert!(err.to_string().contains( "Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
} else {
panic!("Expected error");
}
assert_eq!(angle_start.round(), 0.0);
assert_eq!(angle_end.round(), 180.0);
}
} }

View File

@ -1,8 +1,6 @@
use gloo_utils::format::JsValueSerdeExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS)] #[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)] #[ts(export)]
@ -48,8 +46,7 @@ lazy_static! {
static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap(); static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap(); static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap(); static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref OPERATOR: Regex = static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap(); static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
static ref BLOCK_END: Regex = Regex::new(r"^\}").unwrap(); static ref BLOCK_END: Regex = Regex::new(r"^\}").unwrap();
static ref PARAN_START: Regex = Regex::new(r"^\(").unwrap(); static ref PARAN_START: Regex = Regex::new(r"^\(").unwrap();
@ -116,9 +113,7 @@ fn is_block_comment(character: &str) -> bool {
} }
fn match_first(str: &str, regex: &Regex) -> Option<String> { fn match_first(str: &str, regex: &Regex) -> Option<String> {
regex regex.find(str).map(|the_match| the_match.as_str().to_string())
.find(str)
.map(|the_match| the_match.as_str().to_string())
} }
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token { fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
@ -253,11 +248,7 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
} }
pub fn lexer(str: &str) -> Vec<Token> { pub fn lexer(str: &str) -> Vec<Token> {
fn recursively_tokenise( fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
str: &str,
current_index: usize,
previous_tokens: Vec<Token>,
) -> Vec<Token> {
if current_index >= str.len() { if current_index >= str.len() {
return previous_tokens; return previous_tokens;
} }
@ -273,19 +264,12 @@ pub fn lexer(str: &str) -> Vec<Token> {
recursively_tokenise(str, 0, Vec::new()) recursively_tokenise(str, 0, Vec::new())
} }
// wasm_bindgen wrapper for lexer
// test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
#[wasm_bindgen]
pub fn lexer_js(str: &str) -> Result<JsValue, JsError> {
let tokens = lexer(str);
Ok(JsValue::from_serde(&tokens)?)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*;
#[test] #[test]
fn is_number_test() { fn is_number_test() {
assert!(is_number("1")); assert!(is_number("1"));

View File

@ -0,0 +1,6 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,159 +0,0 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine.
use anyhow::Result;
use futures::{SinkExt, StreamExt};
use kittycad::types::{OkWebSocketResponseData, WebSocketRequest, WebSocketResponse};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use crate::errors::{KclError, KclErrorDetails};
#[derive(Debug)]
pub struct EngineConnection {
tcp_write: futures::stream::SplitSink<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
WsMsg,
>,
tcp_read_handle: tokio::task::JoinHandle<()>,
}
impl Drop for EngineConnection {
fn drop(&mut self) {
// Drop the read handle.
self.tcp_read_handle.abort();
}
}
pub struct TcpRead {
stream: futures::stream::SplitStream<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
>,
}
impl TcpRead {
pub async fn read(&mut self) -> Result<WebSocketResponse> {
let msg = self.stream.next().await.unwrap()?;
let msg = match msg {
WsMsg::Text(text) => text,
WsMsg::Binary(bin) => bincode::deserialize(&bin)?,
other => anyhow::bail!("Unexpected websocket message from server: {}", other),
};
let msg = serde_json::from_str::<WebSocketResponse>(&msg)?;
Ok(msg)
}
}
impl EngineConnection {
pub async fn new(conn_str: &str, auth_token: &str, origin: &str) -> Result<EngineConnection> {
let method = http::Method::GET.to_string();
let key = tokio_tungstenite::tungstenite::handshake::client::generate_key();
// Establish a websocket connection.
let (ws_stream, _) = tokio_tungstenite::connect_async(httparse::Request {
method: Some(&method),
path: Some(conn_str),
// TODO pass in the origin from elsewhere.
headers: &mut websocket_headers(auth_token, &key, origin),
version: Some(1), // HTTP/1.1
})
.await?;
let (tcp_write, tcp_read) = ws_stream.split();
let mut tcp_read = TcpRead { stream: tcp_read };
let tcp_read_handle = tokio::spawn(async move {
// Get Websocket messages from API server
while let Ok(ws_resp) = tcp_read.read().await {
if !ws_resp.success {
println!("got ws errors: {:?}", ws_resp.errors);
continue;
}
if let Some(msg) = ws_resp.resp {
match msg {
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
println!("got ice server info: {:?}", ice_servers);
}
OkWebSocketResponseData::SdpAnswer { answer } => {
println!("got sdp answer: {:?}", answer);
}
OkWebSocketResponseData::TrickleIce { candidate } => {
println!("got trickle ice: {:?}", candidate);
}
OkWebSocketResponseData::Modeling { .. } => {}
OkWebSocketResponseData::Export { .. } => {}
}
}
}
});
Ok(EngineConnection {
tcp_write,
tcp_read_handle,
})
}
pub async fn tcp_send(&mut self, msg: WebSocketRequest) -> Result<()> {
let msg = serde_json::to_string(&msg)?;
self.tcp_write.send(WsMsg::Text(msg)).await?;
Ok(())
}
pub fn send_modeling_cmd(
&mut self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<(), KclError> {
futures::executor::block_on(
self.tcp_send(WebSocketRequest::ModelingCmdReq { cmd, cmd_id: id }),
)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
})?;
Ok(())
}
}
/// Headers for starting a websocket session with api-deux.
fn websocket_headers<'a>(
token: &'a str,
key: &'a str,
origin: &'a str,
) -> [httparse::Header<'a>; 6] {
[
httparse::Header {
name: "Authorization",
value: token.as_bytes(),
},
httparse::Header {
name: "Connection",
value: b"Upgrade",
},
httparse::Header {
name: "Upgrade",
value: b"websocket",
},
httparse::Header {
name: "Sec-WebSocket-Version",
value: b"13",
},
httparse::Header {
name: "Sec-WebSocket-Key",
value: key.as_bytes(),
},
httparse::Header {
name: "Host",
value: origin.as_bytes(),
},
]
}

View File

@ -1,49 +0,0 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError {
#[error("syntax: {0:?}")]
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
Unimplemented(KclErrorDetails),
#[error("unexpected: {0:?}")]
Unexpected(KclErrorDetails),
#[error("value already defined: {0:?}")]
ValueAlreadyDefined(KclErrorDetails),
#[error("undefined value: {0:?}")]
UndefinedValue(KclErrorDetails),
#[error("invalid expression: {0:?}")]
InvalidExpression(crate::math_parser::MathExpression),
#[error("engine: {0:?}")]
Engine(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
pub source_ranges: Vec<crate::executor::SourceRange>,
#[serde(rename = "msg")]
pub message: String,
}
/// 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.
impl From<KclError> for String {
fn from(error: KclError) -> Self {
serde_json::to_string(&error).unwrap()
}
}
impl From<String> for KclError {
fn from(error: String) -> Self {
serde_json::from_str(&error).unwrap()
}
}

View File

@ -1,25 +0,0 @@
//! Functions for exported files from the server.
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
let ws_resp: kittycad::types::WebSocketResponse = bincode::deserialize(data)?;
if !ws_resp.success {
return Err(JsError::new(&format!(
"Server returned error: {:?}",
ws_resp.errors
)));
}
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
return Ok(JsValue::from_serde(&files)?);
}
Err(JsError::new(&format!(
"Invalid response type, got: {:?}",
ws_resp
)))
}

View File

@ -1,11 +1,74 @@
mod abstract_syntax_tree_types; //! Wasm bindings for `kcl`.
mod docs;
mod engine; use gloo_utils::format::JsValueSerdeExt;
mod errors; use wasm_bindgen::prelude::*;
mod executor;
mod export; // wasm_bindgen wrapper for execute
mod math_parser; #[cfg(target_arch = "wasm32")]
mod parser; #[wasm_bindgen]
mod recast; pub async fn execute_wasm(
mod std; program_str: &str,
mod tokeniser; memory_str: &str,
manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
) -> Result<JsValue, String> {
// deserialize the ast from a stringified json
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let mut engine = kcl_lib::engine::EngineConnection::new(manager)
.await
.map_err(|e| format!("{:?}", e))?;
let memory = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)
.map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&memory).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn deserialize_files(data: &[u8]) -> Result<JsValue, JsError> {
let ws_resp: kittycad::types::WebSocketResponse = bson::from_slice(data)?;
if let Some(success) = ws_resp.success {
if !success {
return Err(JsError::new(&format!("Server returned error: {:?}", ws_resp.errors)));
}
}
if let Some(kittycad::types::OkWebSocketResponseData::Export { files }) = ws_resp.resp {
return Ok(JsValue::from_serde(&files)?);
}
Err(JsError::new(&format!("Invalid response type, got: {:?}", ws_resp)))
}
// wasm_bindgen wrapper for lexer
// test for this function and by extension lexer are done in javascript land src/lang/tokeniser.test.ts
#[wasm_bindgen]
pub fn lexer_js(js: &str) -> Result<JsValue, JsError> {
let tokens = kcl_lib::tokeniser::lexer(js);
Ok(JsValue::from_serde(&tokens)?)
}
#[wasm_bindgen]
pub fn parse_js(js: &str) -> Result<JsValue, String> {
let tokens = kcl_lib::tokeniser::lexer(js);
let program = kcl_lib::parser::abstract_syntax_tree(&tokens).map_err(String::from)?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&program).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for recast
// test for this function and by extension the recaster are done in javascript land src/lang/recast.test.ts
#[wasm_bindgen]
pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
// deserialize the ast from a stringified json
let program: kcl_lib::abstract_syntax_tree_types::Program =
serde_json::from_str(json_str).map_err(JsError::from)?;
let result = kcl_lib::recast::recast(&program, "", false);
Ok(JsValue::from_serde(&result)?)
}

View File

@ -39,5 +39,7 @@ module.exports = {
}, },
}, },
darkMode: 'class', darkMode: 'class',
plugins: [], plugins: [
require('@headlessui/tailwindcss'),
],
} }

View File

@ -1642,6 +1642,11 @@
dependencies: dependencies:
client-only "^0.0.1" client-only "^0.0.1"
"@headlessui/tailwindcss@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz#2c55c98fd8eee4b4f21ec6eb35a014b840059eec"
integrity sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==
"@humanwhocodes/config-array@^0.11.10": "@humanwhocodes/config-array@^0.11.10":
version "0.11.10" version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
@ -1747,10 +1752,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.34": "@kittycad/lib@^0.0.35":
version "0.0.34" version "0.0.35"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.34.tgz#c1f1021f6c77bd9f47caa685cfbff0ef358a0316" resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
integrity sha512-9pUUuspJB/rayW4adfF7UqRYLw1pugBy3t0+V6qK3sWttG9flgv54fPw3JKewn7VFoEjRtNtoREMAoWb4ZrUIw== integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
dependencies: dependencies:
node-fetch "3.3.2" node-fetch "3.3.2"
openapi-types "^12.0.0" openapi-types "^12.0.0"
@ -1981,6 +1986,70 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz#31b9c510d8cada9683549e1dbb4284cca5001faf" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz#31b9c510d8cada9683549e1dbb4284cca5001faf"
integrity sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw== integrity sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==
"@sentry-internal/tracing@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.65.0.tgz#f7c56885d10c753ef03a25405dae13728916c0f5"
integrity sha512-TEYkiq5vKr1Y79YIu+UYr1sO3vEMttQOBsOZLziDbqiC7TvKUARBR4W5XWfb9qBVDeon87EFNKluW0/+7rzYWw==
dependencies:
"@sentry/core" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/browser@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.65.0.tgz#fb2009d6f8f1e5e3e1c616ce0ea70dd728c46ce7"
integrity sha512-TUzZPAXNJ/Y1yakFODYhsEtdDpLdkgjTfrx5i9MOnXQLrcRR0C4TC1KitqbP6Tv7Xha9WiR0TDZkh7gS/9RxEA==
dependencies:
"@sentry-internal/tracing" "7.65.0"
"@sentry/core" "7.65.0"
"@sentry/replay" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/core@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.65.0.tgz#01c1320b4e7c62ccf757258c1622d07cc743468a"
integrity sha512-EwZABW8CtAbRGXV69FqeCqcNApA+Jbq308dko0W+MFdFe+9t2RGubUkpPxpJcbWy/dN2j4LiuENu1T7nWn0ZAQ==
dependencies:
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/react@^7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.65.0.tgz#98c044bc2d7a99da7dfdef2686c3214d8f2f4ee0"
integrity sha512-1ABxHwEHw5J4avUr8TBch3l7UszbNIroWergwiLPSy+EJU8WuB3Fdx0zSU+hS4Sujf8HNcRgu1JyWThZFTnIMA==
dependencies:
"@sentry/browser" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
hoist-non-react-statics "^3.3.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/replay@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.65.0.tgz#e73a8a577c8b492c3f18ab769db15993b96e77fe"
integrity sha512-vhlk5F9RrhMQ+gOjNlLoWXamAPLNIT6wNII1O9ae+DRhZFmiUYirP5ag6dH5lljvNZndKl+xw+lJGJ3YdjXKlQ==
dependencies:
"@sentry/core" "7.65.0"
"@sentry/types" "7.65.0"
"@sentry/utils" "7.65.0"
"@sentry/types@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.65.0.tgz#f0f4e6583c631408d15ee5fb46901fd195fa1cc4"
integrity sha512-YYq7IDLLhpSBTmHoyWFtq/5ZDaEJ01r7xGuhB0aSIq33cm2I7im/B3ipzoOP/ukGZSIhuYVW9t531xZEO0+6og==
"@sentry/utils@7.65.0":
version "7.65.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.65.0.tgz#a7929c5b019fa33e819b08a99744fa27cd38c85f"
integrity sha512-2JEBf4jzRSClhp+LJpX/E3QgHEeKvXqFMeNhmwQ07qqd6szhfH2ckYFj4gXk6YiGGY4Act3C6oxLfdZovG71bw==
dependencies:
"@sentry/types" "7.65.0"
tslib "^2.4.1 || ^1.9.3"
"@sinclair/typebox@^0.27.8": "@sinclair/typebox@^0.27.8":
version "0.27.8" version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@ -3827,6 +3896,11 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gensync@^1.0.0-beta.2: gensync@^1.0.0-beta.2:
version "1.0.0-beta.2" version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@ -4021,7 +4095,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hoist-non-react-statics@^3.3.0: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -5753,6 +5827,11 @@ tslib@^2.0.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==
"tslib@^2.4.1 || ^1.9.3":
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@~2.4: tslib@~2.4:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"