Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
cccedceea0 | |||
ed68a34560 | |||
00ee913e3f | |||
46cc67e2db | |||
ff1be34f54 | |||
848bf61277 | |||
043333d3bc | |||
19d90b8081 | |||
4837c52908 | |||
afcf820bdd | |||
18959510f8 | |||
798cbe968a | |||
9cbc088ba3 | |||
2693a5609b | |||
3507da7b39 | |||
56cfb6d1f0 | |||
2b974ef1de | |||
253f1992fd | |||
76d3794b45 | |||
e52c8c9db6 | |||
eb48d51309 | |||
f3274e03ff | |||
46937199a3 | |||
e2a4798c2f | |||
659e6d5b45 | |||
1fbd0ad675 | |||
743ea1af4d | |||
2b1a556b81 | |||
853389ba22 | |||
023af60781 | |||
18db6f2dc1 | |||
4afec15323 | |||
152108f7a5 | |||
32d928ae0c | |||
6f0fae625f | |||
9bc47cf14a |
@ -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=
|
||||
|
@ -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
|
||||
|
2
.github/workflows/cargo-build.yml
vendored
2
.github/workflows/cargo-build.yml
vendored
@ -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
|
||||
|
5
.github/workflows/cargo-test.yml
vendored
5
.github/workflows/cargo-test.yml
vendored
@ -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}}
|
||||
|
||||
|
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -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
1
.gitignore
vendored
@ -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
|
||||
|
1993
docs/kcl.json
1993
docs/kcl.json
File diff suppressed because it is too large
Load Diff
350
docs/kcl.md
350
docs/kcl.md
@ -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,
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
13
package.json
13
package.json
@ -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
77
src-tauri/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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" }
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling-app",
|
||||
"version": "0.1.0"
|
||||
"version": "0.3.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
80
src/App.tsx
80
src/App.tsx
@ -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={
|
||||
|
@ -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>
|
||||
|
112
src/Router.tsx
112
src/Router.tsx
@ -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
60
src/Toolbar.module.css
Normal 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;
|
||||
}
|
310
src/Toolbar.tsx
310
src/Toolbar.tsx
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
7
src/components/AppHeader.module.css
Normal file
7
src/components/AppHeader.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
Some CSS cannot be represented
|
||||
in Tailwind, such as complex grid layouts.
|
||||
*/
|
||||
.header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
290
src/components/CommandBar.tsx
Normal file
290
src/components/CommandBar.tsx
Normal 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
|
158
src/components/GlobalStateProvider.tsx
Normal file
158
src/components/GlobalStateProvider.tsx
Normal 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',
|
||||
})
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
})
|
||||
}
|
6
src/hooks/useCommandsContext.ts
Normal file
6
src/hooks/useCommandsContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { CommandsContext } from 'components/CommandBar'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useCommandsContext = () => {
|
||||
return useContext(CommandsContext)
|
||||
}
|
6
src/hooks/useGlobalStateContext.ts
Normal file
6
src/hooks/useGlobalStateContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { GlobalStateContext } from 'components/GlobalStateProvider'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useGlobalStateContext = () => {
|
||||
return useContext(GlobalStateContext)
|
||||
}
|
42
src/hooks/useStateMachineCommands.ts
Normal file
42
src/hooks/useStateMachineCommands.ts
Normal 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])
|
||||
}
|
@ -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()
|
||||
}, [])
|
||||
}
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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
124
src/lib/commands.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
64
src/lib/sorting.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
23
src/lib/theme.ts
Normal 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')
|
||||
}
|
||||
}
|
@ -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
218
src/machines/homeMachine.ts
Normal 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[] }
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
99
src/machines/homeMachine.typegen.ts
Normal file
99
src/machines/homeMachine.typegen.ts
Normal 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
|
||||
}
|
207
src/machines/settingsMachine.ts
Normal file
207
src/machines/settingsMachine.ts
Normal 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
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
46
src/machines/settingsMachine.typegen.ts
Normal file
46
src/machines/settingsMachine.typegen.ts
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
297
src/wasm-lib/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
47
src/wasm-lib/kcl/Cargo.toml
Normal file
47
src/wasm-lib/kcl/Cargo.toml
Normal 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"] }
|
@ -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 {
|
@ -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()),
|
||||
}
|
||||
}
|
175
src/wasm-lib/kcl/src/engine/conn.rs
Normal file
175
src/wasm-lib/kcl/src/engine/conn.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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 {})
|
||||
}
|
||||
|
@ -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;
|
@ -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())?;
|
78
src/wasm-lib/kcl/src/errors.rs
Normal file
78
src/wasm-lib/kcl/src/errors.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
10
src/wasm-lib/kcl/src/lib.rs
Normal file
10
src/wasm-lib/kcl/src/lib.rs
Normal 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;
|
@ -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![
|
@ -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(¤t_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,
|
@ -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)?)
|
||||
}
|
@ -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(),
|
@ -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(),
|
||||
);
|
||||
}
|
@ -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!(
|
@ -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, ¢er.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, ¢er.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();
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"));
|
6
src/wasm-lib/rustfmt.toml
Normal file
6
src/wasm-lib/rustfmt.toml
Normal 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"
|
@ -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(),
|
||||
},
|
||||
]
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
)))
|
||||
}
|
@ -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)?)
|
||||
}
|
||||
|
@ -39,5 +39,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
darkMode: 'class',
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('@headlessui/tailwindcss'),
|
||||
],
|
||||
}
|
||||
|
89
yarn.lock
89
yarn.lock
@ -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"
|
||||
|
Reference in New Issue
Block a user