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_BASE_URL=https://api.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_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_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224

View File

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

View File

@ -45,4 +45,7 @@ jobs:
shell: bash
run: |-
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:
runs-on: ubuntu-20.04
if: github.event_name == 'release'
permissions:
contents: write
needs: [build-test-web, build-apps]
env:
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
@ -189,3 +191,8 @@ jobs:
with:
path: last_update.json
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
src/wasm-lib/target
src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,8 @@
* [`angledLineThatIntersects`](#angledLineThatIntersects)
* [`startSketchAt`](#startSketchAt)
* [`close`](#close)
* [`arc`](#arc)
* [`bezierCurve`](#bezierCurve)
## 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",
"version": "0.1.0",
"version": "0.3.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@ -8,8 +8,10 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@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",
"@sentry/react": "^7.65.0",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
@ -22,6 +24,7 @@
"@xstate/react": "^3.2.2",
"crypto-js": "^4.1.1",
"formik": "^2.4.3",
"fuse.js": "^6.6.2",
"http-server": "^14.1.1",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
@ -54,15 +57,15 @@
"build:both:local": "yarn build:wasm && vite build",
"test": "vitest --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",
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
"simpleserver": "http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg --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\"",
"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",
"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",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.1"
@ -1150,7 +1156,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@ -1163,6 +1169,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.3.3"
@ -1378,7 +1390,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"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",
]
@ -1628,6 +1651,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]]
name = "miniz_oxide"
version = "0.6.2"
@ -2122,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64 0.21.2",
"indexmap",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
"serde",
@ -2695,14 +2724,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "2.3.3"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [
"base64 0.13.1",
"base64 0.21.2",
"chrono",
"hex",
"indexmap",
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
@ -2711,9 +2741,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "2.3.3"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [
"darling",
"proc-macro2",
@ -3022,6 +3052,7 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
dependencies = [
"anyhow",
"attohttpc",
"base64 0.21.2",
"cocoa",
"dirs-next",
"embed_plist",
@ -3034,6 +3065,7 @@ dependencies = [
"heck 0.4.1",
"http",
"ignore",
"minisign-verify",
"objc",
"once_cell",
"open",
@ -3055,19 +3087,21 @@ dependencies = [
"tauri-utils",
"tempfile",
"thiserror",
"time",
"tokio",
"url",
"uuid",
"webkit2gtk",
"webview2-com",
"windows 0.39.0",
"zip",
]
[[package]]
name = "tauri-build"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
dependencies = [
"anyhow",
"cargo_toml",
@ -3078,7 +3112,6 @@ dependencies = [
"serde_json",
"tauri-utils",
"tauri-winres",
"winnow",
]
[[package]]
@ -3176,12 +3209,13 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
dependencies = [
"brotli",
"ctor",
"dunce",
"glob",
"heck 0.4.1",
"html5ever",
@ -3397,7 +3431,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"nom8",
"serde",
"serde_spanned",
@ -3410,7 +3444,7 @@ version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"serde",
"serde_spanned",
"toml_datetime 0.6.2",
@ -4228,3 +4262,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"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
[build-dependencies]
tauri-build = { version = "1.3.0", features = [] }
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
anyhow = "1"
oauth2 = "4.4.1"
serde = { version = "1.0", features = ["derive"] }
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"] }
toml = "0.6.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,15 @@ import {
createBrowserRouter,
Outlet,
redirect,
useLocation,
RouterProvider,
} from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, {
@ -24,7 +31,47 @@ import {
} from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
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 =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -68,7 +115,11 @@ const addGlobalContextToElements = (
'element' in route
? {
...route,
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
element: (
<CommandBarProvider>
<GlobalStateProvider>{route.element}</GlobalStateProvider>
</CommandBarProvider>
),
}
: route
)
@ -95,26 +146,25 @@ const router = createBrowserRouter(
request,
params,
}): Promise<IndexLoaderData | Response> => {
const store = localStorage.getItem('store')
if (store === null) {
return redirect(paths.ONBOARDING.INDEX)
} 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
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
ContextFrom<typeof settingsMachine>
>
if (shouldRedirectToOnboarding) {
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
)
}
const status = persistedSettings.onboardingStatus || ''
const notEnRouteToOnboarding = !request.url.includes(
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') {
@ -164,9 +214,23 @@ const router = createBrowserRouter(
if (!isTauri()) {
return redirect(paths.FILE + '/new')
}
const projectDir = await initializeProjectDirectory()
const projectsNoMeta = (await readDir(projectDir.dir)).filter(
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
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
)
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 { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
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 = () => {
const {
@ -29,72 +34,26 @@ export const Toolbar = () => {
programMemory: s.programMemory,
}))
return (
<div>
{guiMode.mode === 'default' && (
<button
onClick={() => {
setGuiMode({
mode: 'sketch',
sketchMode: 'selectFace',
})
}}
>
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' && (
<>
useEffect(() => {
console.log('guiMode', guiMode)
}, [guiMode])
function ToolbarButtons() {
return (
<>
{guiMode.mode === 'default' && (
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
setGuiMode({
mode: 'sketch',
sketchMode: 'selectFace',
})
}}
>
ExtrudeSketch
Start Sketch
</button>
)}
{guiMode.mode === 'canEditExtrude' && (
<button
onClick={() => {
if (!ast) return
@ -102,77 +61,182 @@ export const Toolbar = () => {
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
false
programMemory
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
updateAst(modifiedAst)
}}
>
ExtrudeSketch (w/o pipe)
SketchOnFace
</button>
</>
)}
{guiMode.mode === 'sketch' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>
Exit sketch
</button>
)}
{toolTips
.filter(
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
(sketchFnName) => ['line'].includes(sketchFnName)
)
.map((sketchFnName) => {
if (
guiMode.mode !== 'sketch' ||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
)
return null
return (
)}
{(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
key={sketchFnName}
onClick={() =>
setGuiMode({
...guiMode,
...(guiMode.sketchMode === sketchFnName
? {
sketchMode: 'sketchEdit',
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
}
: {
sketchMode: sketchFnName,
isTooltip: true,
}),
})
}
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
>
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
ExtrudeSketch
</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)
)
})}
<br></br>
<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 />
</div>
.map((sketchFnName) => {
if (
guiMode.mode !== 'sketch' ||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
)
return null
return (
<button
key={sketchFnName}
onClick={() =>
setGuiMode({
...guiMode,
...(guiMode.sketchMode === sketchFnName
? {
sketchMode: 'sketchEdit',
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
}
: {
sketchMode: sketchFnName,
isTooltip: true,
}),
})
}
>
{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 = {
sm: 12,
md: 14.4,
lg: 18,
lg: 20,
xl: 28,
}
export interface ActionIconProps extends React.PropsWithChildren {
icon?: SolidIconDefinition | BrandIconDefinition
className?: string
bgClassName?: string
iconClassName?: string
size?: keyof typeof iconSizes
@ -20,6 +22,7 @@ export interface ActionIconProps extends React.PropsWithChildren {
export const ActionIcon = ({
icon = faCircleExclamation,
className,
bgClassName,
iconClassName,
size = 'md',
@ -28,7 +31,9 @@ export const ActionIcon = ({
return (
<div
className={
'p-1 w-fit inline-grid place-content-center ' +
`p-${
size === 'xl' ? '2' : '1'
} w-fit inline-grid place-content-center ${className} ` +
(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')
}
@ -40,7 +45,7 @@ export const ActionIcon = ({
height={iconSizes[size]}
className={
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 { ProjectWithEntryPointMetadata } from '../Router'
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 {
showToolbar?: boolean
@ -18,12 +19,18 @@ export const AppHeader = ({
className = '',
enableMenu = false,
}: AppHeaderProps) => {
const [user] = useAuthMachine((s) => s?.context?.user)
const {
auth: {
context: { user },
},
} = useGlobalStateContext()
return (
<header
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
}
>
@ -35,7 +42,11 @@ export const AppHeader = ({
</div>
)}
{/* If there are children, show them, otherwise show User menu */}
{children || <UserSidebarMenu user={user} />}
{children || (
<div className="ml-auto">
<UserSidebarMenu user={user} />
</div>
)}
</header>
)
}

View File

@ -1,10 +1,10 @@
.panel {
@apply relative overflow-auto z-0;
@apply bg-chalkboard-20/40;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50;
@apply bg-chalkboard-110/50 backdrop-blur-0;
}
.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 { useEffect } from 'react'
import { Themes, useStore } from '../useStore'
import { useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
const ReactJsonTypeHack = ReactJson as any

View File

@ -1,8 +1,9 @@
import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes, useStore } from '../useStore'
import { useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
import { Themes } from '../lib/theme'
interface MemoryPanelProps extends CollapsiblePanelProps {
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 { faHome } from '@fortawesome/free-solid-svg-icons'
import { ProjectWithEntryPointMetadata, paths } from '../Router'
import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'
import { Fragment } from 'react'
const ProjectSidebarMenu = ({
project,
@ -34,7 +35,7 @@ const ProjectSidebarMenu = ({
) : (
<Popover className="relative">
<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"
>
<img
@ -46,54 +47,77 @@ const ProjectSidebarMenu = ({
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
</span>
</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">
<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"
/>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 -translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
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>
<p
className="m-0 text-energy-10 text-mono"
data-testid="projectName"
>
{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()}
<div>
<p
className="m-0 text-energy-10 text-mono"
data-testid="projectName"
>
{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>
)}
</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 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>
</Popover.Panel>
</Popover.Panel>
</Transition>
</Popover>
)
}

View File

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

View File

@ -1,13 +1,13 @@
import { Popover } from '@headlessui/react'
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { Fragment, useState } from 'react'
import { paths } from '../Router'
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
import { useAuthMachine } from '../hooks/useAuthMachine'
import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type User = Models['User_type']
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate()
const [_, send] = useAuthMachine()
const {
auth: { send },
} = useGlobalStateContext()
// Fallback logic for displaying user's "name":
// 1. user.name
@ -59,82 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
Menu
</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">
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<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>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 translate-x-4"
as={Fragment}
>
<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">
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<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 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 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('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.Panel>
</Transition>
</Popover>
)
}

View File

@ -8,6 +8,9 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
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;
}
#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 {
@apply bg-chalkboard-10/50;
@apply bg-chalkboard-10/30;
}
.dark #code-mirror-override .cm-gutters {
@ -99,14 +109,24 @@ code {
}
#code-mirror-override .cm-cursor {
display: block;
width: 200px;
background: linear-gradient(
to right,
rgb(0, 55, 94) 0%,
#0084e2ff 2%,
#0084e255 5%,
transparent 100%
);
width: 1ch;
@apply bg-liquid-40 mix-blend-multiply;
animation: blink 2s ease-out infinite;
}
.dark #code-mirror-override .cm-cursor {
@apply bg-liquid-50;
}
@keyframes blink {
0%,
100% {
opacity: 0;
}
15% {
opacity: 0.75;
}
}
.react-json-view {

View File

@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
import './index.css'
import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast'
import { Themes, useStore } from './useStore'
import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook'
import { getSystemTheme } from './lib/getSystemTheme'
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(
<HotkeysProvider>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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
export class KCLError {

View File

@ -4,10 +4,10 @@ import {
ArtifactMap,
SourceRangeMap,
} 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 { 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'
export type SourceRange = [number, number]
@ -146,10 +146,8 @@ export const _executor = async (
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
parsed.kind === 'invalid_expression'
? [[parsed.start, parsed.end]]
: rangeTypeFix(parsed.sourceRanges)
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)

View File

@ -1,9 +1,14 @@
import { SourceRange } from 'lang/executor'
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 { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
interface ResultCommand {
type: 'result'
@ -22,16 +27,6 @@ export interface SourceRangeMap {
[key: string]: SourceRange
}
interface SelectionsArgs {
id: string
type: Selections['codeBasedSelections'][number]['type']
}
interface CursorSelectionsArgs {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
}
interface NewTrackArgs {
conn: EngineConnection
mediaStream: MediaStream
@ -45,7 +40,7 @@ type WebSocketResponse = Models['OkWebSocketResponseData_type']
export class EngineConnection {
websocket?: WebSocket
pc?: RTCPeerConnection
lossyDataChannel?: RTCDataChannel
unreliableDataChannel?: RTCDataChannel
private ready: boolean
@ -107,6 +102,11 @@ export class EngineConnection {
isReady() {
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
// establish the WebRTC connections.
//
@ -116,6 +116,44 @@ export class EngineConnection {
// TODO(paultag): make this safe to call multiple times, and figure out
// 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.binaryType = 'arraybuffer'
@ -129,6 +167,37 @@ export class EngineConnection {
})
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)
})
@ -162,7 +231,7 @@ export class EngineConnection {
} else {
console.error(`Error from server:`)
}
message.errors.forEach((error) => {
message?.errors?.forEach((error) => {
console.error(` - ${error.error_code}: ${error.message}`)
})
return
@ -191,6 +260,13 @@ export class EngineConnection {
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') {
let candidate = resp.data?.candidate
@ -220,9 +296,9 @@ export class EngineConnection {
// PeerConnection and waiting for events to fire our callbacks.
this.pc.addEventListener('connectionstatechange', (event) => {
// if (this.pc?.iceConnectionState === 'disconnected') {
// this.close()
// }
if (this.pc?.iceConnectionState === 'connected') {
iceSpan.resolve?.()
}
})
this.pc.addEventListener('icecandidate', (event) => {
@ -272,8 +348,142 @@ export class EngineConnection {
})
this.pc.addEventListener('track', (event) => {
console.log('received track', event)
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({
conn: this,
mediaStream: mediaStream,
@ -285,45 +495,48 @@ export class EngineConnection {
let connectionStarted = new Date()
this.pc.addEventListener('datachannel', (event) => {
this.lossyDataChannel = event.channel
this.unreliableDataChannel = event.channel
console.log('accepted lossy data channel', event.channel.label)
this.lossyDataChannel.addEventListener('open', (event) => {
console.log('lossy data channel opened', event)
console.log('accepted unreliable data channel', event.channel.label)
this.unreliableDataChannel.addEventListener('open', (event) => {
console.log('unreliable data channel opened', event)
if (this.shouldTrace()) {
dataChannelSpan.resolve?.()
}
this.onDataChannelOpen(this)
let timeToConnectMs = new Date().getTime() - connectionStarted.getTime()
console.log(`engine connection time to connect: ${timeToConnectMs}ms`)
this.onEngineConnectionOpen(this)
this.ready = true
})
this.lossyDataChannel.addEventListener('close', (event) => {
console.log('lossy data channel closed')
this.unreliableDataChannel.addEventListener('close', (event) => {
console.log('unreliable data channel closed')
this.close()
})
this.lossyDataChannel.addEventListener('error', (event) => {
console.log('lossy data channel error')
this.unreliableDataChannel.addEventListener('error', (event) => {
console.log('unreliable data channel error')
this.close()
})
})
this.onConnectionStarted(this)
}
send(message: object) {
send(message: object | string) {
// TODO(paultag): Add in logic to determine the connection state and
// take actions if needed?
this.websocket?.send(JSON.stringify(message))
this.websocket?.send(
typeof message === 'string' ? message : JSON.stringify(message)
)
}
close() {
this.websocket?.close()
this.pc?.close()
this.lossyDataChannel?.close()
this.unreliableDataChannel?.close()
this.websocket = undefined
this.pc = undefined
this.lossyDataChannel = undefined
this.unreliableDataChannel = undefined
this.onClose(this)
this.ready = false
@ -331,6 +544,23 @@ export class EngineConnection {
}
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 {
artifactMap: ArtifactMap = {}
@ -340,10 +570,17 @@ export class EngineCommandManager {
engineConnection?: EngineConnection
waitForReady: Promise<void> = new Promise(() => {})
private resolveReady = () => {}
onHoverCallback: (id?: string) => void = () => {}
onClickCallback: (selection?: SelectionsArgs) => void = () => {}
onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void =
() => {}
subscriptions: {
[event: string]: {
[localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
unreliableSubscriptions: {
[event: string]: {
[localUnsubscribeId: string]: (a: any) => void
}
} = {} as any
constructor({
setMediaStream,
setIsStreamReady,
@ -373,20 +610,28 @@ export class EngineCommandManager {
},
onConnectionStarted: (engineConnection) => {
engineConnection?.pc?.addEventListener('datachannel', (event) => {
let lossyDataChannel = event.channel
let unreliableDataChannel = event.channel
lossyDataChannel.addEventListener('message', (event) => {
const result: Models['OkModelingCmdResponse_type'] = JSON.parse(
event.data
unreliableDataChannel.addEventListener('message', (event) => {
const result: UnreliableResponses = JSON.parse(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', () => {
console.log('peer is not sending video to us')
this.engineConnection?.close()
this.engineConnection?.connect()
// this.engineConnection?.close()
// this.engineConnection?.connect()
})
setMediaStream(mediaStream)
@ -433,18 +678,11 @@ export class EngineCommandManager {
return
}
const modelingResponse = message.data.modeling_response
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse)
)
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') {
const resolve = command.resolve
this.artifactMap[id] = {
@ -453,6 +691,7 @@ export class EngineCommandManager {
}
resolve({
id,
data: modelingResponse,
})
} else {
this.artifactMap[id] = {
@ -468,21 +707,49 @@ export class EngineCommandManager {
this.artifactMap = {}
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() {
// this.websocket?.close()
// 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: {
otherSelections: Selections['otherSelections']
idBasedSelections: { type: string; id: string }[]
@ -507,32 +774,38 @@ export class EngineCommandManager {
cmd_id: uuidv4(),
})
}
sendSceneCommand(command: EngineCommand) {
sendSceneCommand(command: EngineCommand): Promise<any> {
if (!this.engineConnection?.isReady()) {
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
if (
cmd.type === 'camera_drag_move' &&
this.engineConnection?.lossyDataChannel
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
return
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
} else if (
cmd.type === 'highlight_set_entity' &&
this.engineConnection?.lossyDataChannel
this.engineConnection?.unreliableDataChannel
) {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
return
this.engineConnection?.unreliableDataChannel?.send(
JSON.stringify(command)
)
return Promise.resolve()
}
console.log('sending command', command)
// since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id)
}
sendModelingCommand({
id,
@ -541,15 +814,18 @@ export class EngineCommandManager {
}: {
id: string
range: SourceRange
command: EngineCommand
command: EngineCommand | string
}): Promise<any> {
this.sourceRangeMap[id] = range
if (!this.engineConnection?.isReady()) {
console.log('socket not ready')
return new Promise(() => {})
return Promise.resolve()
}
this.engineConnection?.send(command)
return this.handlePendingCommand(id)
}
handlePendingCommand(id: string) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
@ -575,10 +851,9 @@ export class EngineCommandManager {
if (commandStr === undefined) {
throw new Error('commandStr is undefined')
}
const command: EngineCommand = JSON.parse(commandStr)
const range: SourceRange = JSON.parse(rangeStr)
return this.sendModelingCommand({ id, range, command })
return this.sendModelingCommand({ id, range, command: commandStr })
}
commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]

View File

@ -1,8 +1,8 @@
import { lexer_js } from '../wasm-lib/pkg/wasm_lib'
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[]> {
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>(
input: RequestInfo,
init: RequestInit = {}
): Promise<JSON> {
const [token] = useAuthMachine((s) => s?.context?.token)
const headers = { ...init.headers } as Record<string, string>
if (token) {
headers.Authorization = `Bearer ${token}`
const credentials = 'include' as RequestCredentials
const res = await fetch(input, { ...init, credentials, headers })
return res.json()
}
const credentials = 'include' as RequestCredentials
const res = await fetch(input, { ...init, credentials, headers })
return res.json()
return fetcherWithToken
}

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 { useStore } from '../useStore'
import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
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
// Initializes the project directory and returns the path
export async function initializeProjectDirectory() {
export async function initializeProjectDirectory(directory: string) {
if (!isTauri()) {
throw new Error(
'initializeProjectDirectory() can only be called from a Tauri app'
)
}
const { defaultDir: projectDir, setDefaultDir } = useStore.getState()
if (projectDir && projectDir.dir.length > 0) {
const dirExists = await exists(projectDir.dir)
if (directory) {
const dirExists = await exists(directory)
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 = {
dir: appData + PROJECT_FOLDER,
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir)
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
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
}
@ -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
// Returns the path to the new file
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 { Models } from '@kittycad/lib'
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 {
user?: Models['User_type']
@ -9,16 +27,22 @@ export interface UserContext {
export type Events =
| {
type: 'logout'
type: 'Log out'
}
| {
type: 'tryLogin'
type: 'Log in'
token?: string
}
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
export const authCommandBarMeta: CommandBarMeta = {
'Log in': {
hide: 'both',
},
}
export const authMachine = createMachine<UserContext, Events>(
{
id: 'Auth',
@ -50,7 +74,7 @@ export const authMachine = createMachine<UserContext, Events>(
loggedIn: {
entry: ['goToIndexPage'],
on: {
logout: {
'Log out': {
target: 'loggedOut',
},
},
@ -58,10 +82,10 @@ export const authMachine = createMachine<UserContext, Events>(
loggedOut: {
entry: ['goToSignInPage'],
on: {
tryLogin: {
'Log in': {
target: 'checkIfLoggedIn',
actions: assign({
token: (context, event) => {
token: (_, event) => {
const token = event.token || ''
localStorage.setItem(TOKEN_PERSIST_KEY, 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,
preserveActionOrder: true,
context: { token: persistedToken },
context: {
token: persistedToken,
},
},
{
actions: {},
@ -91,12 +117,17 @@ async function getUser(context: UserContext) {
}
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
if (SKIP_AUTH) return LOCAL_USER
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json()
if ('error_code' in user) throw new Error(user.message)
return user
} catch (e) {
console.error(e)
}
}

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 { readDir, removeDir, renameFile } from '@tauri-apps/api/fs'
import { FormEvent, useEffect } from 'react'
import { removeDir, renameFile } from '@tauri-apps/api/fs'
import {
createNewProject,
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
isProjectDirectory,
PROJECT_ENTRYPOINT,
getProjectsInDir,
} from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton'
import {
faArrowDown,
faArrowUp,
faCircleDot,
faPlus,
} from '@fortawesome/free-solid-svg-icons'
import { useStore } from '../useStore'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
import { toast } from 'react-hot-toast'
import { AppHeader } from '../components/AppHeader'
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 { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
import Loading from '../components/Loading'
import { metadata } from 'tauri-plugin-fs-extra-api'
const DESC = ':desc'
import { useMachine } from '@xstate/react'
import { homeCommandMeta, homeMachine } from '../machines/homeMachine'
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,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
const Home = () => {
const [searchParams, setSearchParams] = useSearchParams()
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const { commands, setCommandBarOpen } = useCommandsContext()
const navigate = useNavigate()
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
const [isLoading, setIsLoading] = useState(true)
const [projects, setProjects] = useState(loadedProjects || [])
const { defaultDir, defaultProjectName } = useStore((s) => ({
defaultDir: s.defaultDir,
defaultProjectName: s.defaultProjectName,
}))
const {
settings: {
context: { defaultDirectory, 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(
async (projectDir = defaultDir) => {
const readProjects = (
await readDir(projectDir.dir, {
await createNewProject(context.defaultDirectory + '/' + name)
return `Successfully created "${name}"`
},
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,
})
).filter(isProjectDirectory)
const projectsWithMetadata = await Promise.all(
readProjects.map(async (p) => ({
entrypoint_metadata: await metadata(
p.path + '/' + PROJECT_ENTRYPOINT
),
...p,
}))
)
setProjects(projectsWithMetadata)
return `Successfully deleted "${event.data.name}"`
},
},
[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(() => {
refreshProjects(defaultDir).then(() => {
setIsLoading(false)
})
}, [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')
}
send({ type: 'assign', data: { defaultProjectName, defaultDirectory } })
}, [defaultDirectory, defaultProjectName, send])
async function handleRenameProject(
e: FormEvent<HTMLFormElement>,
@ -96,85 +142,14 @@ const Home = () => {
const { newProjectName } = Object.fromEntries(
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()
toast.success('Project renamed')
}
send('Rename project', {
data: { oldName: project.name, newName: newProjectName },
})
}
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
if (project.path) {
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
}
send('Delete project', { data: { name: project.name || '' } })
}
return (
@ -191,9 +166,9 @@ const Home = () => {
? 'text-chalkboard-80 dark:text-chalkboard-40'
: ''
}
onClick={() => setSearchParams(getNextSearchParams('name'))}
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
icon={{
icon: getSortIcon('name'),
icon: getSortIcon(sort, 'name'),
bgClassName: !sort?.includes('name')
? 'bg-liquid-50 dark:bg-liquid-70'
: '',
@ -207,17 +182,19 @@ const Home = () => {
<ActionButton
Element="button"
className={
!modifiedSelected
!isSortByModified
? 'text-chalkboard-80 dark:text-chalkboard-40'
: ''
}
onClick={() => setSearchParams(getNextSearchParams('modified'))}
onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified'))
}
icon={{
icon: sort ? getSortIcon('modified') : faArrowDown,
bgClassName: !modifiedSelected
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
bgClassName: !isSortByModified
? 'bg-liquid-50 dark:bg-liquid-70'
: '',
iconClassName: !modifiedSelected
iconClassName: !isSortByModified
? '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">
Are being saved at{' '}
<code className="text-liquid-80 dark:text-liquid-30">
{defaultDir.dir}
{defaultDirectory}
</code>
, which you can change in your <Link to="settings">Settings</Link>.
</p>
{isLoading ? (
{state.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading>
) : (
<>
@ -256,7 +233,7 @@ const Home = () => {
)}
<ActionButton
Element="button"
onClick={handleNewProject}
onClick={() => send('Create project')}
icon={{ icon: faPlus }}
>
New file

View File

@ -20,9 +20,25 @@ export default function Units() {
>
<h1 className="text-2xl font-bold">Camera</h1>
<p className="mt-6">
Moving the camera is easy. Just click and drag anywhere in the scene
to rotate the camera, or hold down the <kbd>Ctrl</kbd> key and drag to
pan the camera.
Moving the camera is easy! The controls are as you might expect:
</p>
<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>
<div className="flex justify-between mt-6">
<ActionButton

View File

@ -1,34 +1,21 @@
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 { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle'
import { useState } from 'react'
import { onboardingPaths, useDismiss, useNextClick } from '.'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export default function Units() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
const {
defaultUnitSystem: ogDefaultUnitSystem,
setDefaultUnitSystem: saveDefaultUnitSystem,
defaultBaseUnit: ogDefaultBaseUnit,
setDefaultBaseUnit: saveDefaultBaseUnit,
} = useStore((s) => ({
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()
}
settings: {
send,
context: { unitSystem, baseUnit },
},
} = useGlobalStateContext()
return (
<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"
onLabel="Metric"
name="settings-units"
checked={defaultUnitSystem === 'metric'}
onChange={(e) =>
setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial')
}
checked={unitSystem === UnitSystem.Metric}
onChange={(e) => {
const newUnitSystem = e.target.checked
? UnitSystem.Metric
: UnitSystem.Imperial
send({
type: 'Set Unit System',
data: { unitSystem: newUnitSystem },
})
}}
/>
</SettingsSection>
<SettingsSection
@ -55,10 +48,15 @@ export default function Units() {
<select
id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultBaseUnit}
onChange={(e) => setDefaultBaseUnit(e.target.value)}
value={baseUnit}
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}>
{unit}
</option>
@ -81,7 +79,7 @@ export default function Units() {
</ActionButton>
<ActionButton
Element="button"
onClick={handleNextClick}
onClick={next}
icon={{ icon: faArrowRight }}
>
Next: Camera

View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ import {
} from './lang/executor'
import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state'
import { BaseDirectory } from '@tauri-apps/api/fs'
import {
ArtifactMap,
SourceRangeMap,
@ -95,22 +94,14 @@ export type GuiModes =
position: Position
}
type UnitSystem = 'imperial' | 'metric'
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export const baseUnits: Record<UnitSystem, string[]> = {
export const baseUnits = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
}
} as const
interface DefaultDir {
base?: BaseDirectory
dir: string
}
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
@ -181,21 +172,8 @@ export interface StoreState {
streamHeight: number
}) => 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
setHomeShowMenu: (showMenu: boolean) => void
onboardingStatus: string
setOnboardingStatus: (status: string) => void
theme: Themes
setTheme: (theme: Themes) => void
isBannerDismissed: boolean
setBannerDismissed: (isBannerDismissed: boolean) => void
openPanes: PaneType[]
@ -205,8 +183,6 @@ export interface StoreState {
path: string
}[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void
debugPanel: boolean
setDebugPanel: (debugPanel: boolean) => void
}
let pendingAstUpdates: number[] = []
@ -385,18 +361,6 @@ export const useStore = create<StoreState>()(
defaultDir: {
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,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'],
@ -405,25 +369,13 @@ export const useStore = create<StoreState>()(
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
debugPanel: false,
setDebugPanel: (debugPanel) => set({ debugPanel }),
}),
{
name: 'store',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
[
'code',
'defaultDir',
'defaultProjectName',
'defaultUnitSystem',
'defaultBaseUnit',
'debugPanel',
'onboardingStatus',
'theme',
'openPanes',
].includes(key)
['code', '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"
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]]
name = "aho-corasick"
version = "1.0.4"
@ -109,6 +121,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.2"
@ -147,6 +165,18 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "block-buffer"
version = "0.10.4"
@ -156,6 +186,28 @@ dependencies = [
"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]]
name = "bumpalo"
version = "3.13.0"
@ -281,6 +333,16 @@ dependencies = [
"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]]
name = "core-foundation-sys"
version = "0.8.4"
@ -352,6 +414,20 @@ dependencies = [
"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]]
name = "diff"
version = "0.1.13"
@ -488,6 +564,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.28"
@ -606,28 +688,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "gloo-utils"
version = "0.2.0"
@ -693,6 +753,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.5"
@ -881,13 +947,41 @@ dependencies = [
]
[[package]]
name = "kittycad"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0443a9f76cee80d5a43d076028d3ce39d2f6f6b66fc5c1a0ce24f8d7caf733b9"
name = "kcl-lib"
version = "0.1.10"
dependencies = [
"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",
"chrono",
"data-encoding",
@ -896,6 +990,7 @@ dependencies = [
"phonenumber",
"schemars",
"serde",
"serde_bytes",
"serde_json",
"thiserror",
"url",
@ -1151,6 +1246,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "os_str_bytes"
version = "6.5.1"
@ -1244,9 +1345,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.12"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@ -1321,6 +1422,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@ -1433,7 +1540,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64",
"base64 0.21.2",
"bytes",
"encoding_rs",
"futures-core",
@ -1519,9 +1626,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.8"
version = "0.38.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49"
dependencies = [
"bitflags 2.4.0",
"errno",
@ -1542,13 +1649,25 @@ dependencies = [
"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]]
name = "rustls-pemfile"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
"base64",
"base64 0.21.2",
]
[[package]]
@ -1583,10 +1702,19 @@ dependencies = [
]
[[package]]
name = "schemars"
version = "0.8.12"
name = "schannel"
version = "0.1.22"
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 = [
"bigdecimal",
"bytes",
@ -1601,9 +1729,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.12"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
dependencies = [
"proc-macro2",
"quote",
@ -1627,6 +1755,29 @@ dependencies = [
"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]]
name = "semver"
version = "0.11.0"
@ -1647,18 +1798,27 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.186"
version = "1.0.187"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
checksum = "30a7fe14252655bd1e578af19f5fa00fe02fd0013b100ca6b49fde31c41bae4c"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.186"
name = "serde_bytes"
version = "0.11.12"
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 = [
"proc-macro2",
"quote",
@ -1682,6 +1842,7 @@ version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
@ -1925,6 +2086,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "task-local-extensions"
version = "0.1.4"
@ -2110,7 +2277,10 @@ checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
"tungstenite",
]
@ -2192,9 +2362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "ts-rs"
name = "ts-rs-json-value"
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 = [
"serde_json",
"thiserror",
@ -2205,7 +2376,8 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
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 = [
"Inflector",
"proc-macro2",
@ -2227,6 +2399,7 @@ dependencies = [
"httparse",
"log",
"rand",
"rustls",
"sha1",
"thiserror",
"url",
@ -2435,30 +2608,11 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
name = "wasm-lib"
version = "0.1.0"
dependencies = [
"anyhow",
"backtrace",
"bincode",
"derive-docs",
"expectorate",
"futures",
"gloo-file",
"bson",
"gloo-utils",
"http",
"httparse",
"js-sys",
"kcl-lib",
"kittycad",
"lazy_static",
"parse-display",
"pretty_assertions",
"regex",
"schemars",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-tungstenite",
"ts-rs",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
]
@ -2661,6 +2815,15 @@ dependencies = [
"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]]
name = "yaml-rust"
version = "0.4.5"

View File

@ -8,28 +8,11 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.75"
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 }
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
gloo-utils = "0.2.0"
http = "0.2.9"
httparse = { version = "1.8.0", optional = true }
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"] }
kcl-lib = { path = "kcl" }
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
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-futures = "0.4.37"
@ -37,17 +20,8 @@ wasm-bindgen-futures = "0.4.37"
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"] }
[features]
default = ["web"]
web = ["dep:gloo-file", "dep:js-sys"]
noweb = ["dep:futures", "dep:httparse", "dep:tokio", "dep:tokio-tungstenite"]
[workspace]
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"
version = "0.1.0"
edition = "2021"
license = "MIT"
# 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]
pub fn stdlib(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
@ -50,9 +47,7 @@ fn do_stdlib(
do_stdlib_inner(metadata, attr, item)
}
fn do_output(
res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>,
) -> proc_macro::TokenStream {
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
@ -207,11 +202,7 @@ fn do_stdlib_inner(
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let ty_string = ty
.to_string()
.replace('&', "")
.replace("mut", "")
.replace(' ', "");
let ty_string = ty.to_string().replace('&', "").replace("mut", "").replace(' ', "");
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
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 nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
return normalize_comment_string(s.value());
@ -508,10 +498,7 @@ mod tests {
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/lineTo.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
expectorate::assert_contents("tests/lineTo.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
@ -540,9 +527,6 @@ mod tests {
let _expected = quote! {};
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/min.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
expectorate::assert_contents("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 parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Map;
@ -56,7 +57,7 @@ pub trait ValueMeta {
macro_rules! impl_value_meta {
{$name:ident} => {
impl ValueMeta for $name {
impl crate::abstract_syntax_tree_types::ValueMeta for $name {
fn start(&self) -> usize {
self.start
}
@ -86,6 +87,8 @@ macro_rules! impl_value_meta {
};
}
pub(crate) use impl_value_meta;
impl Value {
pub fn start(&self) -> usize {
match self {
@ -192,16 +195,11 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => {
binary_expression.get_result(memory, pipe_info, stdlib, engine)
}
BinaryPart::CallExpression(call_expression) => {
call_expression.execute(memory, pipe_info, stdlib, engine)
}
BinaryPart::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, stdlib, engine),
BinaryPart::UnaryExpression(unary_expression) => {
// Return an error this should not happen.
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"UnaryExpression should not be a BinaryPart: {:?}",
unary_expression
),
message: format!("UnaryExpression should not be a BinaryPart: {:?}", unary_expression),
source_ranges: vec![unary_expression.into()],
}))
}
@ -313,10 +311,7 @@ impl CallExpression {
}
Value::PipeExpression(pipe_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeExpression not implemented here: {:?}",
pipe_expression
),
message: format!("PipeExpression not implemented here: {:?}", pipe_expression),
source_ranges: vec![pipe_expression.into()],
}));
}
@ -325,29 +320,20 @@ impl CallExpression {
.get(&pipe_info.index - 1)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution index out of bounds: {:?}",
pipe_info
),
message: format!("PipeSubstitution index out of bounds: {:?}", pipe_info),
source_ranges: vec![pipe_substitution.into()],
})
})?
.clone(),
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -363,14 +349,7 @@ impl CallExpression {
if pipe_info.is_in_pipe {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
@ -390,14 +369,7 @@ impl CallExpression {
pipe_info.index += 1;
pipe_info.previous_results.push(result);
execute_pipe_body(
memory,
&pipe_info.body.clone(),
pipe_info,
self.into(),
stdlib,
engine,
)
execute_pipe_body(memory, &pipe_info.body.clone(), pipe_info, self.into(), stdlib, engine)
} else {
Ok(result)
}
@ -412,11 +384,22 @@ pub struct VariableDeclaration {
pub start: usize,
pub end: usize,
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);
#[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)]
#[ts(export)]
#[serde(tag = "type")]
@ -533,28 +516,19 @@ impl ArrayExpression {
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -619,28 +593,19 @@ impl ObjectExpression {
}
Value::PipeSubstitution(pipe_substitution) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"PipeSubstitution not implemented here: {:?}",
pipe_substitution
),
message: format!("PipeSubstitution not implemented here: {:?}", pipe_substitution),
source_ranges: vec![pipe_substitution.into()],
}));
}
Value::MemberExpression(member_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"MemberExpression not implemented here: {:?}",
member_expression
),
message: format!("MemberExpression not implemented here: {:?}", member_expression),
source_ranges: vec![member_expression.into()],
}));
}
Value::FunctionExpression(function_expression) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"FunctionExpression not implemented here: {:?}",
function_expression
),
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
source_ranges: vec![function_expression.into()],
}));
}
@ -712,10 +677,7 @@ impl MemberExpression {
string
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected string literal for property name, found {:?}",
value
),
message: format!("Expected string literal for property name, found {:?}", value),
source_ranges: vec![literal.into()],
}));
}
@ -837,10 +799,7 @@ impl BinaryExpression {
}
}
pub fn parse_json_number_as_f64(
j: &serde_json::Value,
source_range: SourceRange,
) -> Result<f64, KclError> {
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {

View File

@ -44,6 +44,11 @@ impl StdLibFnArg {
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)]
pub fn description(&self) -> Option<String> {
get_description_string_from_schema(&self.schema)
@ -93,9 +98,24 @@ pub trait StdLibFn {
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 Some(metadata) = &o.metadata {
if let Some(description) = &metadata.description {
@ -107,7 +127,7 @@ fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Opti
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 {
schemars::schema::Schema::Object(o) => {
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(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
// Let's print out the object's properties.
return Ok((
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false));
} else if let Some(items) = &array_val.contains {
return Ok((
format!("[{}]", get_type_string_from_schema(items)?.0),
false,
));
return Ok((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)),
}
}
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 {}
impl EngineConnection {
pub async fn new(
_conn_str: &str,
_auth_token: &str,
_origin: &str,
) -> Result<EngineConnection> {
pub async fn new() -> Result<EngineConnection> {
Ok(EngineConnection {})
}

View File

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

View File

@ -2,25 +2,36 @@
use wasm_bindgen::prelude::*;
#[cfg(feature = "noweb")]
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(test))]
pub mod conn_noweb;
#[cfg(feature = "noweb")]
#[cfg(feature = "engine")]
pub mod conn;
#[cfg(not(target_arch = "wasm32"))]
#[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))]
pub mod conn_web;
#[cfg(feature = "web")]
#[cfg(feature = "engine")]
pub mod conn_wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub use conn_web::EngineConnection;
#[cfg(feature = "engine")]
pub use conn_wasm::EngineConnection;
#[cfg(test)]
pub mod conn_mock;
#[cfg(test)]
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;
#[derive(Debug)]
@ -31,27 +42,17 @@ pub struct EngineManager {
#[wasm_bindgen]
impl EngineManager {
#[cfg(feature = "web")]
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
#[cfg(feature = "engine")]
#[wasm_bindgen(constructor)]
pub async fn new(manager: conn_web::EngineCommandManager) -> EngineManager {
pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
EngineManager {
// This unwrap is safe because the connection is always created.
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> {
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())?;

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 serde::{Deserialize, Serialize};
#[cfg(not(test))]
use wasm_bindgen::prelude::*;
use crate::{
abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value},
engine::EngineConnection,
@ -33,12 +30,7 @@ impl ProgramMemory {
}
/// Add to the program memory.
pub fn add(
&mut self,
key: &str,
value: MemoryItem,
source_range: SourceRange,
) -> Result<(), KclError> {
pub fn add(&mut self, key: &str, value: MemoryItem, source_range: SourceRange) -> Result<(), KclError> {
if self.root.get(key).is_some() {
return Err(KclError::ValueAlreadyDefined(KclErrorDetails {
message: format!("Cannot redefine {}", key),
@ -172,12 +164,7 @@ impl MemoryItem {
memory: &ProgramMemory,
engine: &mut EngineConnection,
) -> Result<Option<ProgramReturn>, KclError> {
if let MemoryItem::Function {
func,
expression,
meta,
} = self
{
if let MemoryItem::Function { func, expression, meta } = self {
if let Some(func) = func {
func(args, memory, expression, meta, engine)
} else {
@ -228,10 +215,7 @@ impl SketchGroup {
if self.start.name == name {
Some(&self.start)
} else {
self.value
.iter()
.find(|p| p.get_name() == name)
.map(|p| p.get_base())
self.value.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] {
fn from(p: Point2d) -> Self {
[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)]
#[ts(export)]
pub struct Point3d {
@ -506,7 +502,7 @@ impl Default for PipeInfo {
}
/// Execute a AST's program.
fn execute(
pub fn execute(
program: crate::abstract_syntax_tree_types::Program,
memory: &mut ProgramMemory,
options: BodyType,
@ -526,8 +522,7 @@ fn execute(
match arg {
Value::Literal(literal) => args.push(literal.into()),
Value::Identifier(identifier) => {
let memory_item =
memory.get(&identifier.name, identifier.into())?;
let memory_item = memory.get(&identifier.name, identifier.into())?;
args.push(memory_item.clone());
}
// We do nothing for the rest.
@ -542,8 +537,7 @@ fn execute(
}));
}
memory.return_ =
Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else if let Some(func) = memory.clone().root.get(&fn_name) {
func.call_fn(&args, memory, engine)?;
} else {
@ -569,12 +563,7 @@ fn execute(
memory.add(&var_name, value.clone(), source_range)?;
}
Value::BinaryExpression(binary_expression) => {
let result = binary_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = binary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::FunctionExpression(function_expression) => {
@ -611,41 +600,28 @@ fn execute(
)?;
}
Value::CallExpression(call_expression) => {
let result =
call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
let result = call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeExpression(pipe_expression) => {
let result = pipe_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = pipe_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::PipeSubstitution(pipe_substitution) => {
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()],
}));
}
Value::ArrayExpression(array_expression) => {
let result = array_expression.execute(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = array_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::ObjectExpression(object_expression) => {
let result = object_expression.execute(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = object_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
Value::MemberExpression(member_expression) => {
@ -653,12 +629,7 @@ fn execute(
memory.add(&var_name, result, source_range)?;
}
Value::UnaryExpression(unary_expression) => {
let result = unary_expression.get_result(
memory,
&mut pipe_info,
&stdlib,
engine,
)?;
let result = unary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
memory.add(&var_name, result, source_range)?;
}
}
@ -681,63 +652,17 @@ fn execute(
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)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let tokens = crate::tokeniser::lexer(code);
let program = crate::parser::abstract_syntax_tree(&tokens)?;
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)?;
Ok(memory)
@ -780,23 +705,13 @@ show(part001)"#,
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
assert_eq!(
serde_json::json!(1.0 + 2.0f64.sqrt()),
memory
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
memory.root.get("intersect").unwrap().get_json_value().unwrap()
);
let memory = parse_execute(&ast_fn("0")).await.unwrap();
assert_eq!(
serde_json::json!(1.0000000000000002),
memory
.root
.get("intersect")
.unwrap()
.get_json_value()
.unwrap()
memory.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 serde::{Deserialize, Serialize};
use crate::abstract_syntax_tree_types::{
BinaryExpression, BinaryPart, CallExpression, Identifier, Literal,
use crate::{
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 {
// might be useful for reference to make it match
@ -182,6 +183,8 @@ pub struct ParenthesisToken {
pub end: usize,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ParenthesisToken);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
@ -195,10 +198,12 @@ pub struct ExtendedBinaryExpression {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedBinaryExpression);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ExntendedLiteral {
pub struct ExtendedLiteral {
pub start: usize,
pub end: usize,
pub value: serde_json::Value,
@ -207,11 +212,13 @@ pub struct ExntendedLiteral {
pub end_extended: Option<usize>,
}
crate::abstract_syntax_tree_types::impl_value_meta!(ExtendedLiteral);
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MathExpression {
ExntendedLiteral(Box<ExntendedLiteral>),
ExtendedLiteral(Box<ExtendedLiteral>),
Identifier(Box<Identifier>),
CallExpression(Box<CallExpression>),
BinaryExpression(Box<BinaryExpression>),
@ -219,6 +226,30 @@ pub enum MathExpression {
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(
reverse_polish_notation_tokens: &[Token],
stack: Vec<MathExpression>,
@ -241,80 +272,76 @@ fn build_tree(
}),
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];
if current_token.token_type == TokenType::Number
|| current_token.token_type == TokenType::String
{
if current_token.token_type == TokenType::Number || current_token.token_type == TokenType::String {
let mut new_stack = stack;
new_stack.push(MathExpression::ExntendedLiteral(Box::new(
ExntendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid float: {}", current_token.value),
}));
}
new_stack.push(MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: if current_token.token_type == TokenType::Number {
if let Ok(value) = current_token.value.parse::<i64>() {
serde_json::Value::Number(value.into())
} else if let Ok(value) = current_token.value.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(value) {
serde_json::Value::Number(n)
} else {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value),
message: format!("Invalid float: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
serde_json::Value::String(str_val)
},
start: current_token.start,
end: current_token.end,
raw: current_token.value.clone(),
end_extended: None,
start_extended: None,
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![current_token.into()],
message: format!("Invalid integer: {}", current_token.value),
}));
}
} else {
let mut str_val = current_token.value.clone();
str_val.remove(0);
str_val.pop();
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,
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);
} 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(),
} else if current_token.token_type == TokenType::Word {
if reverse_polish_notation_tokens.len() > 1 {
if reverse_polish_notation_tokens[1].token_type == TokenType::Brace
&& reverse_polish_notation_tokens[1].value == "("
{
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,
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);
} else if current_token.token_type == TokenType::Brace && current_token.value == ")" {
let inner_node: MathExpression = match &stack[stack.len() - 1] {
@ -340,14 +367,22 @@ fn build_tree(
end_extended: None,
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(literal.clone())
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(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] {
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 {
MathExpression::ExtendedBinaryExpression(bin_exp) => {
@ -372,22 +407,33 @@ fn build_tree(
end_extended: Some(current_token.end),
}))
}
MathExpression::ExntendedLiteral(literal) => {
MathExpression::ExntendedLiteral(Box::new(ExntendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
start_extended: Some(paran.start),
MathExpression::ExtendedLiteral(literal) => MathExpression::ExtendedLiteral(Box::new(ExtendedLiteral {
value: literal.value.clone(),
start: literal.start,
end: literal.end,
raw: literal.raw.clone(),
end_extended: Some(current_token.end),
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();
new_stack.push(expression);
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] {
MathExpression::ExtendedBinaryExpression(bin_exp) => (
BinaryPart::BinaryExpression(Box::new(BinaryExpression {
@ -399,7 +445,7 @@ fn build_tree(
})),
bin_exp.start_extended.unwrap_or(bin_exp.start),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -409,13 +455,14 @@ fn build_tree(
lit.start_extended.unwrap_or(lit.start),
),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.start),
MathExpression::CallExpression(call) => {
(BinaryPart::CallExpression(call.clone()), call.start)
MathExpression::CallExpression(call) => (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] {
MathExpression::ExtendedBinaryExpression(bin_exp) => (
@ -428,7 +475,7 @@ fn build_tree(
})),
bin_exp.end_extended.unwrap_or(bin_exp.end),
),
MathExpression::ExntendedLiteral(lit) => (
MathExpression::ExtendedLiteral(lit) => (
BinaryPart::Literal(Box::new(Literal {
value: lit.value.clone(),
start: lit.start,
@ -438,13 +485,14 @@ fn build_tree(
lit.end_extended.unwrap_or(lit.end),
),
MathExpression::Identifier(ident) => (BinaryPart::Identifier(ident.clone()), ident.end),
MathExpression::CallExpression(call) => {
(BinaryPart::CallExpression(call.clone()), call.end)
MathExpression::CallExpression(call) => (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() {
@ -458,11 +506,7 @@ fn build_tree(
let tree = BinaryExpression {
operator: current_token.value.clone(),
start: left.1,
end: if right.1 > right_end {
right.1
} else {
right_end
},
end: if right.1 > right_end { right.1 } else { right_end },
left: left.0,
right: right.0,
};
@ -510,9 +554,10 @@ pub fn parse_expression(tokens: &[Token]) -> Result<BinaryExpression, KclError>
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_parse_expression() {
let tokens = crate::tokeniser::lexer("1 + 2");
@ -833,8 +878,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 + 2 * 3"), &[], &[]).unwrap();
assert_eq!(
result,
vec![
@ -874,8 +918,7 @@ mod test {
#[test]
fn test_reverse_polish_notation_complex_with_parentheses() {
let result =
reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
let result = reverse_polish_notation(&crate::tokeniser::lexer("1 * ( 2 + 3 )"), &[], &[]).unwrap();
assert_eq!(
result,
vec![

View File

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

View File

@ -1,21 +1,15 @@
//! Generates source code from the AST.
//! 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::{
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression,
Literal, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression,
Program, UnaryExpression, Value,
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, Literal,
LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, Program, UnaryExpression,
Value,
};
fn recast_literal(literal: Literal) -> String {
if let serde_json::Value::String(value) = literal.value {
let quote = if literal.raw.trim().starts_with('"') {
'"'
} else {
'\''
};
let quote = if literal.raw.trim().starts_with('"') { '"' } else { '\'' };
format!("{}{}{}", quote, value, quote)
} else {
literal.value.to_string()
@ -41,16 +35,13 @@ fn recast_binary_expression(expression: BinaryExpression) -> String {
let should_wrap_right = match expression.right.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
|| expression.operator == "-"
precedence(&expression.operator) > precedence(&bin_exp.operator) || expression.operator == "-"
}
_ => false,
};
let should_wrap_left = match expression.left.clone() {
BinaryPart::BinaryExpression(bin_exp) => {
precedence(&expression.operator) > precedence(&bin_exp.operator)
}
BinaryPart::BinaryExpression(bin_exp) => precedence(&expression.operator) > precedence(&bin_exp.operator),
_ => false,
};
@ -66,12 +57,8 @@ fn recast_binary_part(part: BinaryPart) -> String {
match part {
BinaryPart::Literal(literal) => recast_literal(*literal),
BinaryPart::Identifier(identifier) => identifier.name,
BinaryPart::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
BinaryPart::CallExpression(call_expression) => {
recast_call_expression(&call_expression, "", false)
}
BinaryPart::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
BinaryPart::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
_ => String::new(),
}
}
@ -81,15 +68,11 @@ fn recast_value(node: Value, _indentation: String, is_in_pipe_expression: bool)
match node {
Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp),
Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation),
Value::ObjectExpression(ref obj_exp) => {
recast_object_expression(obj_exp, &indentation, is_in_pipe_expression)
}
Value::ObjectExpression(ref obj_exp) => recast_object_expression(obj_exp, &indentation, is_in_pipe_expression),
Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp),
Value::Literal(literal) => recast_literal(*literal),
Value::FunctionExpression(func_exp) => recast_function(*func_exp),
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, &indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, &indentation, is_in_pipe_expression),
Value::Identifier(ident) => ident.name,
Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_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(
expression: &ObjectExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_object_expression(expression: &ObjectExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
let flat_recast = format!(
"{{ {} }}",
expression
@ -159,11 +138,7 @@ fn recast_object_expression(
format!(
"{}: {}",
prop.key.name,
recast_value(
prop.value.clone(),
_indentation.clone(),
is_in_pipe_expression
)
recast_value(prop.value.clone(), _indentation.clone(), is_in_pipe_expression)
)
})
.collect::<Vec<String>>()
@ -175,11 +150,7 @@ fn recast_object_expression(
}
}
fn recast_call_expression(
expression: &CallExpression,
indentation: &str,
is_in_pipe_expression: bool,
) -> String {
fn recast_call_expression(expression: &CallExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
format!(
"{}({})",
expression.callee.name,
@ -201,9 +172,7 @@ fn recast_argument(argument: Value, indentation: &str, is_in_pipe_expression: bo
Value::ObjectExpression(object_exp) => {
recast_object_expression(&object_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => {
recast_call_expression(&call_exp, indentation, is_in_pipe_expression)
}
Value::CallExpression(call_exp) => recast_call_expression(&call_exp, indentation, is_in_pipe_expression),
Value::FunctionExpression(function_exp) => recast_function(*function_exp),
Value::PipeSubstitution(_) => "%".to_string(),
Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
@ -224,9 +193,7 @@ fn recast_member_expression(expression: MemberExpression) -> String {
};
match expression.object {
MemberObject::MemberExpression(member_exp) => {
recast_member_expression(*member_exp) + key_str.as_str()
}
MemberObject::MemberExpression(member_exp) => recast_member_expression(*member_exp) + 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 {
BinaryPart::Literal(literal) => Value::Literal(literal),
BinaryPart::Identifier(identifier) => Value::Identifier(identifier),
BinaryPart::BinaryExpression(binary_expression) => {
Value::BinaryExpression(binary_expression)
}
BinaryPart::BinaryExpression(binary_expression) => Value::BinaryExpression(binary_expression),
BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_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
.iter()
.map(|statement| match statement.clone() {
BodyItem::ExpressionStatement(expression_statement) => {
match expression_statement.expression {
Value::BinaryExpression(binary_expression) => {
recast_binary_expression(*binary_expression)
}
Value::ArrayExpression(array_expression) => {
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::ExpressionStatement(expression_statement) => match expression_statement.expression {
Value::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
Value::ArrayExpression(array_expression) => 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
.declarations
.iter()
@ -310,22 +265,16 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
})
.collect::<String>(),
BodyItem::ReturnStatement(return_statement) => {
format!(
"return {}",
recast_argument(return_statement.argument, "", false)
)
format!("return {}", recast_argument(return_statement.argument, "", false))
}
})
.enumerate()
.map(|(index, recast_str)| {
let is_legit_custom_whitespace_or_comment =
|str: String| str != " " && str != "\n" && str != " ";
let is_legit_custom_whitespace_or_comment = |str: String| str != " " && str != "\n" && str != " ";
// determine the value of startString
let last_white_space_or_comment = if index > 0 {
let tmp = if let Some(non_code_node) =
ast.non_code_meta.none_code_nodes.get(&(index - 1))
{
let tmp = if let Some(non_code_node) = ast.non_code_meta.none_code_nodes.get(&(index - 1)) {
non_code_node.value.clone()
} else {
" ".to_string()
@ -335,12 +284,11 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
" ".to_string()
};
// indentation of this line will be covered by the previous if we're using a custom whitespace or comment
let mut start_string =
if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
let mut start_string = if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
String::new()
} else {
indentation.to_owned()
};
if index == 0 {
if let Some(start) = ast.non_code_meta.start.clone() {
start_string = start.value;
@ -358,13 +306,10 @@ pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
} else {
"\n".to_string()
};
let mut custom_white_space_or_comment =
match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => {
custom_white_space_or_comment.value.clone()
}
None => String::new(),
};
let mut custom_white_space_or_comment = match ast.non_code_meta.none_code_nodes.get(&index) {
Some(custom_white_space_or_comment) => custom_white_space_or_comment.value.clone(),
None => String::new(),
};
if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) {
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.
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ExtrudeGroup, ExtrudeTransform, MemoryItem, SketchGroup},
std::Args,
};
use anyhow::Result;
use derive_docs::stdlib;
use schemars::JsonSchema;
/// Extrudes by a given amount.
pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
let (length, sketch_group) = args.get_number_sketch_group()?;
@ -23,11 +23,7 @@ pub fn extrude(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "extrude"
}]
fn inner_extrude(
length: f64,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<ExtrudeGroup, KclError> {
fn inner_extrude(length: f64, sketch_group: SketchGroup, args: &mut Args) -> Result<ExtrudeGroup, KclError> {
let id = uuid::Uuid::new_v4();
let cmd = kittycad::types::ModelingCmd::Extrude {
@ -65,17 +61,15 @@ fn inner_get_extrude_wall_transform(
extrude_group: ExtrudeGroup,
args: &mut Args,
) -> Result<ExtrudeTransform, KclError> {
let surface = extrude_group
.get_path_by_name(surface_name)
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
let surface = extrude_group.get_path_by_name(surface_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(ExtrudeTransform {
position: surface.get_position(),

View File

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

View File

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

View File

@ -1,5 +1,6 @@
//! Functions related to sketching.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{ModelingCmd, Point3D};
use schemars::JsonSchema;
@ -9,13 +10,11 @@ use crate::{
errors::{KclError, KclErrorDetails},
executor::{BasePath, GeoMeta, MemoryItem, Path, Point2d, Position, Rotation, SketchGroup},
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,
},
};
use anyhow::Result;
/// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -44,11 +43,7 @@ pub fn line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "lineTo",
}]
fn inner_line_to(
data: LineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_line_to(data: LineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let to = match data {
LineToData::PointWithTag { to, .. } => to,
@ -56,6 +51,21 @@ fn inner_line_to(
};
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 {
base: BasePath {
from: from.into(),
@ -106,18 +116,11 @@ pub fn x_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "xLineTo",
}]
fn inner_x_line_to(
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_x_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
to: [to, from.y],
tag,
},
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [to, from.y], tag },
AxisLineToData::Point(data) => LineToData::Point([data, from.y]),
};
@ -138,18 +141,11 @@ pub fn y_line_to(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "yLineTo",
}]
fn inner_y_line_to(
data: AxisLineToData,
sketch_group: SketchGroup,
args: &Args,
) -> Result<SketchGroup, KclError> {
fn inner_y_line_to(data: AxisLineToData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let line_to_data = match data {
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag {
to: [from.x, to],
tag,
},
AxisLineToData::PointWithTag { to, tag } => LineToData::PointWithTag { to: [from.x, to], tag },
AxisLineToData::Point(data) => LineToData::Point([from.x, data]),
};
@ -207,11 +203,7 @@ pub fn line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "line",
}]
fn inner_line(
data: LineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
fn inner_line(data: LineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?;
let default = [0.2, 1.0];
@ -289,11 +281,7 @@ pub fn x_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "xLine",
}]
fn inner_x_line(
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
fn inner_x_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([length, 0.0]),
@ -318,11 +306,7 @@ pub fn y_line(args: &mut Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "yLine",
}]
fn inner_y_line(
data: AxisLineData,
sketch_group: SketchGroup,
args: &mut Args,
) -> Result<SketchGroup, KclError> {
fn inner_y_line(data: AxisLineData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let line_data = match data {
AxisLineData::LengthWithTag { length, tag } => LineData::PointWithTag {
to: PointOrDefault::Point([0.0, length]),
@ -373,9 +357,7 @@ fn inner_angled_line(
let from = sketch_group.get_coords_from_paths()?;
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to: [f64; 2] = [
from.x + length * f64::cos(angle * std::f64::consts::PI / 180.0),
@ -424,9 +406,7 @@ fn inner_angled_line_of_x_length(
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_y_component(angle, length);
@ -494,10 +474,7 @@ fn inner_angled_line_to_x(
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag {
to: [x_to, y_to],
tag,
}
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
@ -527,9 +504,7 @@ fn inner_angled_line_of_y_length(
) -> Result<SketchGroup, KclError> {
let (angle, length) = match &data {
AngledLineData::AngleWithTag { angle, length, .. } => (*angle, *length),
AngledLineData::AngleAndLength(angle_and_length) => {
(angle_and_length[0], angle_and_length[1])
}
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
};
let to = get_x_component(angle, length);
@ -579,10 +554,7 @@ fn inner_angled_line_to_y(
let new_sketch_group = inner_line_to(
if let AngledLineToData::AngleWithTag { tag, .. } = data {
LineToData::PointWithTag {
to: [x_to, y_to],
tag,
}
LineToData::PointWithTag { to: [x_to, y_to], tag }
} else {
LineToData::Point([x_to, y_to])
},
@ -610,8 +582,7 @@ pub struct AngeledLineThatIntersectsData {
/// Draw an angled line that intersects with a given line.
pub fn angled_line_that_intersects(args: &mut Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) =
args.get_data_and_sketch_group()?;
let (data, sketch_group): (AngeledLineThatIntersectsData, SketchGroup) = args.get_data_and_sketch_group()?;
let new_sketch_group = inner_angled_line_that_intersects(data, sketch_group, args)?;
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)
}
/// 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)]
mod tests {
use crate::std::sketch::{LineData, PointOrDefault};
use pretty_assertions::assert_eq;
use crate::std::sketch::{LineData, PointOrDefault};
#[test]
fn test_deserialize_line_data() {
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 {
let x = b[0] - a[0];
let y = b[1] - a[1];
@ -44,7 +49,10 @@ pub fn normalize_rad(angle: f64) -> f64 {
/// # 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)]
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
///
/// ```
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[0.0, 5.0]), 5.0);
/// assert_eq!(distance_between_points(&[0.0, 0.0], &[3.0, 4.0]), 5.0);
/// assert_eq!(
/// 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)]
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()
}
pub fn calculate_intersection_of_two_lines(
line1: &[[f64; 2]; 2],
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] {
let line2_point_b = [
line2_point[0] + f64::cos(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
line2_point[1] + f64::sin(line2_angle * std::f64::consts::PI / 180.0) * 10.0,
@ -117,27 +127,17 @@ pub fn intersection_with_parallel_line(
line2_angle: f64,
line2_point: [f64; 2],
) -> [f64; 2] {
calculate_intersection_of_two_lines(
&offset_line(line1_offset, line1[0], line1[1]),
line2_angle,
line2_point,
)
calculate_intersection_of_two_lines(&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] {
if p1[0] == p2[0] {
let direction = (p1[1] - p2[1]).signum();
return [
[p1[0] + offset * direction, p1[1]],
[p2[0] + offset * direction, p2[1]],
];
return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
}
if p1[1] == p2[1] {
let direction = (p2[0] - p1[0]).signum();
return [
[p1[0], p1[1] + offset * direction],
[p2[0], p2[1] + offset * direction],
];
return [[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]));
[[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]
}
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)]
mod tests {
// Here you can bring your functions into scope
use super::{get_x_component, get_y_component};
use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component};
use crate::executor::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [
(-315, [1, 1]),
(-225, [-1, 1]),
@ -245,4 +314,77 @@ mod tests {
assert!((result[0] - 0.0).abs() < f64::EPSILON);
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 regex::Regex;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
@ -48,8 +46,7 @@ lazy_static! {
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 STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
static ref OPERATOR: Regex =
Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
static ref OPERATOR: 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 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> {
regex
.find(str)
.map(|the_match| the_match.as_str().to_string())
regex.find(str).map(|the_match| the_match.as_str().to_string())
}
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> {
fn recursively_tokenise(
str: &str,
current_index: usize,
previous_tokens: Vec<Token>,
) -> Vec<Token> {
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
if current_index >= str.len() {
return previous_tokens;
}
@ -273,19 +264,12 @@ pub fn lexer(str: &str) -> Vec<Token> {
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)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn is_number_test() {
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;
mod docs;
mod engine;
mod errors;
mod executor;
mod export;
mod math_parser;
mod parser;
mod recast;
mod std;
mod tokeniser;
//! Wasm bindings for `kcl`.
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::prelude::*;
// wasm_bindgen wrapper for execute
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn execute_wasm(
program_str: &str,
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',
plugins: [],
plugins: [
require('@headlessui/tailwindcss'),
],
}

View File

@ -1642,6 +1642,11 @@
dependencies:
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":
version "0.11.10"
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"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.34":
version "0.0.34"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.34.tgz#c1f1021f6c77bd9f47caa685cfbff0ef358a0316"
integrity sha512-9pUUuspJB/rayW4adfF7UqRYLw1pugBy3t0+V6qK3sWttG9flgv54fPw3JKewn7VFoEjRtNtoREMAoWb4ZrUIw==
"@kittycad/lib@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"
@ -1981,6 +1986,70 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz#31b9c510d8cada9683549e1dbb4284cca5001faf"
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":
version "0.27.8"
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"
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:
version "1.0.0-beta.2"
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"
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"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
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"
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:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"