Compare commits
7 Commits
v0.11.0
...
mike/relat
| Author | SHA1 | Date | |
|---|---|---|---|
| b4cadd8cbb | |||
| 219a0b7a88 | |||
| da9117c8a4 | |||
| 0d038f5717 | |||
| da6e158524 | |||
| c1d2cd5f51 | |||
| 93d36796df |
3
.github/workflows/cargo-build.yml
vendored
3
.github/workflows/cargo-build.yml
vendored
@ -15,9 +15,6 @@ on:
|
|||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
- .github/workflows/cargo-build.yml
|
- .github/workflows/cargo-build.yml
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo build
|
name: cargo build
|
||||||
jobs:
|
jobs:
|
||||||
cargobuild:
|
cargobuild:
|
||||||
|
|||||||
5
.github/workflows/cargo-clippy.yml
vendored
5
.github/workflows/cargo-clippy.yml
vendored
@ -15,9 +15,6 @@ on:
|
|||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
- '**.rs'
|
- '**.rs'
|
||||||
- .github/workflows/cargo-build.yml
|
- .github/workflows/cargo-build.yml
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo clippy
|
name: cargo clippy
|
||||||
jobs:
|
jobs:
|
||||||
cargoclippy:
|
cargoclippy:
|
||||||
@ -57,4 +54,4 @@ jobs:
|
|||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
cargo clippy --all --tests --benches -- -D warnings
|
cargo clippy --all --tests -- -D warnings
|
||||||
|
|||||||
40
.github/workflows/cargo-criterion.yml
vendored
40
.github/workflows/cargo-criterion.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '**.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- .github/workflows/cargo-criterion.yml
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- .github/workflows/cargo-criterion.yml
|
|
||||||
workflow_dispatch:
|
|
||||||
permissions: read-all
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo criterion
|
|
||||||
jobs:
|
|
||||||
cargocriterion:
|
|
||||||
name: cargo criterion
|
|
||||||
runs-on: ubuntu-latest-8-cores
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cargo install cargo-criterion
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
|
||||||
- name: Benchmark kcl library
|
|
||||||
shell: bash
|
|
||||||
run: |-
|
|
||||||
cd src/wasm-lib/kcl; cargo criterion
|
|
||||||
|
|
||||||
3
.github/workflows/cargo-fmt.yml
vendored
3
.github/workflows/cargo-fmt.yml
vendored
@ -18,9 +18,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
packages: read
|
packages: read
|
||||||
contents: read
|
contents: read
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo fmt
|
name: cargo fmt
|
||||||
jobs:
|
jobs:
|
||||||
cargofmt:
|
cargofmt:
|
||||||
|
|||||||
4
.github/workflows/cargo-test.yml
vendored
4
.github/workflows/cargo-test.yml
vendored
@ -17,9 +17,6 @@ on:
|
|||||||
- .github/workflows/cargo-test.yml
|
- .github/workflows/cargo-test.yml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo test
|
name: cargo test
|
||||||
jobs:
|
jobs:
|
||||||
cargotest:
|
cargotest:
|
||||||
@ -61,5 +58,4 @@ jobs:
|
|||||||
cargo nextest run --workspace --no-fail-fast -P ci
|
cargo nextest run --workspace --no-fail-fast -P ci
|
||||||
env:
|
env:
|
||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
RUST_MIN_STACK: 10485760000
|
|
||||||
|
|
||||||
|
|||||||
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@ -7,14 +7,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
schedule:
|
|
||||||
- cron: '0 4 * * *'
|
|
||||||
# Daily at 04:00 AM UTC
|
|
||||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
check-format:
|
check-format:
|
||||||
runs-on: 'ubuntu-20.04'
|
runs-on: 'ubuntu-20.04'
|
||||||
@ -46,6 +39,8 @@ jobs:
|
|||||||
|
|
||||||
build-test-web:
|
build-test-web:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.export_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -68,37 +63,11 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn test:cov
|
- run: yarn test:cov
|
||||||
|
|
||||||
prepare-json-files:
|
|
||||||
runs-on: ubuntu-20.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.export_version.outputs.version }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: Set nightly version
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
run: |
|
|
||||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
|
||||||
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/test/nightly/last_update.json' \
|
|
||||||
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
package.json
|
|
||||||
src-tauri/tauri.conf.json
|
|
||||||
|
|
||||||
- id: export_version
|
- id: export_version
|
||||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build-apps:
|
build-apps:
|
||||||
needs: [check-format, build-test-web, prepare-json-files, check-types]
|
needs: [check-format, build-test-web, check-types]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -106,15 +75,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
|
|
||||||
- name: Copy updated .json files
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
run: |
|
|
||||||
ls -l artifact
|
|
||||||
cp artifact/package.json package.json
|
|
||||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
|
||||||
|
|
||||||
- name: install ubuntu system dependencies
|
- name: install ubuntu system dependencies
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
@ -193,7 +153,6 @@ jobs:
|
|||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||||
|
|
||||||
@ -203,14 +162,12 @@ jobs:
|
|||||||
|
|
||||||
publish-apps-release:
|
publish-apps-release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
if: github.event_name == 'release'
|
||||||
needs: [build-test-web, prepare-json-files, build-apps]
|
needs: [build-test-web, build-apps]
|
||||||
env:
|
env:
|
||||||
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||||
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
PUB_DATE: ${{ github.event.release.created_at }}
|
||||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
NOTES: ${{ github.event.release.body }}
|
||||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
|
||||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
@ -220,9 +177,9 @@ jobs:
|
|||||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||||
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "${VERSION}" \
|
--arg version "v${VERSION_NO_V}" \
|
||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_sig "$DARWIN_SIG" \
|
--arg darwin_sig "$DARWIN_SIG" \
|
||||||
@ -258,9 +215,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate the download static endpoint
|
- name: Generate the download static endpoint
|
||||||
run: |
|
run: |
|
||||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "${VERSION}" \
|
--arg version "v${VERSION_NO_V}" \
|
||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||||
@ -300,22 +257,21 @@ jobs:
|
|||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/*itty*'
|
glob: '*/*itty*'
|
||||||
parent: false
|
parent: false
|
||||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
with:
|
with:
|
||||||
path: last_update.json
|
path: last_update.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: dl.kittycad.io/releases/modeling-app
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
with:
|
with:
|
||||||
path: last_download.json
|
path: last_download.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: dl.kittycad.io/releases/modeling-app
|
||||||
|
|
||||||
- name: Upload release files to Github
|
- name: Upload release files to Github
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: artifact/*/*itty*
|
files: artifact/*/*itty*
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -22,11 +22,6 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
src/wasm-lib/.idea
|
|
||||||
src/wasm-lib/.vscode
|
|
||||||
|
|
||||||
# rust
|
# rust
|
||||||
src/wasm-lib/target
|
src/wasm-lib/target
|
||||||
src/wasm-lib/bindings
|
src/wasm-lib/bindings
|
||||||
|
|||||||
@ -7,6 +7,3 @@ coverage
|
|||||||
target
|
target
|
||||||
src/wasm-lib/pkg
|
src/wasm-lib/pkg
|
||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
|
|
||||||
# XState generated files
|
|
||||||
src/machines/modelingMachine.typegen.ts
|
|
||||||
|
|||||||
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2023 The KittyCAD Authors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
27
README.md
27
README.md
@ -29,7 +29,6 @@ The 3D view in KittyCAD Modeling App is just a video stream from our hosted geom
|
|||||||
- [React](https://react.dev/)
|
- [React](https://react.dev/)
|
||||||
- [Headless UI](https://headlessui.com/)
|
- [Headless UI](https://headlessui.com/)
|
||||||
- [TailwindCSS](https://tailwindcss.com/)
|
- [TailwindCSS](https://tailwindcss.com/)
|
||||||
- [XState](https://xstate.js.org/)
|
|
||||||
- Networking
|
- Networking
|
||||||
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
|
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
|
||||||
- Code Editor
|
- Code Editor
|
||||||
@ -57,7 +56,7 @@ yarn install
|
|||||||
followed by:
|
followed by:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn build:wasm-dev
|
yarn build:wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||||
@ -89,16 +88,9 @@ yarn test
|
|||||||
|
|
||||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
|
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
|
||||||
|
|
||||||
For running the rust (not tauri rust though) only, you can
|
|
||||||
```bash
|
|
||||||
cd src/wasm-lib
|
|
||||||
cargo test
|
|
||||||
```
|
|
||||||
but you will need to have install ffmpeg prior to.
|
|
||||||
|
|
||||||
## Tauri
|
## Tauri
|
||||||
|
|
||||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
|
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn tauri dev
|
yarn tauri dev
|
||||||
@ -131,24 +123,13 @@ Before you submit a contribution PR to this repo, please ensure that:
|
|||||||
|
|
||||||
## Release a new version
|
## Release a new version
|
||||||
|
|
||||||
1. Bump the versions in the .json files by creating a `Cut release v{x}.{y}.{z}` PR, committing the changes from
|
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VERSION=x.y.z yarn run bump-jsons
|
VERSION=x.y.z yarn run bump-jsons
|
||||||
```
|
```
|
||||||
|
|
||||||
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and past in the following
|
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||||
|
|
||||||
```typescript
|
|
||||||
console.log(
|
|
||||||
'- ' +
|
|
||||||
Array.from(
|
|
||||||
document.querySelectorAll('[data-hovercard-type="pull_request"]')
|
|
||||||
).map((a) => `[${a.innerText}](${a.href})`).join(`
|
|
||||||
- `)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
grab the md list and delete any that are older than the last bump
|
|
||||||
|
|
||||||
2. Merge the PR
|
2. Merge the PR
|
||||||
|
|
||||||
|
|||||||
5264
docs/kcl/std.json
5264
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
1890
docs/kcl/std.md
1890
docs/kcl/std.md
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.11.0",
|
"version": "0.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
@ -10,13 +10,13 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.43",
|
"@kittycad/lib": "^0.0.37",
|
||||||
"@lezer/javascript": "^1.4.7",
|
"@lezer/javascript": "^1.4.7",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@replit/codemirror-interact": "^6.3.0",
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
"@sentry/react": "^7.65.0",
|
"@sentry/react": "^7.65.0",
|
||||||
"@tauri-apps/api": "^1.5.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
@ -25,15 +25,13 @@
|
|||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/react-codemirror": "^4.21.13",
|
"@uiw/react-codemirror": "^4.21.13",
|
||||||
"@xstate/inspect": "^0.8.0",
|
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"debounce-promise": "^3.1.2",
|
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"re-resizable": "^6.9.11",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
@ -49,7 +47,7 @@
|
|||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^0.34.1",
|
||||||
"vscode-jsonrpc": "^8.1.0",
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
"vscode-languageserver-protocol": "^3.17.3",
|
"vscode-languageserver-protocol": "^3.17.3",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
@ -59,7 +57,7 @@
|
|||||||
"zustand": "^4.1.4"
|
"zustand": "^4.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "BROWSER=none vite",
|
||||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||||
"build:local": "vite build",
|
"build:local": "vite build",
|
||||||
"build:both": "vite build",
|
"build:both": "vite build",
|
||||||
@ -67,13 +65,12 @@
|
|||||||
"pretest": "yarn remove-importmeta",
|
"pretest": "yarn remove-importmeta",
|
||||||
"test": "vitest --mode development",
|
"test": "vitest --mode development",
|
||||||
"test:nowatch": "vitest run --mode development",
|
"test:nowatch": "vitest run --mode development",
|
||||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
|
||||||
"test:cov": "vitest run --coverage --mode development",
|
"test:cov": "vitest run --coverage --mode development",
|
||||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src",
|
"fmt": "prettier --write ./src",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src",
|
||||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
|
||||||
"build:wasm": "(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",
|
"build:wasm": "(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",
|
||||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||||
@ -102,9 +99,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.22.9",
|
"@babel/preset-env": "^7.22.9",
|
||||||
"@tauri-apps/cli": "^1.5.0",
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/debounce-promise": "^3.1.6",
|
"@types/debounce": "^1.2.1",
|
||||||
"@types/isomorphic-fetch": "^0.0.36",
|
"@types/isomorphic-fetch": "^0.0.36",
|
||||||
"@types/react-modal": "^3.16.0",
|
"@types/react-modal": "^3.16.0",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
@ -118,7 +115,7 @@
|
|||||||
"eslint-plugin-css-modules": "^2.11.0",
|
"eslint-plugin-css-modules": "^2.11.0",
|
||||||
"happy-dom": "^10.8.0",
|
"happy-dom": "^10.8.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.19",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.4",
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 475 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 469 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 200 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z" fill="#D0FF00"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
206
src-tauri/Cargo.lock
generated
206
src-tauri/Cargo.lock
generated
@ -84,7 +84,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-fs-extra",
|
"tauri-plugin-fs-extra",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.8.2",
|
"toml 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -155,20 +155,6 @@ version = "0.21.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bigdecimal"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "454bca3db10617b88b566f205ed190aedb0e0e6dd4cad61d3988a72e8c5594cb"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"libm",
|
|
||||||
"num-bigint",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@ -1309,21 +1295,7 @@ checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"mac",
|
"mac",
|
||||||
"markup5ever 0.10.1",
|
"markup5ever",
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "html5ever"
|
|
||||||
version = "0.26.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"mac",
|
|
||||||
"markup5ever 0.11.0",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
@ -1658,14 +1630,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.33"
|
version = "0.2.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d341a81a4dfef43460d395c87d86c17e24affb96db0e7f4a35e8688f0e092344"
|
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"bigdecimal",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
@ -1700,20 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
|
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cssparser",
|
"cssparser",
|
||||||
"html5ever 0.25.2",
|
"html5ever",
|
||||||
"matches",
|
|
||||||
"selectors",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kuchikiki"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
|
|
||||||
dependencies = [
|
|
||||||
"cssparser",
|
|
||||||
"html5ever 0.26.0",
|
|
||||||
"indexmap 1.9.3",
|
|
||||||
"matches",
|
"matches",
|
||||||
"selectors",
|
"selectors",
|
||||||
]
|
]
|
||||||
@ -1730,12 +1688,6 @@ version = "0.2.148"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libm"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "line-wrap"
|
name = "line-wrap"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -1775,9 +1727,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.20"
|
version = "0.4.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -1829,21 +1781,7 @@ checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"phf 0.8.0",
|
"phf 0.8.0",
|
||||||
"phf_codegen 0.8.0",
|
"phf_codegen",
|
||||||
"string_cache",
|
|
||||||
"string_cache_codegen",
|
|
||||||
"tendril",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markup5ever"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"phf 0.10.1",
|
|
||||||
"phf_codegen 0.10.0",
|
|
||||||
"string_cache",
|
"string_cache",
|
||||||
"string_cache_codegen",
|
"string_cache_codegen",
|
||||||
"tendril",
|
"tendril",
|
||||||
@ -2021,17 +1959,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-bigint"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@ -2055,9 +1982,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.16"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
@ -2415,16 +2342,6 @@ dependencies = [
|
|||||||
"phf_shared 0.8.0",
|
"phf_shared 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_codegen"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
|
||||||
dependencies = [
|
|
||||||
"phf_generator 0.10.0",
|
|
||||||
"phf_shared 0.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -2493,9 +2410,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phonenumber"
|
name = "phonenumber"
|
||||||
version = "0.3.3+8.13.9"
|
version = "0.3.2+8.13.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "635f3e6288e4f01c049d89332a031bd74f25d64b6fb94703ca966e819488cd06"
|
checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"either",
|
"either",
|
||||||
@ -2508,7 +2425,6 @@ dependencies = [
|
|||||||
"regex-cache",
|
"regex-cache",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"strum",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3105,11 +3021,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.15"
|
version = "0.8.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
|
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bigdecimal",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
@ -3122,9 +3037,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "0.8.15"
|
version = "0.8.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
|
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3190,7 +3105,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"matches",
|
"matches",
|
||||||
"phf 0.8.0",
|
"phf 0.8.0",
|
||||||
"phf_codegen 0.8.0",
|
"phf_codegen",
|
||||||
"precomputed-hash",
|
"precomputed-hash",
|
||||||
"servo_arc",
|
"servo_arc",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -3208,9 +3123,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.189"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
|
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -3226,9 +3141,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.189"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3543,28 +3458,6 @@ dependencies = [
|
|||||||
"syn 2.0.33",
|
"syn 2.0.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.24.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
|
||||||
dependencies = [
|
|
||||||
"strum_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum_macros"
|
|
||||||
version = "0.24.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.4.1",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustversion",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
@ -3712,9 +3605,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "1.5.2"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
|
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
@ -3768,13 +3661,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "1.5.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"dirs-next",
|
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"semver",
|
"semver",
|
||||||
@ -3782,14 +3674,13 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tauri-winres",
|
"tauri-winres",
|
||||||
"walkdir",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "1.4.1"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
|
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"brotli",
|
"brotli",
|
||||||
@ -3813,9 +3704,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "1.4.1"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af"
|
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3828,7 +3719,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs-extra"
|
name = "tauri-plugin-fs-extra"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9b20f28d747f6ec3ba5a80bfcd5edc1d573b4c90"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3839,9 +3730,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "0.14.1"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
|
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@ -3860,9 +3751,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "0.14.1"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895"
|
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"gtk",
|
"gtk",
|
||||||
@ -3880,20 +3771,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "1.5.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46"
|
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"html5ever 0.26.0",
|
"html5ever",
|
||||||
"infer",
|
"infer",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"kuchikiki",
|
"kuchiki",
|
||||||
"log",
|
|
||||||
"memchr",
|
"memchr",
|
||||||
"phf 0.10.1",
|
"phf 0.10.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -4007,9 +3897,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.33.0"
|
version = "1.32.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
|
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -4078,14 +3968,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.2"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit 0.20.2",
|
"toml_edit 0.20.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4112,9 +4002,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.20.2"
|
version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
"serde",
|
"serde",
|
||||||
@ -4909,9 +4799,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.24.4"
|
version = "0.24.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ef04bdad49eba2e01f06e53688c8413bd6a87b0bc14b72284465cf96e3578e"
|
checksum = "33748f35413c8a98d45f7a08832d848c0c5915501803d1faade5a4ebcd258cea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"block",
|
"block",
|
||||||
@ -4923,7 +4813,7 @@ dependencies = [
|
|||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"gtk",
|
"gtk",
|
||||||
"html5ever 0.25.2",
|
"html5ever",
|
||||||
"http",
|
"http",
|
||||||
"kuchiki",
|
"kuchiki",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
@ -12,18 +12,18 @@ rust-version = "1.60"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.0", features = [] }
|
tauri-build = { version = "1.4.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kittycad = "0.2.33"
|
kittycad = "0.2.25"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
tauri = { version = "1.4.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tokio = { version = "1.33.0", features = ["time"] }
|
tokio = { version = "1.32.0", features = ["time"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "kittycad-modeling",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.11.0"
|
"version": "0.8.2"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|||||||
@ -1,16 +1,9 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
import { describe, test, vi } from 'vitest'
|
import { describe, test, vi } from 'vitest'
|
||||||
import {
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
Route,
|
|
||||||
RouterProvider,
|
|
||||||
createMemoryRouter,
|
|
||||||
createRoutesFromElements,
|
|
||||||
} from 'react-router-dom'
|
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
|
||||||
import { BROWSER_FILE_NAME } from 'Router'
|
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
let listener: ((rect: any) => void) | undefined = undefined
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
;(global as any).ResizeObserver = class ResizeObserver {
|
||||||
@ -31,7 +24,7 @@ describe('App tests', () => {
|
|||||||
>
|
>
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useParams: () => ({ id: BROWSER_FILE_NAME }),
|
useParams: () => ({ id: 'new' }),
|
||||||
useLoaderData: () => ({ code: null }),
|
useLoaderData: () => ({ code: null }),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -48,26 +41,12 @@ describe('App tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
// We have to use a memory router in the testing environment,
|
// wrap in router and xState context
|
||||||
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
|
return (
|
||||||
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
<BrowserRouter>
|
||||||
const router = createMemoryRouter(
|
<CommandBarProvider>
|
||||||
createRoutesFromElements(
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
<Route
|
</CommandBarProvider>
|
||||||
path="/file/:id"
|
</BrowserRouter>
|
||||||
element={
|
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>
|
|
||||||
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
initialEntries: ['/file/new'],
|
|
||||||
initialIndex: 0,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return <RouterProvider router={router} />
|
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/App.tsx
104
src/App.tsx
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useCallback, MouseEventHandler } from 'react'
|
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { PaneType, useStore } from './useStore'
|
import { PaneType, useStore } from './useStore'
|
||||||
@ -29,33 +29,46 @@ import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/mod
|
|||||||
import { CodeMenu } from 'components/CodeMenu'
|
import { CodeMenu } from 'components/CodeMenu'
|
||||||
import { TextEditor } from 'components/TextEditor'
|
import { TextEditor } from 'components/TextEditor'
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { engineCommandManager } from './lang/std/engineConnection'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project, file } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
|
|
||||||
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
|
setCode,
|
||||||
|
engineCommandManager,
|
||||||
buttonDownInStream,
|
buttonDownInStream,
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
executeAst,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
setCode: s.setCode,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
buttonDownInStream: s.buttonDownInStream,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
|
executeAst: s.executeAst,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { settings } = useGlobalStateContext()
|
const {
|
||||||
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
auth: {
|
||||||
settings?.context || {}
|
context: { token },
|
||||||
const { state, send } = useModelingContext()
|
},
|
||||||
|
settings: {
|
||||||
|
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
@ -72,7 +85,50 @@ export function App() {
|
|||||||
useHotkeys('shift + l', () => togglePane('logs'))
|
useHotkeys('shift + l', () => togglePane('logs'))
|
||||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||||
useHotkeys('shift + d', () => togglePane('debug'))
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
useHotkeys('esc', () => send('Cancel'))
|
useHotkeys('esc', () => {
|
||||||
|
if (guiMode.mode === 'sketch') {
|
||||||
|
if (guiMode.sketchMode === 'selectFace') return
|
||||||
|
if (guiMode.sketchMode === 'sketchEdit') {
|
||||||
|
// TODO: share this with Toolbar's "Exit sketch" button
|
||||||
|
// exiting sketch should be done consistently across all exits
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
// this is necessary to get the UI back into a consistent
|
||||||
|
// state right now, hopefully won't need to rerender
|
||||||
|
// when exiting sketch mode in the future
|
||||||
|
executeAst()
|
||||||
|
} else {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'set_tool',
|
||||||
|
tool: 'select',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
rotation: guiMode.rotation,
|
||||||
|
position: guiMode.position,
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
pathId: guiMode.pathId,
|
||||||
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||||
(p) => p === onboardingStatus
|
(p) => p === onboardingStatus
|
||||||
@ -86,26 +142,21 @@ export function App() {
|
|||||||
// on mount, and overwrite any locally-stored code
|
// on mount, and overwrite any locally-stored code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTauri() && loadedCode !== null) {
|
if (isTauri() && loadedCode !== null) {
|
||||||
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
|
setCode(loadedCode)
|
||||||
// If the engine is ready, promptly execute the loaded code
|
|
||||||
kclManager.setCodeAndExecute(loadedCode)
|
|
||||||
} else {
|
|
||||||
// Otherwise, just set the code and wait for the connection to complete
|
|
||||||
kclManager.setCode(loadedCode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
// Clear code on unmount if in desktop app
|
// Clear code on unmount if in desktop app
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
kclManager.setCode('')
|
setCode('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loadedCode])
|
}, [loadedCode, setCode])
|
||||||
|
|
||||||
|
useSetupEngineManager(streamRef, token)
|
||||||
useEngineConnectionSubscriptions()
|
useEngineConnectionSubscriptions()
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager.sendSceneCommand(message)
|
engineCommandManager?.sendSceneCommand(message)
|
||||||
}, 16)
|
}, 16)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
e.nativeEvent.preventDefault()
|
e.nativeEvent.preventDefault()
|
||||||
@ -119,7 +170,10 @@ export function App() {
|
|||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (buttonDownInStream === undefined) {
|
if (buttonDownInStream === undefined) {
|
||||||
if (state.matches('Sketch.Line Tool')) {
|
if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any)
|
||||||
|
) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
@ -139,7 +193,7 @@ export function App() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (state.matches('Sketch.Move Tool')) {
|
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
@ -162,6 +216,7 @@ export function App() {
|
|||||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||||
interaction = 'zoom'
|
interaction = 'zoom'
|
||||||
} else {
|
} else {
|
||||||
|
console.log('none')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,8 +234,9 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative h-full flex flex-col"
|
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
ref={streamRef}
|
||||||
>
|
>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className={
|
className={
|
||||||
@ -188,7 +244,7 @@ export function App() {
|
|||||||
paneOpacity +
|
paneOpacity +
|
||||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={{ project, file }}
|
project={project}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
|
|||||||
@ -3,8 +3,10 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const {
|
||||||
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
auth: { state },
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
const isLoggingIn = state.matches('checkIfLoggedIn')
|
||||||
|
|
||||||
return isLoggingIn ? (
|
return isLoggingIn ? (
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import {
|
|||||||
} from './lib/tauriFS'
|
} from './lib/tauriFS'
|
||||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
import { WasmErrBanner } from './components/WasmErrBanner'
|
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
import {
|
import {
|
||||||
SETTINGS_PERSIST_KEY,
|
SETTINGS_PERSIST_KEY,
|
||||||
@ -41,10 +40,6 @@ import { ContextFrom } from 'xstate'
|
|||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
|
||||||
import { KclContextProvider } from 'lang/KclSinglton'
|
|
||||||
import FileMachineProvider from 'components/FileMachineProvider'
|
|
||||||
import { sep } from '@tauri-apps/api/path'
|
|
||||||
|
|
||||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@ -99,16 +94,13 @@ export const paths = {
|
|||||||
) as typeof onboardingPaths,
|
) as typeof onboardingPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BROWSER_FILE_NAME = 'new'
|
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
export type IndexLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
project?: ProjectWithEntryPointMetadata
|
project?: ProjectWithEntryPointMetadata
|
||||||
file?: FileEntry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectWithEntryPointMetadata = FileEntry & {
|
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||||
entrypointMetadata: Metadata
|
entrypoint_metadata: Metadata
|
||||||
}
|
}
|
||||||
export type HomeLoaderData = {
|
export type HomeLoaderData = {
|
||||||
projects: ProjectWithEntryPointMetadata[]
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
@ -137,24 +129,15 @@ const router = createBrowserRouter(
|
|||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: () =>
|
||||||
isTauri()
|
isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'),
|
||||||
? redirect(paths.HOME)
|
|
||||||
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
|
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.FILE + '/:id',
|
path: paths.FILE + '/:id',
|
||||||
element: (
|
element: (
|
||||||
<Auth>
|
<Auth>
|
||||||
<FileMachineProvider>
|
<Outlet />
|
||||||
<KclContextProvider>
|
<App />
|
||||||
<ModelingMachineProvider>
|
|
||||||
<Outlet />
|
|
||||||
<App />
|
|
||||||
</ModelingMachineProvider>
|
|
||||||
<WasmErrBanner />
|
|
||||||
</KclContextProvider>
|
|
||||||
</FileMachineProvider>
|
|
||||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
@ -184,41 +167,21 @@ const router = createBrowserRouter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultDir = persistedSettings.defaultDirectory || ''
|
if (params.id && params.id !== 'new') {
|
||||||
|
|
||||||
if (params.id && params.id !== BROWSER_FILE_NAME) {
|
|
||||||
const decodedId = decodeURIComponent(params.id)
|
|
||||||
const projectAndFile = decodedId.replace(defaultDir + sep, '')
|
|
||||||
const firstSlashIndex = projectAndFile.indexOf(sep)
|
|
||||||
const projectName = projectAndFile.slice(0, firstSlashIndex)
|
|
||||||
const projectPath = defaultDir + sep + projectName
|
|
||||||
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
|
|
||||||
|
|
||||||
if (firstSlashIndex === -1 || !currentFileName)
|
|
||||||
return redirect(
|
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
|
||||||
`${params.id}${sep}${PROJECT_ENTRYPOINT}`
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
const code = await readTextFile(decodedId)
|
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT)
|
||||||
const entrypointMetadata = await metadata(
|
const entrypoint_metadata = await metadata(
|
||||||
projectPath + sep + PROJECT_ENTRYPOINT
|
params.id + '/' + PROJECT_ENTRYPOINT
|
||||||
)
|
)
|
||||||
const children = await readDir(projectPath, { recursive: true })
|
const children = await readDir(params.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
project: {
|
project: {
|
||||||
name: projectName,
|
name: params.id.slice(params.id.lastIndexOf('/') + 1),
|
||||||
path: projectPath,
|
|
||||||
children,
|
|
||||||
entrypointMetadata,
|
|
||||||
},
|
|
||||||
file: {
|
|
||||||
name: currentFileName,
|
|
||||||
path: params.id,
|
path: params.id,
|
||||||
|
children,
|
||||||
|
entrypoint_metadata,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,7 +212,7 @@ const router = createBrowserRouter(
|
|||||||
),
|
),
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
return redirect(paths.FILE + '/new')
|
||||||
}
|
}
|
||||||
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
@ -271,9 +234,9 @@ const router = createBrowserRouter(
|
|||||||
isProjectDirectory
|
isProjectDirectory
|
||||||
)
|
)
|
||||||
const projects = await Promise.all(
|
const projects = await Promise.all(
|
||||||
projectsNoMeta.map(async (p: FileEntry) => ({
|
projectsNoMeta.map(async (p) => ({
|
||||||
entrypointMetadata: await metadata(
|
entrypoint_metadata: await metadata(
|
||||||
p.path + sep + PROJECT_ENTRYPOINT
|
p.path + '/' + PROJECT_ENTRYPOINT
|
||||||
),
|
),
|
||||||
...p,
|
...p,
|
||||||
}))
|
}))
|
||||||
|
|||||||
328
src/Toolbar.tsx
328
src/Toolbar.tsx
@ -1,13 +1,23 @@
|
|||||||
import { ToolTip } from './useStore'
|
import { useStore, toolTips, ToolTip } from './useStore'
|
||||||
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||||
|
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||||
|
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||||
|
import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
||||||
|
import { EqualLength } from './components/Toolbar/EqualLength'
|
||||||
|
import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||||
|
import { Intersect } from './components/Toolbar/Intersect'
|
||||||
|
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||||
|
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||||
|
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||||
|
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||||
|
import { Fragment, useEffect } from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import styles from './Toolbar.module.css'
|
import styles from './Toolbar.module.css'
|
||||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useAppMode } from 'hooks/useAppMode'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import { engineCommandManager } from './lang/std/engineConnection'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
|
|
||||||
export const sketchButtonClassnames = {
|
export const sketchButtonClassnames = {
|
||||||
background:
|
background:
|
||||||
@ -33,151 +43,247 @@ const sketchFnLabels: Record<ToolTip | 'sketch_line' | 'move', string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const { state, send, context } = useModelingContext()
|
const {
|
||||||
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
|
setGuiMode,
|
||||||
const pathId = useMemo(
|
guiMode,
|
||||||
() =>
|
selectionRanges,
|
||||||
isCursorInSketchCommandRange(
|
ast,
|
||||||
engineCommandManager.artifactMap,
|
updateAst,
|
||||||
context.selectionRanges
|
programMemory,
|
||||||
),
|
engineCommandManager,
|
||||||
[engineCommandManager.artifactMap, context.selectionRanges]
|
executeAst,
|
||||||
)
|
} = useStore((s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
ast: s.ast,
|
||||||
|
updateAst: s.updateAst,
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
executeAst: s.executeAst,
|
||||||
|
}))
|
||||||
|
useAppMode()
|
||||||
|
|
||||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
useEffect(() => {
|
||||||
const span = toolbarButtonsRef.current
|
console.log('guiMode', guiMode)
|
||||||
if (!span) {
|
}, [guiMode])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={styles.toolbarButtons + ' ' + className}>
|
||||||
ref={toolbarButtonsRef}
|
{guiMode.mode === 'default' && (
|
||||||
onWheel={handleToolbarButtonsWheelEvent}
|
|
||||||
className={styles.toolbarButtons + ' ' + className}
|
|
||||||
>
|
|
||||||
{state.nextEvents.includes('Enter sketch') && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => send({ type: 'Enter sketch' })}
|
onClick={() => {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'selectFace',
|
||||||
|
})
|
||||||
|
}}
|
||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
Start Sketch
|
Start Sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
{guiMode.mode === 'canEditExtrude' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => send({ type: 'Enter sketch' })}
|
onClick={() => {
|
||||||
|
if (!ast) return
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
const { modifiedAst } = sketchOnExtrudedFace(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
programMemory
|
||||||
|
)
|
||||||
|
updateAst(modifiedAst, true)
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
|
Sketch on Face
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'enterSketchEdit',
|
||||||
|
pathToNode: pathToNode,
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathId: guiMode.pathId,
|
||||||
|
})
|
||||||
|
}}
|
||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
Edit Sketch
|
Edit Sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
<button onClick={() => send({ type: 'Cancel' })} className="group">
|
<>
|
||||||
<ActionIcon icon="exit" className="!p-0.5" size="md" />
|
<button
|
||||||
Exit Sketch
|
onClick={() => {
|
||||||
</button>
|
if (!ast) return
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
|
ast,
|
||||||
|
pathToNode
|
||||||
|
)
|
||||||
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||||
|
Extrude
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!ast) return
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||||
|
Extrude as new
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{state.matches('Sketch') && !state.matches('idle') && (
|
|
||||||
|
{guiMode.mode === 'sketch' && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
state.matches('Sketch.Line Tool')
|
engineCommandManager?.sendSceneCommand({
|
||||||
? send('CancelSketch')
|
type: 'modeling_cmd_req',
|
||||||
: send('Equip tool')
|
cmd_id: uuidv4(),
|
||||||
}
|
cmd: { type: 'edit_mode_exit' },
|
||||||
className={
|
})
|
||||||
'group ' +
|
engineCommandManager?.sendSceneCommand({
|
||||||
(state.matches('Sketch.Line Tool')
|
type: 'modeling_cmd_req',
|
||||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
cmd_id: uuidv4(),
|
||||||
: '')
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
}
|
})
|
||||||
|
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
executeAst()
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
>
|
>
|
||||||
<ActionIcon icon="line" className="!p-0.5" size="md" />
|
<ActionIcon
|
||||||
Line
|
icon="exit"
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
Exit sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{state.matches('Sketch') && (
|
{toolTips
|
||||||
<button
|
.filter(
|
||||||
onClick={() =>
|
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||||
state.matches('Sketch.Move Tool')
|
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||||
? send('CancelSketch')
|
)
|
||||||
: send('Equip move tool')
|
.map((sketchFnName) => {
|
||||||
}
|
if (
|
||||||
className={
|
guiMode.mode !== 'sketch' ||
|
||||||
'group ' +
|
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||||
(state.matches('Sketch.Move Tool')
|
|
||||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionIcon icon="move" className="!p-0.5" size="md" />
|
|
||||||
Move
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{state.matches('Sketch.SketchIdle') &&
|
|
||||||
state.nextEvents
|
|
||||||
.filter(
|
|
||||||
(eventName) =>
|
|
||||||
eventName.includes('Make segment') ||
|
|
||||||
eventName.includes('Constrain')
|
|
||||||
)
|
)
|
||||||
.map((eventName) => (
|
return null
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={eventName}
|
key={sketchFnName}
|
||||||
onClick={() => send(eventName)}
|
onClick={() => {
|
||||||
className="group"
|
engineCommandManager?.sendSceneCommand({
|
||||||
disabled={
|
type: 'modeling_cmd_req',
|
||||||
!state.nextEvents
|
cmd_id: uuidv4(),
|
||||||
.filter((event) => state.can(event as any))
|
cmd: {
|
||||||
.includes(eventName)
|
type: 'set_tool',
|
||||||
|
tool:
|
||||||
|
guiMode.sketchMode === sketchFnName
|
||||||
|
? 'select'
|
||||||
|
: (sketchFnName as any),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
...guiMode,
|
||||||
|
...(guiMode.sketchMode === sketchFnName
|
||||||
|
? {
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
sketchMode: sketchFnName,
|
||||||
|
waitingFirstClick: true,
|
||||||
|
isTooltip: true,
|
||||||
|
pathId: guiMode.pathId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'group ' +
|
||||||
|
(guiMode.sketchMode === sketchFnName
|
||||||
|
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||||
|
: '')
|
||||||
}
|
}
|
||||||
title={eventName}
|
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={'line'} // TODO
|
icon={sketchFnName.includes('line') ? 'line' : 'move'}
|
||||||
|
className="!p-0.5"
|
||||||
bgClassName={sketchButtonClassnames.background}
|
bgClassName={sketchButtonClassnames.background}
|
||||||
iconClassName={sketchButtonClassnames.icon}
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
{eventName
|
{sketchFnLabels[sketchFnName]}
|
||||||
.replace('Make segment ', '')
|
|
||||||
.replace('Constrain ', '')}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
)
|
||||||
{state.matches('idle') && (
|
})}
|
||||||
<button
|
<HorzVert horOrVert="horizontal" />
|
||||||
onClick={() => send('extrude intent')}
|
<HorzVert horOrVert="vertical" />
|
||||||
disabled={!state.can('extrude intent')}
|
<EqualLength />
|
||||||
className="group"
|
<EqualAngle />
|
||||||
title={
|
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||||
state.can('extrude intent')
|
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||||
? 'extrude'
|
<SetAbsDistance buttonType="snapToYAxis" />
|
||||||
: 'sketches need to be closed, or not already extruded'
|
<SetAbsDistance buttonType="xAbs" />
|
||||||
}
|
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||||
>
|
<SetAbsDistance buttonType="snapToXAxis" />
|
||||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||||
Extrude
|
<SetAbsDistance buttonType="yAbs" />
|
||||||
</button>
|
<SetAngleLength angleOrLength="setAngle" />
|
||||||
)}
|
<SetAngleLength angleOrLength="setLength" />
|
||||||
|
<Intersect />
|
||||||
|
<RemoveConstrainingValues />
|
||||||
|
<SetAngleBetween />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
|
||||||
className={
|
|
||||||
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||||
{state.matches('Sketch') ? '2D' : '3D'}
|
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||||
</span>
|
</span>
|
||||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||||
<ToolbarButtons />
|
<ToolbarButtons />
|
||||||
@ -213,7 +319,7 @@ export const Toolbar = () => {
|
|||||||
<p
|
<p
|
||||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||||
>
|
>
|
||||||
You're in {state.matches('Sketch') ? '2D' : '3D'}
|
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||||
</p>
|
</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">
|
<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" />
|
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||||
|
|||||||
@ -23,7 +23,10 @@ type ActionButtonAsLink = BaseActionButtonProps &
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActionButtonAsExternal = BaseActionButtonProps &
|
type ActionButtonAsExternal = BaseActionButtonProps &
|
||||||
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
Omit<
|
||||||
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
keyof BaseActionButtonProps
|
||||||
|
> & {
|
||||||
Element: 'externalLink'
|
Element: 'externalLink'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,17 +69,12 @@ export const ActionButton = (props: ActionButtonProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'externalLink': {
|
case 'externalLink': {
|
||||||
const { Element, to, icon, children, className, ...rest } = props
|
const { Element, icon, children, className, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<Link
|
<a className={classNames} {...rest}>
|
||||||
to={to || paths.INDEX}
|
|
||||||
className={classNames}
|
|
||||||
{...rest}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { IndexLoaderData } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
@ -8,7 +8,7 @@ import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
|||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
project?: Omit<IndexLoaderData, 'code'>
|
project?: ProjectWithEntryPointMetadata
|
||||||
className?: string
|
className?: string
|
||||||
enableMenu?: boolean
|
enableMenu?: boolean
|
||||||
}
|
}
|
||||||
@ -20,8 +20,11 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const {
|
||||||
const user = auth?.context?.user
|
auth: {
|
||||||
|
context: { user },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
@ -32,11 +35,7 @@ export const AppHeader = ({
|
|||||||
className
|
className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ProjectSidebarMenu
|
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||||
renderAsLink={!enableMenu}
|
|
||||||
project={project?.project}
|
|
||||||
file={project?.file}
|
|
||||||
/>
|
|
||||||
{/* Toolbar if the context deems it */}
|
{/* Toolbar if the context deems it */}
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||||
@ -45,7 +44,7 @@ export const AppHeader = ({
|
|||||||
)}
|
)}
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || (
|
{children || (
|
||||||
<div className="flex items-center gap-1 ml-auto">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<NetworkHealthIndicator />
|
<NetworkHealthIndicator />
|
||||||
<UserSidebarMenu user={user} />
|
<UserSidebarMenu user={user} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
export function AstExplorer() {
|
export function AstExplorer() {
|
||||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
||||||
const { context } = useModelingContext()
|
ast: s.ast,
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
}))
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
// TODO maybe need to have callback to make sure it stays in sync
|
ast,
|
||||||
kclManager.ast,
|
selectionRanges.codeBasedSelections?.[0]?.range
|
||||||
context.selectionRanges.codeBasedSelections?.[0]?.range
|
|
||||||
)
|
)
|
||||||
const node = getNodeFromPath(kclManager.ast, pathToNode).node
|
const node = getNodeFromPath(ast, pathToNode).node
|
||||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,11 +46,7 @@ export function AstExplorer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||||
<DisplayObj
|
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
|
||||||
obj={kclManager.ast}
|
|
||||||
filterKeys={filterKeys}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -88,8 +84,10 @@ function DisplayObj({
|
|||||||
filterKeys: string[]
|
filterKeys: string[]
|
||||||
node: any
|
node: any
|
||||||
}) {
|
}) {
|
||||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
const { setHighlightRange, setCursor2 } = useStore((s) => ({
|
||||||
const { send } = useModelingContext()
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
setCursor2: s.setCursor2,
|
||||||
|
}))
|
||||||
const ref = useRef<HTMLPreElement>(null)
|
const ref = useRef<HTMLPreElement>(null)
|
||||||
const [hasCursor, setHasCursor] = useState(false)
|
const [hasCursor, setHasCursor] = useState(false)
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||||
@ -120,16 +118,7 @@ function DisplayObj({
|
|||||||
setHighlightRange([obj?.start || 0, obj.end])
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
send({
|
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'singleCodeCursor',
|
|
||||||
selection: {
|
|
||||||
type: 'default',
|
|
||||||
range: [obj?.start || 0, obj.end || 0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import { parse, BinaryPart, Value, executor } from '../lang/wasm'
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
|
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
||||||
|
import { executor } from '../lang/executor'
|
||||||
import {
|
import {
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
@ -7,9 +9,7 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
} from '../lang/modifyAst'
|
} from '../lang/modifyAst'
|
||||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { useStore } from '../useStore'
|
||||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
onVarClick,
|
onVarClick,
|
||||||
@ -92,9 +92,14 @@ export function useCalc({
|
|||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
} {
|
} {
|
||||||
const { programMemory } = useKclContext()
|
const { ast, programMemory, selectionRange, engineCommandManager } = useStore(
|
||||||
const { context } = useModelingContext()
|
(s) => ({
|
||||||
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
ast: s.ast,
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
selectionRange: s.selectionRanges.codeBasedSelections[0].range,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
})
|
||||||
|
)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [availableVarInfo, setAvailableVarInfo] = useState<
|
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||||
ReturnType<typeof findAllPreviousVariables>
|
ReturnType<typeof findAllPreviousVariables>
|
||||||
@ -114,7 +119,9 @@ export function useCalc({
|
|||||||
inputRef.current &&
|
inputRef.current &&
|
||||||
inputRef.current.setSelectionRange(0, String(value).length)
|
inputRef.current.setSelectionRange(0, String(value).length)
|
||||||
}, 100)
|
}, 100)
|
||||||
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
if (ast) {
|
||||||
|
setNewVariableName(findUniqueName(ast, valueName))
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -127,30 +134,21 @@ export function useCalc({
|
|||||||
}, [newVariableName])
|
}, [newVariableName])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!programMemory || !selectionRange) return
|
if (!ast || !programMemory || !selectionRange) return
|
||||||
const varInfo = findAllPreviousVariables(
|
const varInfo = findAllPreviousVariables(ast, programMemory, selectionRange)
|
||||||
kclManager.ast,
|
|
||||||
programMemory,
|
|
||||||
selectionRange
|
|
||||||
)
|
|
||||||
setAvailableVarInfo(varInfo)
|
setAvailableVarInfo(varInfo)
|
||||||
}, [kclManager.ast, programMemory, selectionRange])
|
}, [ast, programMemory, selectionRange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!engineCommandManager) return
|
||||||
try {
|
try {
|
||||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const _programMem: any = { root: {}, return: null }
|
const _programMem: any = { root: {}, return: null }
|
||||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||||
})
|
})
|
||||||
|
executor(ast, _programMem, engineCommandManager).then((programMemory) => {
|
||||||
executor(
|
|
||||||
ast,
|
|
||||||
_programMem,
|
|
||||||
engineCommandManager,
|
|
||||||
kclManager.defaultPlanes
|
|
||||||
).then((programMemory) => {
|
|
||||||
const resultDeclaration = ast.body.find(
|
const resultDeclaration = ast.body.find(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.type === 'VariableDeclaration' &&
|
a.type === 'VariableDeclaration' &&
|
||||||
|
|||||||
@ -5,13 +5,16 @@ import {
|
|||||||
faEllipsis,
|
faEllipsis,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionIcon } from './ActionIcon'
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
import styles from './CodeMenu.module.css'
|
import styles from './CodeMenu.module.css'
|
||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { editorShortcutMeta } from './TextEditor'
|
import { editorShortcutMeta } from './TextEditor'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
const { formatCode } = useStore((s) => ({
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
}))
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -38,10 +41,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button onClick={() => formatCode()} className={styles.button}>
|
||||||
onClick={() => kclManager.format()}
|
|
||||||
className={styles.button}
|
|
||||||
>
|
|
||||||
<span>Format code</span>
|
<span>Format code</span>
|
||||||
<small>{editorShortcutMeta.formatCode.display}</small>
|
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
export type CustomIconName =
|
export type CustomIconName =
|
||||||
| 'createFile'
|
|
||||||
| 'createFolder'
|
|
||||||
| 'equal'
|
| 'equal'
|
||||||
| 'exit'
|
| 'exit'
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
| 'file'
|
|
||||||
| 'horizontal'
|
| 'horizontal'
|
||||||
| 'line'
|
| 'line'
|
||||||
| 'move'
|
| 'move'
|
||||||
@ -19,38 +16,6 @@ export const CustomIcon = ({
|
|||||||
name: CustomIconName
|
name: CustomIconName
|
||||||
} & React.SVGProps<SVGSVGElement>) => {
|
} & React.SVGProps<SVGSVGElement>) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'createFile':
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
case 'createFolder':
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
case 'equal':
|
case 'equal':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -96,20 +61,6 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
case 'file':
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
case 'horizontal':
|
case 'horizontal':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { EngineCommand } from '../lang/std/engineConnection'
|
import { EngineCommand } from '../lang/std/engineConnection'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
@ -6,7 +7,6 @@ import { ActionButton } from '../components/ActionButton'
|
|||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { isReducedMotion } from 'lang/util'
|
import { isReducedMotion } from 'lang/util'
|
||||||
import { AstExplorer } from './AstExplorer'
|
import { AstExplorer } from './AstExplorer'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
|
|
||||||
type SketchModeCmd = Extract<
|
type SketchModeCmd = Extract<
|
||||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||||
@ -14,6 +14,9 @@ type SketchModeCmd = Extract<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||||
|
const { engineCommandManager } = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
}))
|
||||||
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
|
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
|
||||||
type: 'default_camera_enable_sketch_mode',
|
type: 'default_camera_enable_sketch_mode',
|
||||||
origin: { x: 0, y: 0, z: 0 },
|
origin: { x: 0, y: 0, z: 0 },
|
||||||
@ -67,18 +70,19 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
|||||||
className="w-16"
|
className="w-16"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={sketchModeCmd.ortho}
|
checked={sketchModeCmd.ortho}
|
||||||
onChange={(a) =>
|
onChange={(a) => {
|
||||||
|
console.log(a, (a as any).checked)
|
||||||
setSketchModeCmd({
|
setSketchModeCmd({
|
||||||
...sketchModeCmd,
|
...sketchModeCmd,
|
||||||
ortho: a.target.checked,
|
ortho: a.target.checked,
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: sketchModeCmd,
|
cmd: sketchModeCmd,
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
|
|||||||
@ -47,9 +47,11 @@ export const ErrorPage = () => {
|
|||||||
Clear storage
|
Clear storage
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="externalLink"
|
Element="link"
|
||||||
icon={{ icon: faBug }}
|
icon={{ icon: faBug }}
|
||||||
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
to="https://discord.com/channels/915388055236509727/1138967922614743060"
|
||||||
>
|
>
|
||||||
Report Bug
|
Report Bug
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
@ -1,40 +1,31 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import Modal from 'react-modal'
|
import Modal from 'react-modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
|
||||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
|
||||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
|
||||||
|
|
||||||
interface ExportButtonProps extends React.PropsWithChildren {
|
interface ExportButtonProps extends React.PropsWithChildren {
|
||||||
className?: {
|
className?: {
|
||||||
button?: string
|
button?: string
|
||||||
icon?: string
|
// If we wanted more classname configuration of sub-elements,
|
||||||
bg?: string
|
// put them here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||||
|
const { engineCommandManager } = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
}))
|
||||||
|
|
||||||
const [modalIsOpen, setIsOpen] = React.useState(false)
|
const [modalIsOpen, setIsOpen] = React.useState(false)
|
||||||
const {
|
|
||||||
settings: {
|
|
||||||
state: {
|
|
||||||
context: { baseUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
const defaultType = 'gltf'
|
const defaultType = 'gltf'
|
||||||
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
|
const [type, setType] = React.useState(defaultType)
|
||||||
const defaultStorage = 'embedded'
|
|
||||||
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
|
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
@ -47,7 +38,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
// Default to gltf and embedded.
|
// Default to gltf and embedded.
|
||||||
const initialValues: OutputFormat = {
|
const initialValues: OutputFormat = {
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
storage: defaultStorage,
|
storage: 'embedded',
|
||||||
presentation: 'pretty',
|
presentation: 'pretty',
|
||||||
}
|
}
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
@ -75,18 +66,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (values.type === 'obj' || values.type === 'stl') {
|
engineCommandManager?.sendSceneCommand({
|
||||||
values.units = baseUnit
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
values.type === 'ply' ||
|
|
||||||
values.type === 'stl' ||
|
|
||||||
values.type === 'gltf'
|
|
||||||
) {
|
|
||||||
// Set the storage type.
|
|
||||||
values.storage = storage
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'export',
|
type: 'export',
|
||||||
@ -95,7 +75,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
// in the scene to export. In that case, you'd pass the IDs thru here.
|
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
format: values,
|
format: values,
|
||||||
source_unit: baseUnit,
|
|
||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
})
|
})
|
||||||
@ -109,11 +88,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{
|
icon={{ icon: faFileExport }}
|
||||||
icon: faFileExport,
|
|
||||||
iconClassName: className?.icon,
|
|
||||||
bgClassName: className?.bg,
|
|
||||||
}}
|
|
||||||
className={className?.button}
|
className={className?.button}
|
||||||
>
|
>
|
||||||
{children || 'Export'}
|
{children || 'Export'}
|
||||||
@ -134,17 +109,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
id="type"
|
id="type"
|
||||||
name="type"
|
name="type"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setType(e.target.value as OutputTypeKey)
|
setType(e.target.value)
|
||||||
if (e.target.value === 'gltf') {
|
|
||||||
// Set default to embedded.
|
|
||||||
setStorage('embedded')
|
|
||||||
} else if (e.target.value === 'ply') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
} else if (e.target.value === 'stl') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
}
|
|
||||||
formik.handleChange(e)
|
formik.handleChange(e)
|
||||||
}}
|
}}
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
@ -162,10 +127,10 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<select
|
<select
|
||||||
id="storage"
|
id="storage"
|
||||||
name="storage"
|
name="storage"
|
||||||
onChange={(e) => {
|
onChange={formik.handleChange}
|
||||||
setStorage(e.target.value as StorageUnion)
|
value={
|
||||||
formik.handleChange(e)
|
'storage' in formik.values ? formik.values.storage : ''
|
||||||
}}
|
}
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
>
|
>
|
||||||
{type === 'gltf' && (
|
{type === 'gltf' && (
|
||||||
|
|||||||
@ -1,158 +0,0 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
|
||||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
|
||||||
import { IndexLoaderData, paths } from '../Router'
|
|
||||||
import React, { createContext } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import {
|
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
EventFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
|
||||||
StateFrom,
|
|
||||||
} from 'xstate'
|
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
|
|
||||||
import {
|
|
||||||
createDir,
|
|
||||||
removeDir,
|
|
||||||
removeFile,
|
|
||||||
renameFile,
|
|
||||||
writeFile,
|
|
||||||
} from '@tauri-apps/api/fs'
|
|
||||||
import { FILE_EXT, readProject } from 'lib/tauriFS'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
|
||||||
import { sep } from '@tauri-apps/api/path'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
|
||||||
state: StateFrom<T>
|
|
||||||
context: ContextFrom<T>
|
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileContext = createContext(
|
|
||||||
{} as MachineContext<typeof fileMachine>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const FileMachineProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
|
||||||
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
|
||||||
|
|
||||||
const [state, send] = useMachine(fileMachine, {
|
|
||||||
context: {
|
|
||||||
project,
|
|
||||||
selectedDirectory: project,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
navigateToFile: (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine>
|
|
||||||
) => {
|
|
||||||
if (event.data && 'name' in event.data) {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
navigate(
|
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
|
||||||
context.selectedDirectory + sep + event.data.name
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toastSuccess: (_, event) =>
|
|
||||||
event.data && toast.success((event.data || '') + ''),
|
|
||||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
|
||||||
const newFiles = isTauri()
|
|
||||||
? await readProject(context.project.path)
|
|
||||||
: []
|
|
||||||
return {
|
|
||||||
...context.project,
|
|
||||||
children: newFiles,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createFile: async (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine, 'Create file'>
|
|
||||||
) => {
|
|
||||||
let name = event.data.name.trim() || DEFAULT_FILE_NAME
|
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
|
||||||
await createDir(context.selectedDirectory.path + sep + name)
|
|
||||||
} else {
|
|
||||||
await writeFile(
|
|
||||||
context.selectedDirectory.path +
|
|
||||||
sep +
|
|
||||||
name +
|
|
||||||
(name.endsWith(FILE_EXT) ? '' : FILE_EXT),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
|
||||||
},
|
|
||||||
renameFile: async (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
|
||||||
) => {
|
|
||||||
const { oldName, newName, isDir } = event.data
|
|
||||||
let name = newName ? newName : DEFAULT_FILE_NAME
|
|
||||||
|
|
||||||
await renameFile(
|
|
||||||
context.selectedDirectory.path + sep + oldName,
|
|
||||||
context.selectedDirectory.path +
|
|
||||||
sep +
|
|
||||||
name +
|
|
||||||
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
|
|
||||||
)
|
|
||||||
},
|
|
||||||
deleteFile: async (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
|
||||||
) => {
|
|
||||||
const isDir = !!event.data.children
|
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
await removeDir(event.data.path, {
|
|
||||||
recursive: true,
|
|
||||||
}).catch((e) => console.error('Error deleting directory', e))
|
|
||||||
} else {
|
|
||||||
await removeFile(event.data.path).catch((e) =>
|
|
||||||
console.error('Error deleting file', e)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
|
||||||
event.data.name
|
|
||||||
}"`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
|
||||||
if (event.type !== 'done.invoke.read-files') return false
|
|
||||||
return !!event?.data?.children && event.data.children.length > 0
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileContext.Provider
|
|
||||||
value={{
|
|
||||||
send,
|
|
||||||
state,
|
|
||||||
context: state.context, // just a convenience, can remove if we need to save on memory
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</FileContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileMachineProvider
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
.folder {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder::after {
|
|
||||||
content: '';
|
|
||||||
width: 1px;
|
|
||||||
z-index: -1;
|
|
||||||
@apply absolute top-0 bottom-0;
|
|
||||||
left: calc(var(--indent-line-left, 1rem) + 0.25rem);
|
|
||||||
@apply bg-chalkboard-30;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .folder::after {
|
|
||||||
@apply bg-chalkboard-80;
|
|
||||||
}
|
|
||||||
@ -1,400 +0,0 @@
|
|||||||
import { IndexLoaderData, paths } from 'Router'
|
|
||||||
import { ActionButton } from './ActionButton'
|
|
||||||
import Tooltip from './Tooltip'
|
|
||||||
import { FileEntry } from '@tauri-apps/api/fs'
|
|
||||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { Dialog, Disclosure } from '@headlessui/react'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { useFileContext } from 'hooks/useFileContext'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
import styles from './FileTree.module.css'
|
|
||||||
import { sortProject } from 'lib/tauriFS'
|
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
|
||||||
return `calc(1rem * ${level + 1})`
|
|
||||||
}
|
|
||||||
|
|
||||||
function RenameForm({
|
|
||||||
fileOrDir,
|
|
||||||
setIsRenaming,
|
|
||||||
level = 0,
|
|
||||||
}: {
|
|
||||||
fileOrDir: FileEntry
|
|
||||||
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
|
|
||||||
level?: number
|
|
||||||
}) {
|
|
||||||
const { send } = useFileContext()
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsRenaming(false)
|
|
||||||
send({
|
|
||||||
type: 'Rename file',
|
|
||||||
data: {
|
|
||||||
oldName: fileOrDir.name || '',
|
|
||||||
newName: inputRef.current?.value || fileOrDir.name || '',
|
|
||||||
isDir: fileOrDir.children !== undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.stopPropagation()
|
|
||||||
setIsRenaming(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleRenameSubmit}>
|
|
||||||
<label>
|
|
||||||
<span className="sr-only">Rename file</span>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
placeholder={fileOrDir.name}
|
|
||||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={() => setIsRenaming(false)}
|
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button className="sr-only" type="submit">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteConfirmationDialog({
|
|
||||||
fileOrDir,
|
|
||||||
setIsOpen,
|
|
||||||
}: {
|
|
||||||
fileOrDir: FileEntry
|
|
||||||
setIsOpen: Dispatch<React.SetStateAction<boolean>>
|
|
||||||
}) {
|
|
||||||
const { send } = useFileContext()
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={true}
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
className="relative z-50"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
|
|
||||||
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
|
|
||||||
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
|
|
||||||
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Description className="my-6">
|
|
||||||
This will permanently delete "{fileOrDir.name || 'this file'}"
|
|
||||||
{fileOrDir.children !== undefined
|
|
||||||
? ' and all of its contents. '
|
|
||||||
: '. '}
|
|
||||||
This action cannot be undone.
|
|
||||||
</Dialog.Description>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={async () => {
|
|
||||||
send({ type: 'Delete file', data: fileOrDir })
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
icon={{
|
|
||||||
icon: faTrashAlt,
|
|
||||||
bgClassName: 'bg-destroy-80',
|
|
||||||
iconClassName:
|
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
|
||||||
}}
|
|
||||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileTreeItem = ({
|
|
||||||
project,
|
|
||||||
currentFile,
|
|
||||||
fileOrDir,
|
|
||||||
closePanel,
|
|
||||||
level = 0,
|
|
||||||
}: {
|
|
||||||
project?: IndexLoaderData['project']
|
|
||||||
currentFile?: IndexLoaderData['file']
|
|
||||||
fileOrDir: FileEntry
|
|
||||||
closePanel: (
|
|
||||||
focusableElement?:
|
|
||||||
| HTMLElement
|
|
||||||
| React.MutableRefObject<HTMLElement | null>
|
|
||||||
| undefined
|
|
||||||
) => void
|
|
||||||
level?: number
|
|
||||||
}) => {
|
|
||||||
const { send, context } = useFileContext()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false)
|
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
|
||||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
|
||||||
|
|
||||||
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
|
||||||
if (e.metaKey && e.key === 'Backspace') {
|
|
||||||
// Open confirmation dialog
|
|
||||||
setIsConfirmingDelete(true)
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
// Show the renaming form
|
|
||||||
setIsRenaming(true)
|
|
||||||
} else if (e.code === 'Space') {
|
|
||||||
openFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFile() {
|
|
||||||
if (fileOrDir.children !== undefined) return // Don't open directories
|
|
||||||
kclManager.setCode('')
|
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
|
||||||
closePanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{fileOrDir.children === undefined ? (
|
|
||||||
<li
|
|
||||||
className={
|
|
||||||
'group m-0 p-0 border-solid border-0 text-energy-100 hover:text-energy-70 hover:bg-energy-10/50 dark:text-energy-30 dark:hover:!text-energy-20 dark:hover:bg-energy-90/50 focus-within:bg-energy-10/80 dark:focus-within:bg-energy-80/50 hover:focus-within:bg-energy-10/80 dark:hover:focus-within:bg-energy-80/50 ' +
|
|
||||||
(isCurrentFile ? 'bg-energy-10/50 dark:bg-energy-90/50' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!isRenaming ? (
|
|
||||||
<button
|
|
||||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
|
||||||
onDoubleClick={openFile}
|
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
>
|
|
||||||
<KclIcon
|
|
||||||
className={
|
|
||||||
'inline-block w-3 ' +
|
|
||||||
(isCurrentFile
|
|
||||||
? 'text-energy-90 dark:text-energy-10'
|
|
||||||
: 'text-energy-50 dark:text-energy-50')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{fileOrDir.name}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<RenameForm
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
setIsRenaming={setIsRenaming}
|
|
||||||
level={level}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
) : (
|
|
||||||
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
|
|
||||||
{({ open }) => (
|
|
||||||
<div className="group">
|
|
||||||
{!isRenaming ? (
|
|
||||||
<Disclosure.Button
|
|
||||||
className={
|
|
||||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 text-chalkboard-70 dark:text-chalkboard-30 hover:bg-energy-10/50 dark:hover:bg-energy-90/50' +
|
|
||||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
|
||||||
? ' group-focus-within:bg-chalkboard-20/50 dark:group-focus-within:bg-chalkboard-80/20 hover:group-focus-within:bg-chalkboard-20 dark:hover:group-focus-within:bg-chalkboard-80/20 group-active:bg-chalkboard-20/50 dark:group-active:bg-chalkboard-80/20 hover:group-active:bg-chalkboard-20/50 dark:hover:group-active:bg-chalkboard-80/20'
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
|
||||||
onClickCapture={(e) =>
|
|
||||||
send({ type: 'Set selected directory', data: fileOrDir })
|
|
||||||
}
|
|
||||||
onFocusCapture={(e) =>
|
|
||||||
send({ type: 'Set selected directory', data: fileOrDir })
|
|
||||||
}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faChevronRight}
|
|
||||||
className={
|
|
||||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
|
||||||
(open ? 'transform rotate-90' : '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{fileOrDir.name}
|
|
||||||
</Disclosure.Button>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex items-center"
|
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faChevronRight}
|
|
||||||
className={
|
|
||||||
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
|
||||||
(open ? 'transform rotate-90' : '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<RenameForm
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
setIsRenaming={setIsRenaming}
|
|
||||||
level={-1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Disclosure.Panel
|
|
||||||
className={styles.folder}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--indent-line-left': getIndentationCSS(level),
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
className="m-0 p-0"
|
|
||||||
onClickCapture={(e) => {
|
|
||||||
send({ type: 'Set selected directory', data: fileOrDir })
|
|
||||||
}}
|
|
||||||
onFocusCapture={(e) =>
|
|
||||||
send({ type: 'Set selected directory', data: fileOrDir })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{fileOrDir.children?.map((child) => (
|
|
||||||
<FileTreeItem
|
|
||||||
fileOrDir={child}
|
|
||||||
project={project}
|
|
||||||
currentFile={currentFile}
|
|
||||||
closePanel={closePanel}
|
|
||||||
level={level + 1}
|
|
||||||
key={level + '-' + child.path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
)}
|
|
||||||
{isConfirmingDelete && (
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
setIsOpen={setIsConfirmingDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileTreeProps {
|
|
||||||
className?: string
|
|
||||||
file?: IndexLoaderData['file']
|
|
||||||
closePanel: (
|
|
||||||
focusableElement?:
|
|
||||||
| HTMLElement
|
|
||||||
| React.MutableRefObject<HTMLElement | null>
|
|
||||||
| undefined
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTree = ({
|
|
||||||
className = '',
|
|
||||||
file,
|
|
||||||
closePanel,
|
|
||||||
}: FileTreeProps) => {
|
|
||||||
const { send, context } = useFileContext()
|
|
||||||
useHotkeys('meta + n', createFile)
|
|
||||||
useHotkeys('meta + shift + n', createFolder)
|
|
||||||
|
|
||||||
async function createFile() {
|
|
||||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createFolder() {
|
|
||||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
|
|
||||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'createFile',
|
|
||||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
|
||||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 border-none bg-transparent !outline-none"
|
|
||||||
onClick={createFile}
|
|
||||||
>
|
|
||||||
<Tooltip position="inlineStart" delay={750}>
|
|
||||||
Create File
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'createFolder',
|
|
||||||
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
|
||||||
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 border-none bg-transparent !outline-none"
|
|
||||||
onClick={createFolder}
|
|
||||||
>
|
|
||||||
<Tooltip position="inlineStart" delay={750}>
|
|
||||||
Create Folder
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto max-h-full pb-12">
|
|
||||||
<ul
|
|
||||||
className="m-0 p-0 text-sm"
|
|
||||||
onClickCapture={(e) => {
|
|
||||||
send({ type: 'Set selected directory', data: context.project })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
|
||||||
<FileTreeItem
|
|
||||||
project={context.project}
|
|
||||||
currentFile={file}
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
closePanel={closePanel}
|
|
||||||
key={fileOrDir.path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function KclIcon({ className = '' }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={className}
|
|
||||||
viewBox="0 0 40 40"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -24,7 +24,9 @@ import {
|
|||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { invoke } from '@tauri-apps/api'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { Themes } from '../lib/theme'
|
import { Themes } from '../lib/theme'
|
||||||
import { useKclContext } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
const ReactJsonTypeHack = ReactJson as any
|
const ReactJsonTypeHack = ReactJson as any
|
||||||
|
|
||||||
@ -11,7 +11,9 @@ interface LogPanelProps extends CollapsiblePanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||||
const { logs } = useKclContext()
|
const { logs } = useStore(({ logs }) => ({
|
||||||
|
logs,
|
||||||
|
}))
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
if (element) {
|
if (element) {
|
||||||
@ -45,19 +47,21 @@ export const KCLErrors = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: LogPanelProps) => {
|
}: LogPanelProps) => {
|
||||||
const { errors } = useKclContext()
|
const { kclErrors } = useStore(({ kclErrors }) => ({
|
||||||
|
kclErrors,
|
||||||
|
}))
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||||
}
|
}
|
||||||
}, [errors])
|
}, [kclErrors])
|
||||||
return (
|
return (
|
||||||
<CollapsiblePanel {...props}>
|
<CollapsiblePanel {...props}>
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<div className="absolute inset-0 flex flex-col">
|
<div className="absolute inset-0 flex flex-col">
|
||||||
<ReactJsonTypeHack
|
<ReactJsonTypeHack
|
||||||
src={errors}
|
src={kclErrors}
|
||||||
collapsed={1}
|
collapsed={1}
|
||||||
collapseStringsAfterLength={60}
|
collapseStringsAfterLength={60}
|
||||||
enableClipboard={false}
|
enableClipboard={false}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { processMemory } from './MemoryPanel'
|
import { processMemory } from './MemoryPanel'
|
||||||
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { initPromise, parse } from '../lang/wasm'
|
import { initPromise } from '../lang/rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -14,20 +15,18 @@ describe('processMemory', () => {
|
|||||||
}
|
}
|
||||||
const otherVar = myFn(5)
|
const otherVar = myFn(5)
|
||||||
|
|
||||||
const theExtrude = startSketchOn('XY')
|
const theExtrude = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-2.4, myVar], %)
|
|> lineTo([-2.4, myVar], %)
|
||||||
|> lineTo([-0.76, otherVar], %)
|
|> lineTo([-0.76, otherVar], %)
|
||||||
|> extrude(4, %)
|
|> extrude(4, %)
|
||||||
|
|
||||||
const theSketch = startSketchOn('XY')
|
const theSketch = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-3.35, 0.17], %)
|
|> lineTo([-3.35, 0.17], %)
|
||||||
|> lineTo([0.98, 5.16], %)
|
|> lineTo([0.98, 5.16], %)
|
||||||
|> lineTo([2.15, 4.32], %)
|
|> lineTo([2.15, 4.32], %)
|
||||||
// |> rx(90, %)
|
// |> rx(90, %)
|
||||||
show(theExtrude, theSketch)`
|
show(theExtrude, theSketch)`
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast, {
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
root: {},
|
root: {},
|
||||||
return: null,
|
return: null,
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||||
import { Themes } from '../lib/theme'
|
import { Themes } from '../lib/theme'
|
||||||
import { useKclContext } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
theme?: Exclude<Themes, Themes.System>
|
theme?: Exclude<Themes, Themes.System>
|
||||||
@ -13,7 +13,9 @@ export const MemoryPanel = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: MemoryPanelProps) => {
|
}: MemoryPanelProps) => {
|
||||||
const { programMemory } = useKclContext()
|
const { programMemory } = useStore((s) => ({
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
}))
|
||||||
const ProcessedMemory = useMemo(
|
const ProcessedMemory = useMemo(
|
||||||
() => processMemory(programMemory),
|
() => processMemory(programMemory),
|
||||||
[programMemory]
|
[programMemory]
|
||||||
@ -48,7 +50,7 @@ export const MemoryPanel = ({
|
|||||||
|
|
||||||
export const processMemory = (programMemory: ProgramMemory) => {
|
export const processMemory = (programMemory: ProgramMemory) => {
|
||||||
const processedMemory: any = {}
|
const processedMemory: any = {}
|
||||||
Object.keys(programMemory?.root || {}).forEach((key) => {
|
Object.keys(programMemory.root).forEach((key) => {
|
||||||
const val = programMemory.root[key]
|
const val = programMemory.root[key]
|
||||||
if (typeof val.value !== 'function') {
|
if (typeof val.value !== 'function') {
|
||||||
if (val.type === 'SketchGroup') {
|
if (val.type === 'SketchGroup') {
|
||||||
|
|||||||
@ -1,459 +0,0 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
|
||||||
import React, { createContext, useEffect, useRef } from 'react'
|
|
||||||
import {
|
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
|
||||||
StateFrom,
|
|
||||||
assign,
|
|
||||||
} from 'xstate'
|
|
||||||
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
|
||||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|
||||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
|
||||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { addStartSketch } from 'lang/modifyAst'
|
|
||||||
import { roundOff } from 'lib/utils'
|
|
||||||
import {
|
|
||||||
recast,
|
|
||||||
parse,
|
|
||||||
Program,
|
|
||||||
VariableDeclarator,
|
|
||||||
PipeExpression,
|
|
||||||
CallExpression,
|
|
||||||
} from 'lang/wasm'
|
|
||||||
import { getNodeFromPath } from 'lang/queryAst'
|
|
||||||
import {
|
|
||||||
addCloseToPipe,
|
|
||||||
addNewSketchLn,
|
|
||||||
compareVec2Epsilon,
|
|
||||||
} from 'lang/std/sketch'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
|
||||||
import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { pathMapToSelections } from 'lang/util'
|
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
|
||||||
state: StateFrom<T>
|
|
||||||
context: ContextFrom<T>
|
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModelingMachineContext = createContext(
|
|
||||||
{} as MachineContext<typeof modelingMachine>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ModelingMachineProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const { auth } = useGlobalStateContext()
|
|
||||||
const token = auth?.context?.token
|
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
|
||||||
useSetupEngineManager(streamRef, token)
|
|
||||||
|
|
||||||
const { isShiftDown, editorView } = useStore((s) => ({
|
|
||||||
isShiftDown: s.isShiftDown,
|
|
||||||
editorView: s.editorView,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// const { commands } = useCommandsContext()
|
|
||||||
|
|
||||||
// Settings machine setup
|
|
||||||
// const retrievedSettings = useRef(
|
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
|
||||||
// )
|
|
||||||
|
|
||||||
// What should we persist from modeling state? Nothing?
|
|
||||||
// const persistedSettings = Object.assign(
|
|
||||||
// settingsMachine.initialState.context,
|
|
||||||
// JSON.parse(retrievedSettings.current) as Partial<
|
|
||||||
// (typeof settingsMachine)['context']
|
|
||||||
// >
|
|
||||||
// )
|
|
||||||
|
|
||||||
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
|
||||||
// context: persistedSettings,
|
|
||||||
actions: {
|
|
||||||
'Modify AST': () => {},
|
|
||||||
'Update code selection cursors': () => {},
|
|
||||||
'show default planes': () => {
|
|
||||||
kclManager.showPlanes()
|
|
||||||
},
|
|
||||||
'create path': assign({
|
|
||||||
sketchEnginePathId: () => {
|
|
||||||
const sketchUuid = uuidv4()
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: sketchUuid,
|
|
||||||
cmd: {
|
|
||||||
type: 'start_path',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'edit_mode_enter',
|
|
||||||
target: sketchUuid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return sketchUuid
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'AST start new sketch': assign(
|
|
||||||
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
|
||||||
if (!axis) {
|
|
||||||
// Something really weird must have happened for this to happen.
|
|
||||||
console.error('axis is undefined for starting a new sketch')
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
if (!segmentId) {
|
|
||||||
// Something really weird must have happened for this to happen.
|
|
||||||
console.error('segmentId is undefined for starting a new sketch')
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _addStartSketch = addStartSketch(
|
|
||||||
kclManager.ast,
|
|
||||||
axis,
|
|
||||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
|
||||||
[
|
|
||||||
roundOff(coords[1].x - coords[0].x),
|
|
||||||
roundOff(coords[1].y - coords[0].y),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
const _modifiedAst = _addStartSketch.modifiedAst
|
|
||||||
const _pathToNode = _addStartSketch.pathToNode
|
|
||||||
const newCode = recast(_modifiedAst)
|
|
||||||
const astWithUpdatedSource = parse(newCode)
|
|
||||||
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
|
||||||
astWithUpdatedSource,
|
|
||||||
_pathToNode
|
|
||||||
).node
|
|
||||||
const startProfileAtCallExp = updatedPipeNode.body.find(
|
|
||||||
(exp) =>
|
|
||||||
exp.type === 'CallExpression' &&
|
|
||||||
exp.callee.name === 'startProfileAt'
|
|
||||||
)
|
|
||||||
if (startProfileAtCallExp)
|
|
||||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
|
||||||
commandType: 'extend_path',
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
|
||||||
const lineCallExp = updatedPipeNode.body.find(
|
|
||||||
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
|
||||||
)
|
|
||||||
if (lineCallExp)
|
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
|
||||||
commandType: 'extend_path',
|
|
||||||
parentId: sketchEnginePathId,
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
|
||||||
|
|
||||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
|
||||||
|
|
||||||
return {
|
|
||||||
sketchPathToNode: _pathToNode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
'AST add line segment': async (
|
|
||||||
{ sketchPathToNode, sketchEnginePathId },
|
|
||||||
{ data: { coords, segmentId } }
|
|
||||||
) => {
|
|
||||||
if (!sketchPathToNode) return
|
|
||||||
const lastCoord = coords[coords.length - 1]
|
|
||||||
|
|
||||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'path_get_info',
|
|
||||||
path_id: sketchEnginePathId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const firstSegment = pathInfo?.data?.data?.segments.find(
|
|
||||||
(seg: any) => seg.command === 'line_to'
|
|
||||||
)
|
|
||||||
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: firstSegment.command_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
|
||||||
|
|
||||||
const isClose = compareVec2Epsilon(
|
|
||||||
[startPathCoord.x, startPathCoord.y],
|
|
||||||
[lastCoord.x, lastCoord.y]
|
|
||||||
)
|
|
||||||
|
|
||||||
let _modifiedAst: Program
|
|
||||||
if (!isClose) {
|
|
||||||
const newSketchLn = addNewSketchLn({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
to: [lastCoord.x, lastCoord.y],
|
|
||||||
from: [coords[0].x, coords[0].y],
|
|
||||||
fnName: 'line',
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
const _modifiedAst = newSketchLn.modifiedAst
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
|
||||||
const lineCallExp = getNodeFromPath<CallExpression>(
|
|
||||||
kclManager.ast,
|
|
||||||
newSketchLn.pathToNode
|
|
||||||
).node
|
|
||||||
if (segmentId)
|
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
|
||||||
commandType: 'extend_path',
|
|
||||||
parentId: sketchEnginePathId,
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_modifiedAst = addCloseToPipe({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'edit_mode_exit' },
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
|
||||||
})
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true)
|
|
||||||
// updateAst(_modifiedAst, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'sketch exit execute': () => {
|
|
||||||
kclManager.executeAst()
|
|
||||||
},
|
|
||||||
'set tool': () => {}, // TODO
|
|
||||||
'toast extrude failed': () => {
|
|
||||||
toast.error(
|
|
||||||
'Extrude failed, sketches need to be closed, or not already extruded'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
|
||||||
const setSelections = event.data
|
|
||||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
|
||||||
return { selectionRanges: setSelections.selection }
|
|
||||||
else if (setSelections.selectionType === 'otherSelection')
|
|
||||||
return {
|
|
||||||
selectionRanges: {
|
|
||||||
...selectionRanges,
|
|
||||||
otherSelections: [setSelections.selection],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
else if (!editorView) return {}
|
|
||||||
else if (setSelections.selectionType === 'singleCodeCursor') {
|
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
|
||||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
|
||||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
|
||||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
|
||||||
// because we want to respect the user manually placing the cursor too.
|
|
||||||
|
|
||||||
// for more details on how selections see `src/lib/selections.ts`.
|
|
||||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
||||||
handleSelectionWithShift({
|
|
||||||
codeSelection: setSelections.selection,
|
|
||||||
currestSelections: selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
|
||||||
if (codeMirrorSelection) {
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { selectionRangeTypeMap }
|
|
||||||
}
|
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
|
||||||
// same as comment above
|
|
||||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
||||||
handleSelectionBatch({
|
|
||||||
selections: setSelections.selection,
|
|
||||||
})
|
|
||||||
if (codeMirrorSelection) {
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { selectionRangeTypeMap }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Selection contains axis': () => true,
|
|
||||||
'Selection contains edge': () => true,
|
|
||||||
'Selection contains face': () => true,
|
|
||||||
'Selection contains line': () => true,
|
|
||||||
'Selection contains point': () => true,
|
|
||||||
'Selection is not empty': () => true,
|
|
||||||
'Selection is one face': ({ selectionRanges }) => {
|
|
||||||
return !!isCursorInSketchCommandRange(
|
|
||||||
engineCommandManager.artifactMap,
|
|
||||||
selectionRanges
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
'Get horizontal info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setHorzDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get vertical info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setVertDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintAngleBetween({
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get length info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
|
||||||
{ selectionRanges }
|
|
||||||
)
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get perpendicular distance info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devTools: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
|
||||||
if (modelingState.nextEvents.includes('Select default plane')) {
|
|
||||||
modelingSend({
|
|
||||||
type: 'Select default plane',
|
|
||||||
data: { planeId: plane_id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [modelingSend, modelingState.nextEvents])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
kclManager.registerExecuteCallback(() => {
|
|
||||||
modelingSend({ type: 'Re-execute' })
|
|
||||||
})
|
|
||||||
}, [modelingSend])
|
|
||||||
|
|
||||||
// useStateMachineCommands({
|
|
||||||
// state: settingsState,
|
|
||||||
// send: settingsSend,
|
|
||||||
// commands,
|
|
||||||
// owner: 'settings',
|
|
||||||
// commandBarMeta: settingsCommandBarMeta,
|
|
||||||
// })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModelingMachineContext.Provider
|
|
||||||
value={{
|
|
||||||
state: modelingState,
|
|
||||||
context: modelingState.context,
|
|
||||||
send: modelingSend,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
|
|
||||||
since realistically it won't ever have generic children that isn't app.tsx */}
|
|
||||||
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ModelingMachineContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelingMachineProvider
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormEvent, useEffect, useState } from 'react'
|
import { FormEvent, useState } from 'react'
|
||||||
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
|
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faX,
|
faX,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
|
import { FILE_EXT } from '../lib/tauriFS'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
@ -28,8 +28,6 @@ function ProjectCard({
|
|||||||
useHotkeys('esc', () => setIsEditing(false))
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
|
||||||
|
|
||||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -44,17 +42,6 @@ function ProjectCard({
|
|||||||
: date.toLocaleTimeString()
|
: date.toLocaleTimeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getNumberOfParts() {
|
|
||||||
const { kclFileCount, kclDirCount } = getPartsCount(
|
|
||||||
await readProject(project.path)
|
|
||||||
)
|
|
||||||
setNumberOfParts(kclFileCount)
|
|
||||||
setNumberOfFolders(kclDirCount)
|
|
||||||
}
|
|
||||||
getNumberOfParts()
|
|
||||||
}, [project.path])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
{...props}
|
{...props}
|
||||||
@ -89,7 +76,7 @@ function ProjectCard({
|
|||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="p-1 flex flex-col h-full gap-2">
|
<div className="p-1 flex flex-col gap-2">
|
||||||
<Link
|
<Link
|
||||||
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
||||||
className="flex-1 text-liquid-100"
|
className="flex-1 text-liquid-100"
|
||||||
@ -97,14 +84,7 @@ function ProjectCard({
|
|||||||
{project.name?.replace(FILE_EXT, '')}
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)}
|
||||||
{numberOfFolders > 0 &&
|
|
||||||
`/ ${numberOfFolders} folder${
|
|
||||||
numberOfFolders === 1 ? '' : 's'
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
<span className="text-chalkboard-60 text-xs">
|
|
||||||
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
|
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
|
||||||
import CommandBarProvider from './CommandBar'
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -15,7 +13,7 @@ const projectWellFormed = {
|
|||||||
path: '/some/path/Simple Box/main.kcl',
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
entrypointMetadata: {
|
entrypoint_metadata: {
|
||||||
accessedAt: now,
|
accessedAt: now,
|
||||||
blksize: 32,
|
blksize: 32,
|
||||||
blocks: 32,
|
blocks: 32,
|
||||||
@ -40,11 +38,7 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
<GlobalStateProvider>
|
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,11 +55,7 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders app name if given no project', () => {
|
test('Renders app name if given no project', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<ProjectSidebarMenu />
|
||||||
<GlobalStateProvider>
|
|
||||||
<ProjectSidebarMenu />
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,14 +69,7 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders as a link if set to do so', () => {
|
test('Renders as a link if set to do so', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
||||||
<GlobalStateProvider>
|
|
||||||
<ProjectSidebarMenu
|
|
||||||
project={projectWellFormed}
|
|
||||||
renderAsLink={true}
|
|
||||||
/>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,18 @@
|
|||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { IndexLoaderData, paths } from '../Router'
|
import { ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ExportButton } from './ExportButton'
|
import { ExportButton } from './ExportButton'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { FileTree } from './FileTree'
|
|
||||||
import { sep } from '@tauri-apps/api/path'
|
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
file,
|
|
||||||
renderAsLink = false,
|
renderAsLink = false,
|
||||||
}: {
|
}: {
|
||||||
renderAsLink?: boolean
|
renderAsLink?: boolean
|
||||||
project?: IndexLoaderData['project']
|
project?: Partial<ProjectWithEntryPointMetadata>
|
||||||
file?: IndexLoaderData['file']
|
|
||||||
}) => {
|
}) => {
|
||||||
return renderAsLink ? (
|
return renderAsLink ? (
|
||||||
<Link
|
<Link
|
||||||
@ -27,10 +23,10 @@ const ProjectSidebarMenu = ({
|
|||||||
<img
|
<img
|
||||||
src="/kitt-8bit-winking.svg"
|
src="/kitt-8bit-winking.svg"
|
||||||
alt="KittyCAD App"
|
alt="KittyCAD App"
|
||||||
className="w-auto h-9"
|
className="h-9 w-auto"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
|
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block"
|
||||||
data-testid="project-sidebar-link-name"
|
data-testid="project-sidebar-link-name"
|
||||||
>
|
>
|
||||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||||
@ -45,20 +41,11 @@ const ProjectSidebarMenu = ({
|
|||||||
<img
|
<img
|
||||||
src="/kitt-8bit-winking.svg"
|
src="/kitt-8bit-winking.svg"
|
||||||
alt="KittyCAD App"
|
alt="KittyCAD App"
|
||||||
className="w-auto h-full"
|
className="h-full w-auto"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-start py-0.5">
|
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block">
|
||||||
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||||
{isTauri() && file?.name
|
</span>
|
||||||
? file.name.slice(file.name.lastIndexOf(sep) + 1)
|
|
||||||
: 'KittyCAD Modeling App'}
|
|
||||||
</span>
|
|
||||||
{isTauri() && project?.name && (
|
|
||||||
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
|
|
||||||
{project.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition
|
<Transition
|
||||||
enter="duration-200 ease-out"
|
enter="duration-200 ease-out"
|
||||||
@ -69,7 +56,7 @@ const ProjectSidebarMenu = ({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
|
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -81,74 +68,54 @@ const ProjectSidebarMenu = ({
|
|||||||
leaveTo="opacity-0 -translate-x-4"
|
leaveTo="opacity-0 -translate-x-4"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Panel
|
<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">
|
||||||
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-lg shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-energy-100 dark:border-energy-100/50"
|
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
||||||
style={{ gridTemplateRows: 'auto 1fr auto' }}
|
<img
|
||||||
>
|
src="/kitt-8bit-winking.svg"
|
||||||
{({ close }) => (
|
alt="KittyCAD App"
|
||||||
<>
|
className="h-9 w-auto"
|
||||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-10/25 dark:bg-energy-110">
|
/>
|
||||||
<img
|
|
||||||
src="/kitt-8bit-winking.svg"
|
|
||||||
alt="KittyCAD App"
|
|
||||||
className="w-auto h-9"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="m-0 text-chalkboard-100 dark:text-energy-10 text-mono"
|
className="m-0 text-energy-10 text-mono"
|
||||||
data-testid="projectName"
|
data-testid="projectName"
|
||||||
>
|
>
|
||||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||||
</p>
|
</p>
|
||||||
{project?.entrypointMetadata && (
|
{project?.entrypoint_metadata && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-xs text-chalkboard-100 dark:text-energy-40"
|
className="m-0 text-energy-40 text-xs"
|
||||||
data-testid="createdAt"
|
data-testid="createdAt"
|
||||||
>
|
|
||||||
Created{' '}
|
|
||||||
{project.entrypointMetadata.createdAt.toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isTauri() ? (
|
|
||||||
<FileTree
|
|
||||||
file={file}
|
|
||||||
className="overflow-hidden border-0 border-y border-energy-40 dark:border-energy-70"
|
|
||||||
closePanel={close}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 overflow-hidden" />
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-2 p-4 bg-energy-10/25 dark:bg-energy-110">
|
|
||||||
<ExportButton
|
|
||||||
className={{
|
|
||||||
button:
|
|
||||||
'border-transparent dark:border-transparent hover:border-energy-60',
|
|
||||||
icon: 'text-energy-10 dark:text-energy-120',
|
|
||||||
bg: 'bg-energy-120 dark:bg-energy-10',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Export Model
|
Created{' '}
|
||||||
</ExportButton>
|
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
||||||
{isTauri() && (
|
</p>
|
||||||
<ActionButton
|
)}
|
||||||
Element="link"
|
</div>
|
||||||
to={paths.HOME}
|
</div>
|
||||||
icon={{
|
<div className="p-4 flex flex-col gap-2">
|
||||||
icon: faHome,
|
<ExportButton
|
||||||
iconClassName: 'text-energy-10 dark:text-energy-120',
|
className={{
|
||||||
bgClassName: 'bg-energy-120 dark:bg-energy-10',
|
button:
|
||||||
}}
|
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
||||||
className="border-transparent dark:border-transparent hover:border-energy-60"
|
}}
|
||||||
>
|
>
|
||||||
Go to Home
|
Export Model
|
||||||
</ActionButton>
|
</ExportButton>
|
||||||
)}
|
{isTauri() && (
|
||||||
</div>
|
<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>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { type InstanceProps, create } from 'react-modal-promise'
|
import { Value } from '../lang/abstractSyntaxTreeTypes'
|
||||||
import { Value } from '../lang/wasm'
|
|
||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
@ -10,28 +9,6 @@ import {
|
|||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
|
||||||
type ModalResolve = {
|
|
||||||
value: string
|
|
||||||
sign: number
|
|
||||||
valueNode: Value
|
|
||||||
variableName?: string
|
|
||||||
newVariableInsertIndex: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModalReject = boolean
|
|
||||||
|
|
||||||
type SetAngleLengthModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
|
||||||
value: number
|
|
||||||
valueName: string
|
|
||||||
shouldCreateVariable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSetAngleLengthModal = create<
|
|
||||||
SetAngleLengthModalProps,
|
|
||||||
ModalResolve,
|
|
||||||
ModalReject
|
|
||||||
>
|
|
||||||
|
|
||||||
export const SetAngleLengthModal = ({
|
export const SetAngleLengthModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onResolve,
|
onResolve,
|
||||||
@ -39,7 +16,20 @@ export const SetAngleLengthModal = ({
|
|||||||
value: initialValue,
|
value: initialValue,
|
||||||
valueName,
|
valueName,
|
||||||
shouldCreateVariable: initialShouldCreateVariable = false,
|
shouldCreateVariable: initialShouldCreateVariable = false,
|
||||||
}: SetAngleLengthModalProps) => {
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
onResolve: (a: {
|
||||||
|
value: string
|
||||||
|
sign: number
|
||||||
|
valueNode: Value
|
||||||
|
variableName?: string
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
}) => void
|
||||||
|
onReject: (a: any) => void
|
||||||
|
value: number
|
||||||
|
valueName: string
|
||||||
|
shouldCreateVariable: boolean
|
||||||
|
}) => {
|
||||||
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
||||||
const [value, setValue] = useState(String(initialValue * sign))
|
const [value, setValue] = useState(String(initialValue * sign))
|
||||||
const [shouldCreateVariable, setShouldCreateVariable] = useState(
|
const [shouldCreateVariable, setShouldCreateVariable] = useState(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { type InstanceProps, create } from 'react-modal-promise'
|
import { Value } from '../lang/abstractSyntaxTreeTypes'
|
||||||
import { Value } from '../lang/wasm'
|
|
||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
@ -10,30 +9,6 @@ import {
|
|||||||
CreateNewVariable,
|
CreateNewVariable,
|
||||||
} from './AvailableVarsHelpers'
|
} from './AvailableVarsHelpers'
|
||||||
|
|
||||||
type ModalResolve = {
|
|
||||||
value: string
|
|
||||||
segName: string
|
|
||||||
valueNode: Value
|
|
||||||
variableName?: string
|
|
||||||
newVariableInsertIndex: number
|
|
||||||
sign: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModalReject = boolean
|
|
||||||
|
|
||||||
type GetInfoModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
|
||||||
segName: string
|
|
||||||
isSegNameEditable: boolean
|
|
||||||
value?: number
|
|
||||||
initialVariableName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createInfoModal = create<
|
|
||||||
GetInfoModalProps,
|
|
||||||
ModalResolve,
|
|
||||||
ModalReject
|
|
||||||
>
|
|
||||||
|
|
||||||
export const GetInfoModal = ({
|
export const GetInfoModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onResolve,
|
onResolve,
|
||||||
@ -42,12 +17,25 @@ export const GetInfoModal = ({
|
|||||||
isSegNameEditable,
|
isSegNameEditable,
|
||||||
value: initialValue,
|
value: initialValue,
|
||||||
initialVariableName,
|
initialVariableName,
|
||||||
}: GetInfoModalProps) => {
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
onResolve: (a: {
|
||||||
|
value: string
|
||||||
|
segName: string
|
||||||
|
valueNode: Value
|
||||||
|
variableName?: string
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
sign: number
|
||||||
|
}) => void
|
||||||
|
onReject: (a: any) => void
|
||||||
|
segName: string
|
||||||
|
isSegNameEditable: boolean
|
||||||
|
value: number
|
||||||
|
initialVariableName: string
|
||||||
|
}) => {
|
||||||
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
|
||||||
const [segName, setSegName] = useState(initialSegName)
|
const [segName, setSegName] = useState(initialSegName)
|
||||||
const [value, setValue] = useState(
|
const [value, setValue] = useState(String(Math.abs(initialValue)))
|
||||||
initialValue === undefined ? '' : String(Math.abs(initialValue))
|
|
||||||
)
|
|
||||||
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
|
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -4,26 +4,19 @@ import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { type InstanceProps, create } from 'react-modal-promise'
|
|
||||||
|
|
||||||
type ModalResolve = { variableName: string }
|
|
||||||
type ModalReject = boolean
|
|
||||||
type SetVarNameModalProps = InstanceProps<ModalResolve, ModalReject> & {
|
|
||||||
valueName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSetVarNameModal = create<
|
|
||||||
SetVarNameModalProps,
|
|
||||||
ModalResolve,
|
|
||||||
ModalReject
|
|
||||||
>
|
|
||||||
|
|
||||||
export const SetVarNameModal = ({
|
export const SetVarNameModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onResolve,
|
onResolve,
|
||||||
onReject,
|
onReject,
|
||||||
valueName,
|
valueName,
|
||||||
}: SetVarNameModalProps) => {
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
onResolve: (a: { variableName?: string }) => void
|
||||||
|
onReject: (a: any) => void
|
||||||
|
value: number
|
||||||
|
valueName: string
|
||||||
|
}) => {
|
||||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||||
useCalc({ value: '', initialVariableName: valueName })
|
useCalc({ value: '', initialVariableName: valueName })
|
||||||
|
|
||||||
|
|||||||
@ -7,18 +7,24 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { getNormalisedCoordinates } from '../lib/utils'
|
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { addStartSketch } from 'lang/modifyAst'
|
||||||
|
import {
|
||||||
|
addCloseToPipe,
|
||||||
|
addNewSketchLn,
|
||||||
|
compareVec2Epsilon,
|
||||||
|
} from 'lang/std/sketch'
|
||||||
import { getNodeFromPath } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
import { VariableDeclarator, recast, parse, CallExpression } from 'lang/wasm'
|
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { modify_ast_for_sketch } from '../wasm-lib/pkg/wasm_lib'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { KCLError } from 'lang/errors'
|
||||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
import { changeSketchArguments } from 'lang/std/sketch'
|
import { rangeTypeFix } from 'lang/abstractSyntaxTree'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }) => {
|
export const Stream = ({ className = '' }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -26,21 +32,37 @@ export const Stream = ({ className = '' }) => {
|
|||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const {
|
const {
|
||||||
mediaStream,
|
mediaStream,
|
||||||
|
engineCommandManager,
|
||||||
setButtonDownInStream,
|
setButtonDownInStream,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
isExecuting,
|
||||||
|
guiMode,
|
||||||
|
ast,
|
||||||
|
updateAst,
|
||||||
|
setGuiMode,
|
||||||
|
programMemory,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
mediaStream: s.mediaStream,
|
mediaStream: s.mediaStream,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
setButtonDownInStream: s.setButtonDownInStream,
|
setButtonDownInStream: s.setButtonDownInStream,
|
||||||
|
fileId: s.fileId,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
|
isExecuting: s.isExecuting,
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
ast: s.ast,
|
||||||
|
updateAst: s.updateAst,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
programMemory: s.programMemory,
|
||||||
}))
|
}))
|
||||||
const { settings } = useGlobalStateContext()
|
const {
|
||||||
const cameraControls = settings?.context?.cameraControls
|
settings: {
|
||||||
const { send, state, context } = useModelingContext()
|
context: { cameraControls },
|
||||||
const { isExecuting } = useKclContext()
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -51,7 +73,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (!mediaStream) return
|
if (!mediaStream) return
|
||||||
videoRef.current.srcObject = mediaStream
|
videoRef.current.srcObject = mediaStream
|
||||||
}, [mediaStream])
|
}, [mediaStream, engineCommandManager])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
@ -84,14 +106,8 @@ export const Stream = ({ className = '' }) => {
|
|||||||
interaction = 'zoom'
|
interaction = 'zoom'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.matches('Sketch.Move Tool')) {
|
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||||
if (
|
engineCommandManager?.sendSceneCommand({
|
||||||
state.matches('Sketch.Move Tool.No move') ||
|
|
||||||
state.matches('Sketch.Move Tool.Move with execute')
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'handle_mouse_drag_start',
|
type: 'handle_mouse_drag_start',
|
||||||
@ -99,8 +115,13 @@ export const Stream = ({ className = '' }) => {
|
|||||||
},
|
},
|
||||||
cmd_id: newId,
|
cmd_id: newId,
|
||||||
})
|
})
|
||||||
} else if (!state.matches('Sketch.Line Tool')) {
|
} else if (
|
||||||
engineCommandManager.sendSceneCommand({
|
!(
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_start',
|
type: 'camera_drag_start',
|
||||||
@ -118,7 +139,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||||
|
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'default_camera_zoom',
|
type: 'default_camera_zoom',
|
||||||
@ -155,208 +176,215 @@ export const Stream = ({ className = '' }) => {
|
|||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!didDragInStream && state.matches('Sketch no face')) {
|
if (!didDragInStream) {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'select_with_point',
|
||||||
|
selection_type: 'add',
|
||||||
|
selected_at_window: { x, y },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didDragInStream && guiMode.mode === 'default') {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
type: 'select_with_point',
|
type: 'select_with_point',
|
||||||
selection_type: 'add',
|
selection_type: 'add',
|
||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
}
|
}
|
||||||
engineCommandManager.sendSceneCommand(command)
|
} else if (
|
||||||
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
|
(!didDragInStream &&
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||||
|
(guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any))
|
||||||
|
) {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
type: 'mouse_click',
|
type: 'mouse_click',
|
||||||
window: { x, y },
|
window: { x, y },
|
||||||
}
|
}
|
||||||
engineCommandManager.sendSceneCommand(command).then(async (resp) => {
|
|
||||||
const entities_modified = resp?.data?.data?.entities_modified
|
|
||||||
if (!entities_modified) return
|
|
||||||
if (state.matches('Sketch.Line Tool.No Points')) {
|
|
||||||
send('Add point')
|
|
||||||
} else if (state.matches('Sketch.Line Tool.Point Added')) {
|
|
||||||
const curve = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: entities_modified[0],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const coords: { x: number; y: number }[] =
|
|
||||||
curve.data.data.control_points
|
|
||||||
// We need the normal for the plane we are on.
|
|
||||||
const plane = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'get_sketch_mode_plane',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const z_axis = plane.data.data.z_axis
|
|
||||||
|
|
||||||
// Get the current axis.
|
|
||||||
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
|
|
||||||
null
|
|
||||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
|
||||||
if (z_axis.z === -1) {
|
|
||||||
currentAxis = '-xy'
|
|
||||||
} else {
|
|
||||||
currentAxis = 'xy'
|
|
||||||
}
|
|
||||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
|
||||||
if (z_axis.x === -1) {
|
|
||||||
currentAxis = '-yz'
|
|
||||||
} else {
|
|
||||||
currentAxis = 'yz'
|
|
||||||
}
|
|
||||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
|
||||||
if (z_axis.y === -1) {
|
|
||||||
currentAxis = '-xz'
|
|
||||||
} else {
|
|
||||||
currentAxis = 'xz'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
send({
|
|
||||||
type: 'Add point',
|
|
||||||
data: {
|
|
||||||
coords,
|
|
||||||
axis: currentAxis,
|
|
||||||
segmentId: entities_modified[0],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
|
|
||||||
const curve = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: entities_modified[0],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const coords: { x: number; y: number }[] =
|
|
||||||
curve.data.data.control_points
|
|
||||||
send({
|
|
||||||
type: 'Add point',
|
|
||||||
data: { coords, axis: null, segmentId: entities_modified[0] },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (
|
} else if (
|
||||||
!didDragInStream &&
|
guiMode.mode === 'sketch' &&
|
||||||
(state.matches('Sketch.SketchIdle') ||
|
guiMode.sketchMode === ('move' as any)
|
||||||
state.matches('idle') ||
|
|
||||||
state.matches('awaiting selection'))
|
|
||||||
) {
|
) {
|
||||||
command.cmd = {
|
|
||||||
type: 'select_with_point',
|
|
||||||
selected_at_window: { x, y },
|
|
||||||
selection_type: 'add',
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand(command)
|
|
||||||
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
|
||||||
command.cmd = {
|
|
||||||
type: 'select_with_point',
|
|
||||||
selected_at_window: { x, y },
|
|
||||||
selection_type: 'add',
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand(command)
|
|
||||||
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
|
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
type: 'handle_mouse_drag_end',
|
type: 'handle_mouse_drag_end',
|
||||||
window: { x, y },
|
window: { x, y },
|
||||||
}
|
}
|
||||||
engineCommandManager.sendSceneCommand(command).then(async () => {
|
}
|
||||||
if (!context.sketchPathToNode) return
|
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
|
||||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
if (!(guiMode.mode === 'sketch')) return
|
||||||
kclManager.ast,
|
|
||||||
context.sketchPathToNode,
|
if (guiMode.sketchMode === 'selectFace') return
|
||||||
'VariableDeclarator'
|
|
||||||
).node
|
// Check if the sketch group already exists.
|
||||||
// Get the current plane string for plane we are on.
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||||
let currentPlaneString = ''
|
ast,
|
||||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
guiMode.pathToNode,
|
||||||
currentPlaneString = 'XY'
|
'VariableDeclarator'
|
||||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
).node
|
||||||
currentPlaneString = 'YZ'
|
const variableName = varDec?.id?.name
|
||||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
const sketchGroup = programMemory.root[variableName]
|
||||||
currentPlaneString = 'XZ'
|
const isEditingExistingSketch =
|
||||||
|
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
|
||||||
|
let sketchGroupId = ''
|
||||||
|
if (sketchGroup && sketchGroup.type === 'SketchGroup') {
|
||||||
|
sketchGroupId = sketchGroup.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
guiMode.sketchMode === ('move' as any as 'line') &&
|
||||||
|
command.cmd.type === 'handle_mouse_drag_end'
|
||||||
|
) {
|
||||||
|
// Let's get the updated ast.
|
||||||
|
if (sketchGroupId === '') return
|
||||||
|
|
||||||
|
console.log('guiMode.pathId', guiMode.pathId)
|
||||||
|
|
||||||
|
// We have a problem if we do not have an id for the sketch group.
|
||||||
|
if (
|
||||||
|
guiMode.pathId === undefined ||
|
||||||
|
guiMode.pathId === null ||
|
||||||
|
guiMode.pathId === ''
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
let engineId = guiMode.pathId
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedAst: Program = await modify_ast_for_sketch(
|
||||||
|
engineCommandManager,
|
||||||
|
JSON.stringify(ast),
|
||||||
|
variableName,
|
||||||
|
engineId
|
||||||
|
)
|
||||||
|
|
||||||
|
updateAst(updatedAst, false)
|
||||||
|
} catch (e: any) {
|
||||||
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
|
const kclError = new KCLError(
|
||||||
|
parsed.kind,
|
||||||
|
parsed.msg,
|
||||||
|
rangeTypeFix(parsed.sourceRanges)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(kclError)
|
||||||
|
throw kclError
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do not supporting editing/moving lines on a non-default plane.
|
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
||||||
// Eventually we can support this but for now we will just throw an
|
|
||||||
// error.
|
|
||||||
if (currentPlaneString === '') return
|
|
||||||
|
|
||||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
if (!(guiMode.sketchMode === ('sketch_line' as any as 'line'))) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
resp?.data?.data?.entities_modified?.length &&
|
||||||
|
guiMode.waitingFirstClick &&
|
||||||
|
!isEditingExistingSketch
|
||||||
|
) {
|
||||||
|
const curve = await engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'path_get_info',
|
type: 'curve_get_control_points',
|
||||||
path_id: context.sketchEnginePathId,
|
curve_id: resp?.data?.data?.entities_modified[0],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const segmentsWithMappings = (
|
const coords: { x: number; y: number }[] =
|
||||||
pathInfo?.data?.data?.segments as { command_id: string }[]
|
curve.data.data.control_points
|
||||||
)
|
const _addStartSketch = addStartSketch(
|
||||||
.filter(({ command_id }) => {
|
ast,
|
||||||
return command_id && engineCommandManager.artifactMap[command_id]
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||||
})
|
[
|
||||||
.map(({ command_id }) => command_id)
|
roundOff(coords[1].x - coords[0].x),
|
||||||
const segment2dInfo = await Promise.all(
|
roundOff(coords[1].y - coords[0].y),
|
||||||
segmentsWithMappings.map(async (segmentId) => {
|
|
||||||
const response = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: segmentId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const controlPoints: [
|
|
||||||
{ x: number; y: number },
|
|
||||||
{ x: number; y: number }
|
|
||||||
] = response.data.data.control_points
|
|
||||||
return {
|
|
||||||
controlPoints,
|
|
||||||
segmentId,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
let modifiedAst = { ...kclManager.ast }
|
|
||||||
let code = kclManager.code
|
|
||||||
for (const controlPoint of segment2dInfo) {
|
|
||||||
const range =
|
|
||||||
engineCommandManager.artifactMap[controlPoint.segmentId].range
|
|
||||||
if (!range) continue
|
|
||||||
const from = controlPoint.controlPoints[0]
|
|
||||||
const to = controlPoint.controlPoints[1]
|
|
||||||
const modded = changeSketchArguments(
|
|
||||||
modifiedAst,
|
|
||||||
kclManager.programMemory,
|
|
||||||
range,
|
|
||||||
[to.x, to.y],
|
|
||||||
[from.x, from.y]
|
|
||||||
)
|
|
||||||
modifiedAst = modded.modifiedAst
|
|
||||||
|
|
||||||
// update artifact map ranges now that we have updated the ast.
|
|
||||||
code = recast(modded.modifiedAst)
|
|
||||||
const astWithCurrentRanges = parse(code)
|
|
||||||
const updateNode = getNodeFromPath<CallExpression>(
|
|
||||||
astWithCurrentRanges,
|
|
||||||
modded.pathToNode
|
|
||||||
).node
|
|
||||||
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
|
|
||||||
updateNode.start,
|
|
||||||
updateNode.end,
|
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
const _modifiedAst = _addStartSketch.modifiedAst
|
||||||
|
const _pathToNode = _addStartSketch.pathToNode
|
||||||
|
|
||||||
|
// We need to update the guiMode with the right pathId so that we can
|
||||||
|
// move lines later and send the right sketch id to the engine.
|
||||||
|
for (const [id, artifact] of Object.entries(
|
||||||
|
engineCommandManager.artifactMap
|
||||||
|
)) {
|
||||||
|
if (artifact.commandType === 'start_path') {
|
||||||
|
guiMode.pathId = id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kclManager.executeAstMock(modifiedAst, true)
|
setGuiMode({
|
||||||
})
|
...guiMode,
|
||||||
}
|
pathToNode: _pathToNode,
|
||||||
|
waitingFirstClick: false,
|
||||||
|
})
|
||||||
|
updateAst(_modifiedAst, false)
|
||||||
|
} else if (
|
||||||
|
resp?.data?.data?.entities_modified?.length &&
|
||||||
|
(!guiMode.waitingFirstClick || isEditingExistingSketch)
|
||||||
|
) {
|
||||||
|
const curve = await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: resp?.data?.data?.entities_modified[0],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const coords: { x: number; y: number }[] =
|
||||||
|
curve.data.data.control_points
|
||||||
|
|
||||||
|
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
guiMode.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
const variableName = varDec.id.name
|
||||||
|
const sketchGroup = programMemory.root[variableName]
|
||||||
|
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||||
|
const initialCoords = sketchGroup.value[0].from
|
||||||
|
|
||||||
|
const isClose = compareVec2Epsilon(initialCoords, [
|
||||||
|
coords[1].x,
|
||||||
|
coords[1].y,
|
||||||
|
])
|
||||||
|
|
||||||
|
let _modifiedAst: Program
|
||||||
|
if (!isClose) {
|
||||||
|
_modifiedAst = addNewSketchLn({
|
||||||
|
node: ast,
|
||||||
|
programMemory,
|
||||||
|
to: [coords[1].x, coords[1].y],
|
||||||
|
fnName: 'line',
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
}).modifiedAst
|
||||||
|
updateAst(_modifiedAst, false)
|
||||||
|
} else {
|
||||||
|
_modifiedAst = addCloseToPipe({
|
||||||
|
node: ast,
|
||||||
|
programMemory,
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'default',
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
updateAst(_modifiedAst, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
setDidDragInStream(false)
|
setDidDragInStream(false)
|
||||||
setClickCoords(undefined)
|
setClickCoords(undefined)
|
||||||
}
|
}
|
||||||
@ -387,8 +415,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
onWheel={handleScroll}
|
onWheel={handleScroll}
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
onMouseMoveCapture={handleMouseMove}
|
onMouseMoveCapture={handleMouseMove}
|
||||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||||
disablePictureInPicture
|
|
||||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@ -13,26 +13,23 @@ import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
|||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
import { useStore } from 'useStore'
|
import { Selections, useStore } from 'useStore'
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
import { LanguageServerClient } from 'editor/lsp'
|
||||||
import kclLanguage from 'editor/lsp/language'
|
import kclLanguage from 'editor/lsp/language'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||||
|
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
EditorView,
|
EditorView,
|
||||||
addLineHighlight,
|
addLineHighlight,
|
||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
} from 'editor/highlightextension'
|
} from 'editor/highlightextension'
|
||||||
import { roundOff } from 'lib/utils'
|
import { isOverlap, roundOff } from 'lib/utils'
|
||||||
import { kclErrToDiagnostic } from 'lang/errors'
|
import { kclErrToDiagnostic } from 'lang/errors'
|
||||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import interact from '@replit/codemirror-interact'
|
import interact from '@replit/codemirror-interact'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -51,22 +48,37 @@ export const TextEditor = ({
|
|||||||
theme: Themes.Light | Themes.Dark
|
theme: Themes.Light | Themes.Dark
|
||||||
}) => {
|
}) => {
|
||||||
const pathParams = useParams()
|
const pathParams = useParams()
|
||||||
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
|
const {
|
||||||
useStore((s) => ({
|
code,
|
||||||
editorView: s.editorView,
|
deferredSetCode,
|
||||||
isLSPServerReady: s.isLSPServerReady,
|
editorView,
|
||||||
setEditorView: s.setEditorView,
|
engineCommandManager,
|
||||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
formatCode,
|
||||||
}))
|
isLSPServerReady,
|
||||||
const { code, errors } = useKclContext()
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
setEditorView,
|
||||||
|
setIsLSPServerReady,
|
||||||
|
setSelectionRanges,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
code: s.code,
|
||||||
|
deferredSetCode: s.deferredSetCode,
|
||||||
|
editorView: s.editorView,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
isLSPServerReady: s.isLSPServerReady,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||||
|
setEditorView: s.setEditorView,
|
||||||
|
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||||
|
setSelectionRanges: s.setSelectionRanges,
|
||||||
|
}))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
context: { selectionRanges, selectionRangeTypeMap },
|
settings: {
|
||||||
send,
|
context: { textWrapping },
|
||||||
} = useModelingContext()
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
|
||||||
useGlobalStateContext()
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
@ -111,16 +123,18 @@ export const TextEditor = ({
|
|||||||
}, [lspClient, isLSPServerReady])
|
}, [lspClient, isLSPServerReady])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (newCode: string) => {
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
kclManager.setCodeAndExecute(newCode)
|
deferredSetCode(value)
|
||||||
if (isTauri() && pathParams.id) {
|
if (isTauri() && pathParams.id) {
|
||||||
// Save the file to disk
|
// Save the file to disk
|
||||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
writeTextFile(pathParams.id, newCode).catch((err) => {
|
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
(err) => {
|
||||||
console.error('error saving file', err)
|
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||||
toast.error('Error saving file, please check file permissions')
|
console.error('error saving file', err)
|
||||||
})
|
toast.error('Error saving file, please check file permissions')
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (editorView) {
|
if (editorView) {
|
||||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||||
@ -130,17 +144,57 @@ export const TextEditor = ({
|
|||||||
if (!editorView) {
|
if (!editorView) {
|
||||||
setEditorView(viewUpdate.view)
|
setEditorView(viewUpdate.view)
|
||||||
}
|
}
|
||||||
const eventInfo = processCodeMirrorRanges({
|
const ranges = viewUpdate.state.selection.ranges
|
||||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
|
||||||
selectionRanges,
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
})
|
|
||||||
if (!eventInfo) return
|
|
||||||
|
|
||||||
send(eventInfo.modelingEvent)
|
const isChange =
|
||||||
eventInfo.engineEvents.forEach((event) =>
|
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||||
engineCommandManager.sendSceneCommand(event)
|
ranges.some(({ from, to }, i) => {
|
||||||
|
return (
|
||||||
|
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||||
|
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isChange) return
|
||||||
|
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||||
|
({ from, to }) => {
|
||||||
|
if (selectionRangeTypeMap[to]) {
|
||||||
|
return {
|
||||||
|
type: selectionRangeTypeMap[to],
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
const idBasedSelections = codeBasedSelections
|
||||||
|
.map(({ type, range }) => {
|
||||||
|
const hasOverlap = Object.entries(
|
||||||
|
engineCommandManager?.sourceRangeMap || {}
|
||||||
|
).filter(([_, sourceRange]) => {
|
||||||
|
return isOverlap(sourceRange, range)
|
||||||
|
})
|
||||||
|
if (hasOverlap.length) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id: hasOverlap[0][0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as any
|
||||||
|
|
||||||
|
engineCommandManager?.cusorsSelected({
|
||||||
|
otherSelections: [],
|
||||||
|
idBasedSelections,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSelectionRanges({
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
@ -157,7 +211,7 @@ export const TextEditor = ({
|
|||||||
{
|
{
|
||||||
key: editorShortcutMeta.formatCode.codeMirror,
|
key: editorShortcutMeta.formatCode.codeMirror,
|
||||||
run: () => {
|
run: () => {
|
||||||
kclManager.format()
|
formatCode()
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -181,7 +235,7 @@ export const TextEditor = ({
|
|||||||
extensions.push(
|
extensions.push(
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
linter((_view) => {
|
linter((_view) => {
|
||||||
return kclErrToDiagnostic(errors)
|
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||||
}),
|
}),
|
||||||
interact({
|
interact({
|
||||||
rules: [
|
rules: [
|
||||||
@ -220,7 +274,7 @@ export const TextEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}, [kclLSP, textWrapping, convertCallback])
|
}, [kclLSP, textWrapping])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,79 +1,105 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
PathToNodeMap,
|
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import { sketchButtonClassnames } from 'Toolbar'
|
||||||
|
|
||||||
export function equalAngleInfo({
|
export const EqualAngle = () => {
|
||||||
selectionRanges,
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}: {
|
useStore((s) => ({
|
||||||
selectionRanges: Selections
|
guiMode: s.guiMode,
|
||||||
}) {
|
ast: s.ast,
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
updateAst: s.updateAst,
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
selectionRanges: s.selectionRanges,
|
||||||
)
|
programMemory: s.programMemory,
|
||||||
const nodes = paths.map(
|
setCursor: s.setCursor,
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
}))
|
||||||
)
|
const [enableEqual, setEnableEqual] = useState(false)
|
||||||
const varDecs = paths.map(
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
(pathToNode) =>
|
useEffect(() => {
|
||||||
getNodeFromPath<VariableDeclarator>(
|
if (!ast) return
|
||||||
kclManager.ast,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
pathToNode,
|
getNodePathFromSourceRange(ast, range)
|
||||||
'VariableDeclarator'
|
)
|
||||||
)?.node
|
const nodes = paths.map(
|
||||||
)
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
const primaryLine = varDecs[0]
|
)
|
||||||
const secondaryVarDecs = varDecs.slice(1)
|
const varDecs = paths.map(
|
||||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
(pathToNode) =>
|
||||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
getNodeFromPath<VariableDeclarator>(
|
||||||
)
|
ast,
|
||||||
const isAllTooltips = nodes.every(
|
pathToNode,
|
||||||
(node) =>
|
'VariableDeclarator'
|
||||||
node?.type === 'CallExpression' &&
|
)?.node
|
||||||
toolTips.includes(node.callee.name as any)
|
)
|
||||||
)
|
const primaryLine = varDecs[0]
|
||||||
|
const secondaryVarDecs = varDecs.slice(1)
|
||||||
|
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||||
|
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
const transforms = getTransformInfos(
|
const theTransforms = getTransformInfos(
|
||||||
{
|
{
|
||||||
...selectionRanges,
|
...selectionRanges,
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
},
|
},
|
||||||
kclManager.ast,
|
ast,
|
||||||
'equalAngle'
|
'equalAngle'
|
||||||
)
|
)
|
||||||
|
setTransformInfos(theTransforms)
|
||||||
|
|
||||||
const enabled =
|
const _enableEqual =
|
||||||
!!secondaryVarDecs.length &&
|
!!secondaryVarDecs.length &&
|
||||||
isAllTooltips &&
|
isAllTooltips &&
|
||||||
isOthersLinkedToPrimary &&
|
isOthersLinkedToPrimary &&
|
||||||
transforms.every(Boolean)
|
theTransforms.every(Boolean)
|
||||||
return { enabled, transforms }
|
setEnableEqual(_enableEqual)
|
||||||
}
|
}, [guiMode, selectionRanges])
|
||||||
|
if (guiMode.mode !== 'sketch') return null
|
||||||
export function applyConstraintEqualAngle({
|
|
||||||
selectionRanges,
|
return (
|
||||||
}: {
|
<button
|
||||||
selectionRanges: Selections
|
onClick={async () => {
|
||||||
}): {
|
if (!(transformInfos && ast)) return
|
||||||
modifiedAst: Program
|
const { modifiedAst, pathToNodeMap } =
|
||||||
pathToNodeMap: PathToNodeMap
|
transformSecondarySketchLinesTagFirst({
|
||||||
} {
|
ast,
|
||||||
const { transforms } = equalAngleInfo({ selectionRanges })
|
selectionRanges,
|
||||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
transformInfos,
|
||||||
ast: kclManager.ast,
|
programMemory,
|
||||||
selectionRanges,
|
})
|
||||||
transformInfos: transforms,
|
updateAst(modifiedAst, true, {
|
||||||
programMemory: kclManager.programMemory,
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
return { modifiedAst, pathToNodeMap }
|
}}
|
||||||
|
disabled={!enableEqual}
|
||||||
|
title="Parallel (or equal angle)"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon="parallel"
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
Parallel
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +1,105 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
PathToNodeMap,
|
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import { sketchButtonClassnames } from 'Toolbar'
|
||||||
|
|
||||||
export function setEqualLengthInfo({
|
export const EqualLength = () => {
|
||||||
selectionRanges,
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}: {
|
useStore((s) => ({
|
||||||
selectionRanges: Selections
|
guiMode: s.guiMode,
|
||||||
}) {
|
ast: s.ast,
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
updateAst: s.updateAst,
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
selectionRanges: s.selectionRanges,
|
||||||
)
|
programMemory: s.programMemory,
|
||||||
const nodes = paths.map(
|
setCursor: s.setCursor,
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
}))
|
||||||
)
|
const [enableEqual, setEnableEqual] = useState(false)
|
||||||
const varDecs = paths.map(
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
(pathToNode) =>
|
useEffect(() => {
|
||||||
getNodeFromPath<VariableDeclarator>(
|
if (!ast) return
|
||||||
kclManager.ast,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
pathToNode,
|
getNodePathFromSourceRange(ast, range)
|
||||||
'VariableDeclarator'
|
)
|
||||||
)?.node
|
const nodes = paths.map(
|
||||||
)
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
const primaryLine = varDecs[0]
|
)
|
||||||
const secondaryVarDecs = varDecs.slice(1)
|
const varDecs = paths.map(
|
||||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
(pathToNode) =>
|
||||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
getNodeFromPath<VariableDeclarator>(
|
||||||
)
|
ast,
|
||||||
const isAllTooltips = nodes.every(
|
pathToNode,
|
||||||
(node) =>
|
'VariableDeclarator'
|
||||||
node?.type === 'CallExpression' &&
|
)?.node
|
||||||
toolTips.includes(node.callee.name as any)
|
)
|
||||||
)
|
const primaryLine = varDecs[0]
|
||||||
|
const secondaryVarDecs = varDecs.slice(1)
|
||||||
|
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||||
|
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
const transforms = getTransformInfos(
|
const theTransforms = getTransformInfos(
|
||||||
{
|
{
|
||||||
...selectionRanges,
|
...selectionRanges,
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
},
|
},
|
||||||
kclManager.ast,
|
ast,
|
||||||
'equalLength'
|
'equalLength'
|
||||||
|
)
|
||||||
|
setTransformInfos(theTransforms)
|
||||||
|
|
||||||
|
const _enableEqual =
|
||||||
|
!!secondaryVarDecs.length &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
theTransforms.every(Boolean)
|
||||||
|
setEnableEqual(_enableEqual)
|
||||||
|
}, [guiMode, selectionRanges])
|
||||||
|
if (guiMode.mode !== 'sketch') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!(transformInfos && ast)) return
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
})
|
||||||
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={!enableEqual}
|
||||||
|
className="group"
|
||||||
|
title="Equal Length"
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon="equal"
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
Equal Length
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
const enabled =
|
|
||||||
!!secondaryVarDecs.length &&
|
|
||||||
isAllTooltips &&
|
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
transforms.every(Boolean)
|
|
||||||
|
|
||||||
return { enabled, transforms }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyConstraintEqualLength({
|
|
||||||
selectionRanges,
|
|
||||||
}: {
|
|
||||||
selectionRanges: Selections
|
|
||||||
}): {
|
|
||||||
modifiedAst: Program
|
|
||||||
pathToNodeMap: PathToNodeMap
|
|
||||||
} {
|
|
||||||
const { enabled, transforms } = setEqualLengthInfo({ selectionRanges })
|
|
||||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
|
||||||
ast: kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos: transforms,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
})
|
|
||||||
return { modifiedAst, pathToNodeMap }
|
|
||||||
// kclManager.updateAst(modifiedAst, true, {
|
|
||||||
// // callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +1,84 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
PathToNodeMap,
|
TransformInfo,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import { sketchButtonClassnames } from 'Toolbar'
|
||||||
|
|
||||||
export function horzVertInfo(
|
export const HorzVert = ({
|
||||||
selectionRanges: Selections,
|
horOrVert,
|
||||||
|
}: {
|
||||||
horOrVert: 'vertical' | 'horizontal'
|
horOrVert: 'vertical' | 'horizontal'
|
||||||
) {
|
}) => {
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
useStore((s) => ({
|
||||||
)
|
guiMode: s.guiMode,
|
||||||
const nodes = paths.map(
|
ast: s.ast,
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
updateAst: s.updateAst,
|
||||||
)
|
selectionRanges: s.selectionRanges,
|
||||||
const isAllTooltips = nodes.every(
|
programMemory: s.programMemory,
|
||||||
(node) =>
|
setCursor: s.setCursor,
|
||||||
node?.type === 'CallExpression' &&
|
}))
|
||||||
toolTips.includes(node.callee.name as any)
|
const [enableHorz, setEnableHorz] = useState(false)
|
||||||
)
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ast) return
|
||||||
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
|
getNodePathFromSourceRange(ast, range)
|
||||||
|
)
|
||||||
|
const nodes = paths.map(
|
||||||
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(
|
const theTransforms = getTransformInfos(selectionRanges, ast, horOrVert)
|
||||||
selectionRanges,
|
setTransformInfos(theTransforms)
|
||||||
kclManager.ast,
|
|
||||||
horOrVert
|
|
||||||
)
|
|
||||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
|
||||||
return { enabled: _enableHorz, transforms: theTransforms }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyConstraintHorzVert(
|
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||||
selectionRanges: Selections,
|
setEnableHorz(_enableHorz)
|
||||||
horOrVert: 'vertical' | 'horizontal',
|
}, [guiMode, selectionRanges])
|
||||||
ast: Program,
|
if (guiMode.mode !== 'sketch') return null
|
||||||
programMemory: ProgramMemory
|
|
||||||
): {
|
return (
|
||||||
modifiedAst: Program
|
<button
|
||||||
pathToNodeMap: PathToNodeMap
|
onClick={() => {
|
||||||
} {
|
if (!transformInfos || !ast) return
|
||||||
const transformInfos = horzVertInfo(selectionRanges, horOrVert).transforms
|
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||||
return transformAstSketchLines({
|
ast,
|
||||||
ast,
|
selectionRanges,
|
||||||
selectionRanges,
|
transformInfos,
|
||||||
transformInfos,
|
programMemory,
|
||||||
programMemory,
|
referenceSegName: '',
|
||||||
referenceSegName: '',
|
})
|
||||||
})
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={!enableHorz}
|
||||||
|
className="group"
|
||||||
|
title={horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon={horOrVert === 'horizontal' ? 'horizontal' : 'vertical'}
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
{horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { create } from 'react-modal-promise'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
|
import {
|
||||||
|
BinaryPart,
|
||||||
|
Value,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
@ -8,170 +13,184 @@ import {
|
|||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
PathToNodeMap,
|
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
|
||||||
const getModalInfo = createInfoModal(GetInfoModal)
|
const getModalInfo = create(GetInfoModal as any)
|
||||||
|
|
||||||
export function intersectInfo({
|
export const Intersect = () => {
|
||||||
selectionRanges,
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}: {
|
useStore((s) => ({
|
||||||
selectionRanges: Selections
|
guiMode: s.guiMode,
|
||||||
}) {
|
ast: s.ast,
|
||||||
if (selectionRanges.codeBasedSelections.length < 2) {
|
updateAst: s.updateAst,
|
||||||
return {
|
selectionRanges: s.selectionRanges,
|
||||||
enabled: false,
|
programMemory: s.programMemory,
|
||||||
transforms: [],
|
setCursor: s.setCursor,
|
||||||
forcedSelectionRanges: { ...selectionRanges },
|
}))
|
||||||
|
const [enable, setEnable] = useState(false)
|
||||||
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
|
const [forecdSelectionRanges, setForcedSelectionRanges] =
|
||||||
|
useState<typeof selectionRanges>()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ast) return
|
||||||
|
if (selectionRanges.codeBasedSelections.length < 2) {
|
||||||
|
setEnable(false)
|
||||||
|
setForcedSelectionRanges({ ...selectionRanges })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const previousSegment =
|
const previousSegment =
|
||||||
selectionRanges.codeBasedSelections.length > 1 &&
|
selectionRanges.codeBasedSelections.length > 1 &&
|
||||||
isLinesParallelAndConstrained(
|
isLinesParallelAndConstrained(
|
||||||
kclManager.ast,
|
ast,
|
||||||
kclManager.programMemory,
|
programMemory,
|
||||||
selectionRanges.codeBasedSelections[0],
|
selectionRanges.codeBasedSelections[0],
|
||||||
selectionRanges.codeBasedSelections[1]
|
selectionRanges.codeBasedSelections[1]
|
||||||
)
|
)
|
||||||
const shouldUsePreviousSegment =
|
const shouldUsePreviousSegment =
|
||||||
selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' &&
|
selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' &&
|
||||||
previousSegment &&
|
previousSegment &&
|
||||||
previousSegment.isParallelAndConstrained
|
previousSegment.isParallelAndConstrained
|
||||||
|
|
||||||
const _forcedSelectionRanges: typeof selectionRanges = {
|
const _forcedSelectionRanges: typeof selectionRanges = {
|
||||||
...selectionRanges,
|
|
||||||
codeBasedSelections: [
|
|
||||||
selectionRanges.codeBasedSelections?.[0],
|
|
||||||
shouldUsePreviousSegment
|
|
||||||
? {
|
|
||||||
range: previousSegment.sourceRange,
|
|
||||||
type: 'line-end',
|
|
||||||
}
|
|
||||||
: selectionRanges.codeBasedSelections?.[1],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
|
||||||
)
|
|
||||||
const nodes = paths.map(
|
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
|
||||||
)
|
|
||||||
const varDecs = paths.map(
|
|
||||||
(pathToNode) =>
|
|
||||||
getNodeFromPath<VariableDeclarator>(
|
|
||||||
kclManager.ast,
|
|
||||||
pathToNode,
|
|
||||||
'VariableDeclarator'
|
|
||||||
)?.node
|
|
||||||
)
|
|
||||||
const primaryLine = varDecs[0]
|
|
||||||
const secondaryVarDecs = varDecs.slice(1)
|
|
||||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
|
||||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
[
|
|
||||||
...toolTips,
|
|
||||||
'startSketchAt', // TODO probably a better place for this to live
|
|
||||||
].includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(
|
|
||||||
{
|
|
||||||
...selectionRanges,
|
...selectionRanges,
|
||||||
codeBasedSelections: _forcedSelectionRanges.codeBasedSelections.slice(1),
|
codeBasedSelections: [
|
||||||
},
|
selectionRanges.codeBasedSelections?.[0],
|
||||||
kclManager.ast,
|
shouldUsePreviousSegment
|
||||||
'intersect'
|
? {
|
||||||
)
|
range: previousSegment.sourceRange,
|
||||||
|
type: 'line-end',
|
||||||
const _enableEqual =
|
}
|
||||||
secondaryVarDecs.length === 1 &&
|
: selectionRanges.codeBasedSelections?.[1],
|
||||||
isAllTooltips &&
|
],
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
theTransforms.every(Boolean) &&
|
|
||||||
_forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end'
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: _enableEqual,
|
|
||||||
transforms: theTransforms,
|
|
||||||
forcedSelectionRanges: _forcedSelectionRanges,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyConstraintIntersect({
|
|
||||||
selectionRanges,
|
|
||||||
}: {
|
|
||||||
selectionRanges: Selections
|
|
||||||
}): Promise<{
|
|
||||||
modifiedAst: Program
|
|
||||||
pathToNodeMap: PathToNodeMap
|
|
||||||
}> {
|
|
||||||
const { transforms, forcedSelectionRanges } = intersectInfo({
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
|
||||||
transformSecondarySketchLinesTagFirst({
|
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
|
||||||
selectionRanges: forcedSelectionRanges,
|
|
||||||
transformInfos: transforms,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
segName,
|
|
||||||
value,
|
|
||||||
valueNode,
|
|
||||||
variableName,
|
|
||||||
newVariableInsertIndex,
|
|
||||||
sign,
|
|
||||||
} = await getModalInfo({
|
|
||||||
segName: tagInfo?.tag,
|
|
||||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
|
||||||
value: valueUsedInTransform,
|
|
||||||
initialVariableName: 'offset',
|
|
||||||
})
|
|
||||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
|
||||||
return {
|
|
||||||
modifiedAst,
|
|
||||||
pathToNodeMap,
|
|
||||||
}
|
}
|
||||||
}
|
setForcedSelectionRanges(_forcedSelectionRanges)
|
||||||
// transform again but forcing certain values
|
|
||||||
const finalValue = removeDoubleNegatives(
|
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
valueNode as BinaryPart,
|
getNodePathFromSourceRange(ast, range)
|
||||||
sign,
|
|
||||||
variableName
|
|
||||||
)
|
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
|
||||||
transformSecondarySketchLinesTagFirst({
|
|
||||||
ast: kclManager.ast,
|
|
||||||
selectionRanges: forcedSelectionRanges,
|
|
||||||
transformInfos: transforms,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
forceSegName: segName,
|
|
||||||
forceValueUsedInTransform: finalValue,
|
|
||||||
})
|
|
||||||
if (variableName) {
|
|
||||||
const newBody = [..._modifiedAst.body]
|
|
||||||
newBody.splice(
|
|
||||||
newVariableInsertIndex,
|
|
||||||
0,
|
|
||||||
createVariableDeclaration(variableName, valueNode)
|
|
||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
const nodes = paths.map(
|
||||||
}
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
return {
|
)
|
||||||
modifiedAst: _modifiedAst,
|
const varDecs = paths.map(
|
||||||
pathToNodeMap: _pathToNodeMap,
|
(pathToNode) =>
|
||||||
}
|
getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)?.node
|
||||||
|
)
|
||||||
|
const primaryLine = varDecs[0]
|
||||||
|
const secondaryVarDecs = varDecs.slice(1)
|
||||||
|
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||||
|
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
[
|
||||||
|
...toolTips,
|
||||||
|
'startSketchAt', // TODO probably a better place for this to live
|
||||||
|
].includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const theTransforms = getTransformInfos(
|
||||||
|
{
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections:
|
||||||
|
_forcedSelectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
ast,
|
||||||
|
'intersect'
|
||||||
|
)
|
||||||
|
setTransformInfos(theTransforms)
|
||||||
|
|
||||||
|
const _enableEqual =
|
||||||
|
secondaryVarDecs.length === 1 &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
theTransforms.every(Boolean) &&
|
||||||
|
_forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end'
|
||||||
|
setEnable(_enableEqual)
|
||||||
|
}, [guiMode, selectionRanges])
|
||||||
|
if (guiMode.mode !== 'sketch') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!(transformInfos && ast && forecdSelectionRanges)) return
|
||||||
|
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges: forecdSelectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
segName,
|
||||||
|
value,
|
||||||
|
valueNode,
|
||||||
|
variableName,
|
||||||
|
newVariableInsertIndex,
|
||||||
|
sign,
|
||||||
|
}: {
|
||||||
|
segName: string
|
||||||
|
value: number
|
||||||
|
valueNode: Value
|
||||||
|
variableName?: string
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
sign: number
|
||||||
|
} = await getModalInfo({
|
||||||
|
segName: tagInfo?.tag,
|
||||||
|
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||||
|
value: valueUsedInTransform,
|
||||||
|
initialVariableName: 'offset',
|
||||||
|
} as any)
|
||||||
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// transform again but forcing certain values
|
||||||
|
const finalValue = removeDoubleNegatives(
|
||||||
|
valueNode as BinaryPart,
|
||||||
|
sign,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast,
|
||||||
|
selectionRanges: forecdSelectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
forceSegName: segName,
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
updateAst(_modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!enable}
|
||||||
|
title="Set Perpendicular Distance"
|
||||||
|
>
|
||||||
|
Set Perpendicular Distance
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,63 +1,78 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
import { Program, Value } from '../../lang/wasm'
|
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
PathToNodeMap,
|
TransformInfo,
|
||||||
getRemoveConstraintsTransforms,
|
getRemoveConstraintsTransforms,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
|
||||||
export function removeConstrainingValuesInfo({
|
export const RemoveConstrainingValues = () => {
|
||||||
selectionRanges,
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}: {
|
useStore((s) => ({
|
||||||
selectionRanges: Selections
|
guiMode: s.guiMode,
|
||||||
}) {
|
ast: s.ast,
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
updateAst: s.updateAst,
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
selectionRanges: s.selectionRanges,
|
||||||
)
|
programMemory: s.programMemory,
|
||||||
const nodes = paths.map(
|
setCursor: s.setCursor,
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
}))
|
||||||
)
|
const [enableHorz, setEnableHorz] = useState(false)
|
||||||
const isAllTooltips = nodes.every(
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
(node) =>
|
useEffect(() => {
|
||||||
node?.type === 'CallExpression' &&
|
if (!ast) return
|
||||||
toolTips.includes(node.callee.name as any)
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
)
|
getNodePathFromSourceRange(ast, range)
|
||||||
|
)
|
||||||
try {
|
const nodes = paths.map(
|
||||||
const transforms = getRemoveConstraintsTransforms(
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
selectionRanges,
|
)
|
||||||
kclManager.ast,
|
const isAllTooltips = nodes.every(
|
||||||
'removeConstrainingValues'
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
)
|
)
|
||||||
|
|
||||||
const enabled = isAllTooltips && transforms.every(Boolean)
|
try {
|
||||||
return { enabled, transforms }
|
const theTransforms = getRemoveConstraintsTransforms(
|
||||||
} catch (e) {
|
selectionRanges,
|
||||||
console.error(e)
|
ast,
|
||||||
return { enabled: false, transforms: [] }
|
'removeConstrainingValues'
|
||||||
}
|
)
|
||||||
}
|
setTransformInfos(theTransforms)
|
||||||
|
|
||||||
export function applyRemoveConstrainingValues({
|
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||||
selectionRanges,
|
setEnableHorz(_enableHorz)
|
||||||
}: {
|
} catch (e) {
|
||||||
selectionRanges: Selections
|
console.error(e)
|
||||||
}): {
|
}
|
||||||
modifiedAst: Program
|
}, [guiMode, selectionRanges])
|
||||||
pathToNodeMap: PathToNodeMap
|
if (guiMode.mode !== 'sketch') return null
|
||||||
} {
|
|
||||||
const { transforms } = removeConstrainingValuesInfo({ selectionRanges })
|
return (
|
||||||
return transformAstSketchLines({
|
<button
|
||||||
ast: kclManager.ast,
|
onClick={() => {
|
||||||
selectionRanges,
|
if (!transformInfos || !ast) return
|
||||||
transformInfos: transforms,
|
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||||
programMemory: kclManager.programMemory,
|
ast,
|
||||||
referenceSegName: '',
|
selectionRanges,
|
||||||
})
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={!enableHorz}
|
||||||
|
title="Remove Constraining Values"
|
||||||
|
>
|
||||||
|
Remove Constraining Values
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,152 +1,145 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { create } from 'react-modal-promise'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
|
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
PathToNodeMap,
|
ConstraintType,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import {
|
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
||||||
SetAngleLengthModal,
|
|
||||||
createSetAngleLengthModal,
|
|
||||||
} from '../SetAngleLengthModal'
|
|
||||||
import {
|
import {
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
createVariableDeclaration,
|
createVariableDeclaration,
|
||||||
} from '../../lang/modifyAst'
|
} from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
const getModalInfo = create(SetAngleLengthModal as any)
|
||||||
|
|
||||||
type Constraint = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
type ButtonType = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||||
|
|
||||||
export function absDistanceInfo({
|
const buttonLabels: Record<ButtonType, string> = {
|
||||||
selectionRanges,
|
xAbs: 'Set distance from X Axis',
|
||||||
constraint,
|
yAbs: 'Set distance from Y Axis',
|
||||||
}: {
|
snapToYAxis: 'Snap To Y Axis',
|
||||||
selectionRanges: Selections
|
snapToXAxis: 'Snap To X Axis',
|
||||||
constraint: Constraint
|
}
|
||||||
}) {
|
|
||||||
const disType =
|
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||||
constraint === 'xAbs' || constraint === 'yAbs'
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
? constraint
|
useStore((s) => ({
|
||||||
: constraint === 'snapToYAxis'
|
guiMode: s.guiMode,
|
||||||
|
ast: s.ast,
|
||||||
|
updateAst: s.updateAst,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
setCursor: s.setCursor,
|
||||||
|
}))
|
||||||
|
const disType: ConstraintType =
|
||||||
|
buttonType === 'xAbs' || buttonType === 'yAbs'
|
||||||
|
? buttonType
|
||||||
|
: buttonType === 'snapToYAxis'
|
||||||
? 'xAbs'
|
? 'xAbs'
|
||||||
: 'yAbs'
|
: 'yAbs'
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
)
|
useEffect(() => {
|
||||||
const nodes = paths.map(
|
if (!ast) return
|
||||||
(pathToNode) =>
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression').node
|
getNodePathFromSourceRange(ast, range)
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
toolTips.includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const transforms = getTransformInfos(selectionRanges, kclManager.ast, disType)
|
|
||||||
|
|
||||||
const enableY =
|
|
||||||
disType === 'yAbs' &&
|
|
||||||
selectionRanges.otherSelections.length === 1 &&
|
|
||||||
selectionRanges.otherSelections[0] === 'x-axis' // select the x axis to set the distance from it i.e. y
|
|
||||||
const enableX =
|
|
||||||
disType === 'xAbs' &&
|
|
||||||
selectionRanges.otherSelections.length === 1 &&
|
|
||||||
selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x
|
|
||||||
|
|
||||||
const enabled =
|
|
||||||
isAllTooltips &&
|
|
||||||
transforms.every(Boolean) &&
|
|
||||||
selectionRanges.codeBasedSelections.length === 1 &&
|
|
||||||
(enableX || enableY)
|
|
||||||
|
|
||||||
return { enabled, transforms }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyConstraintAbsDistance({
|
|
||||||
selectionRanges,
|
|
||||||
constraint,
|
|
||||||
}: {
|
|
||||||
selectionRanges: Selections
|
|
||||||
constraint: 'xAbs' | 'yAbs'
|
|
||||||
}): Promise<{
|
|
||||||
modifiedAst: Program
|
|
||||||
pathToNodeMap: PathToNodeMap
|
|
||||||
}> {
|
|
||||||
const transformInfos = absDistanceInfo({
|
|
||||||
selectionRanges,
|
|
||||||
constraint,
|
|
||||||
}).transforms
|
|
||||||
const { valueUsedInTransform } = transformAstSketchLines({
|
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
|
||||||
selectionRanges: selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
referenceSegName: '',
|
|
||||||
})
|
|
||||||
let forceVal = valueUsedInTransform || 0
|
|
||||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
|
||||||
await getModalInfo({
|
|
||||||
value: forceVal,
|
|
||||||
valueName: constraint === 'yAbs' ? 'yDis' : 'xDis',
|
|
||||||
})
|
|
||||||
let finalValue = removeDoubleNegatives(
|
|
||||||
valueNode as BinaryPart,
|
|
||||||
sign,
|
|
||||||
variableName
|
|
||||||
)
|
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
|
||||||
selectionRanges: selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
referenceSegName: '',
|
|
||||||
forceValueUsedInTransform: finalValue,
|
|
||||||
})
|
|
||||||
if (variableName) {
|
|
||||||
const newBody = [..._modifiedAst.body]
|
|
||||||
newBody.splice(
|
|
||||||
newVariableInsertIndex,
|
|
||||||
0,
|
|
||||||
createVariableDeclaration(variableName, valueNode)
|
|
||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
const nodes = paths.map(
|
||||||
}
|
(pathToNode) =>
|
||||||
return { modifiedAst: _modifiedAst, pathToNodeMap }
|
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||||
}
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
export function applyConstraintAxisAlign({
|
(node) =>
|
||||||
selectionRanges,
|
node?.type === 'CallExpression' &&
|
||||||
constraint,
|
toolTips.includes(node.callee.name as any)
|
||||||
}: {
|
)
|
||||||
selectionRanges: Selections
|
|
||||||
constraint: 'snapToYAxis' | 'snapToXAxis'
|
const theTransforms = getTransformInfos(selectionRanges, ast, disType)
|
||||||
}): {
|
setTransformInfos(theTransforms)
|
||||||
modifiedAst: Program
|
|
||||||
pathToNodeMap: PathToNodeMap
|
const enableY =
|
||||||
} {
|
disType === 'yAbs' &&
|
||||||
const transformInfos = absDistanceInfo({
|
selectionRanges.otherSelections.length === 1 &&
|
||||||
selectionRanges,
|
selectionRanges.otherSelections[0] === 'x-axis' // select the x axis to set the distance from it i.e. y
|
||||||
constraint,
|
const enableX =
|
||||||
}).transforms
|
disType === 'xAbs' &&
|
||||||
|
selectionRanges.otherSelections.length === 1 &&
|
||||||
let finalValue = createIdentifier('_0')
|
selectionRanges.otherSelections[0] === 'y-axis' // select the y axis to set the distance from it i.e. x
|
||||||
|
|
||||||
return transformAstSketchLines({
|
const _enableHorz =
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
isAllTooltips &&
|
||||||
selectionRanges: selectionRanges,
|
theTransforms.every(Boolean) &&
|
||||||
transformInfos,
|
selectionRanges.codeBasedSelections.length === 1 &&
|
||||||
programMemory: kclManager.programMemory,
|
(enableX || enableY)
|
||||||
referenceSegName: '',
|
setEnableAngLen(_enableHorz)
|
||||||
forceValueUsedInTransform: finalValue,
|
}, [guiMode, selectionRanges])
|
||||||
})
|
if (guiMode.mode !== 'sketch') return null
|
||||||
|
|
||||||
|
const isAlign = buttonType === 'snapToYAxis' || buttonType === 'snapToXAxis'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!(transformInfos && ast)) return
|
||||||
|
const { valueUsedInTransform } = transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges: selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
let forceVal = valueUsedInTransform || 0
|
||||||
|
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||||
|
await (!isAlign &&
|
||||||
|
getModalInfo({
|
||||||
|
value: forceVal,
|
||||||
|
valueName: disType === 'yAbs' ? 'yDis' : 'xDis',
|
||||||
|
} as any))
|
||||||
|
let finalValue = isAlign
|
||||||
|
? createIdentifier('_0')
|
||||||
|
: removeDoubleNegatives(valueNode, sign, variableName)
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges: selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAst(_modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log('e', e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!enableAngLen}
|
||||||
|
title={buttonLabels[buttonType]}
|
||||||
|
>
|
||||||
|
{buttonLabels[buttonType]}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,135 +1,155 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { create } from 'react-modal-promise'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
|
import {
|
||||||
|
BinaryPart,
|
||||||
|
Value,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
PathToNodeMap,
|
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
|
||||||
const getModalInfo = createInfoModal(GetInfoModal)
|
const getModalInfo = create(GetInfoModal as any)
|
||||||
|
|
||||||
export function angleBetweenInfo({
|
export const SetAngleBetween = () => {
|
||||||
selectionRanges,
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}: {
|
useStore((s) => ({
|
||||||
selectionRanges: Selections
|
guiMode: s.guiMode,
|
||||||
}) {
|
ast: s.ast,
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
updateAst: s.updateAst,
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
selectionRanges: s.selectionRanges,
|
||||||
)
|
programMemory: s.programMemory,
|
||||||
|
setCursor: s.setCursor,
|
||||||
const nodes = paths.map(
|
}))
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
const [enable, setEnable] = useState(false)
|
||||||
)
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
const varDecs = paths.map(
|
useEffect(() => {
|
||||||
(pathToNode) =>
|
if (!ast) return
|
||||||
getNodeFromPath<VariableDeclarator>(
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
kclManager.ast,
|
getNodePathFromSourceRange(ast, range)
|
||||||
pathToNode,
|
|
||||||
'VariableDeclarator'
|
|
||||||
)?.node
|
|
||||||
)
|
|
||||||
const primaryLine = varDecs[0]
|
|
||||||
const secondaryVarDecs = varDecs.slice(1)
|
|
||||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
|
||||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
toolTips.includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(
|
|
||||||
{
|
|
||||||
...selectionRanges,
|
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
|
||||||
},
|
|
||||||
kclManager.ast,
|
|
||||||
'setAngleBetween'
|
|
||||||
)
|
|
||||||
|
|
||||||
const _enableEqual =
|
|
||||||
secondaryVarDecs.length === 1 &&
|
|
||||||
isAllTooltips &&
|
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
theTransforms.every(Boolean)
|
|
||||||
return { enabled: _enableEqual, transforms: theTransforms }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyConstraintAngleBetween({
|
|
||||||
selectionRanges,
|
|
||||||
}: // constraint,
|
|
||||||
{
|
|
||||||
selectionRanges: Selections
|
|
||||||
// constraint: 'setHorzDistance' | 'setVertDistance'
|
|
||||||
}): Promise<{
|
|
||||||
modifiedAst: Program
|
|
||||||
pathToNodeMap: PathToNodeMap
|
|
||||||
}> {
|
|
||||||
const transformInfos = angleBetweenInfo({ selectionRanges }).transforms
|
|
||||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
|
||||||
transformSecondarySketchLinesTagFirst({
|
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
segName,
|
|
||||||
value,
|
|
||||||
valueNode,
|
|
||||||
variableName,
|
|
||||||
newVariableInsertIndex,
|
|
||||||
sign,
|
|
||||||
} = await getModalInfo({
|
|
||||||
segName: tagInfo?.tag,
|
|
||||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
|
||||||
value: valueUsedInTransform,
|
|
||||||
initialVariableName: 'angle',
|
|
||||||
} as any)
|
|
||||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
|
||||||
return {
|
|
||||||
modifiedAst,
|
|
||||||
pathToNodeMap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalValue = removeDoubleNegatives(
|
|
||||||
valueNode as BinaryPart,
|
|
||||||
sign,
|
|
||||||
variableName
|
|
||||||
)
|
|
||||||
// transform again but forcing certain values
|
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
|
||||||
transformSecondarySketchLinesTagFirst({
|
|
||||||
ast: kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
forceSegName: segName,
|
|
||||||
forceValueUsedInTransform: finalValue,
|
|
||||||
})
|
|
||||||
if (variableName) {
|
|
||||||
const newBody = [..._modifiedAst.body]
|
|
||||||
newBody.splice(
|
|
||||||
newVariableInsertIndex,
|
|
||||||
0,
|
|
||||||
createVariableDeclaration(variableName, valueNode)
|
|
||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
const nodes = paths.map(
|
||||||
}
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
return {
|
)
|
||||||
modifiedAst: _modifiedAst,
|
const varDecs = paths.map(
|
||||||
pathToNodeMap: _pathToNodeMap,
|
(pathToNode) =>
|
||||||
}
|
getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)?.node
|
||||||
|
)
|
||||||
|
const primaryLine = varDecs[0]
|
||||||
|
const secondaryVarDecs = varDecs.slice(1)
|
||||||
|
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||||
|
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const theTransforms = getTransformInfos(
|
||||||
|
{
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
ast,
|
||||||
|
'setAngleBetween'
|
||||||
|
)
|
||||||
|
setTransformInfos(theTransforms)
|
||||||
|
|
||||||
|
const _enableEqual =
|
||||||
|
secondaryVarDecs.length === 1 &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
theTransforms.every(Boolean)
|
||||||
|
setEnable(_enableEqual)
|
||||||
|
}, [guiMode, selectionRanges])
|
||||||
|
if (guiMode.mode !== 'sketch') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!(transformInfos && ast)) return
|
||||||
|
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
segName,
|
||||||
|
value,
|
||||||
|
valueNode,
|
||||||
|
variableName,
|
||||||
|
newVariableInsertIndex,
|
||||||
|
sign,
|
||||||
|
}: {
|
||||||
|
segName: string
|
||||||
|
value: number
|
||||||
|
valueNode: Value
|
||||||
|
variableName?: string
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
sign: number
|
||||||
|
} = await getModalInfo({
|
||||||
|
segName: tagInfo?.tag,
|
||||||
|
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||||
|
value: valueUsedInTransform,
|
||||||
|
initialVariableName: 'angle',
|
||||||
|
} as any)
|
||||||
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const finalValue = removeDoubleNegatives(
|
||||||
|
valueNode as BinaryPart,
|
||||||
|
sign,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
// transform again but forcing certain values
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
forceSegName: segName,
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
updateAst(_modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!enable}
|
||||||
|
title="Set Angle Between"
|
||||||
|
>
|
||||||
|
Set Angle Between
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,170 +1,188 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { create } from 'react-modal-promise'
|
||||||
|
import { toolTips, useStore } from '../../useStore'
|
||||||
|
import {
|
||||||
|
BinaryPart,
|
||||||
|
Value,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
|
TransformInfo,
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
PathToNodeMap,
|
ConstraintType,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
import { Selections } from 'lib/selections'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import { sketchButtonClassnames } from 'Toolbar'
|
||||||
|
|
||||||
const getModalInfo = createInfoModal(GetInfoModal)
|
const getModalInfo = create(GetInfoModal as any)
|
||||||
|
|
||||||
export function horzVertDistanceInfo({
|
type ButtonType =
|
||||||
selectionRanges,
|
| 'setHorzDistance'
|
||||||
constraint,
|
| 'setVertDistance'
|
||||||
}: {
|
| 'alignEndsHorizontally'
|
||||||
selectionRanges: Selections
|
| 'alignEndsVertically'
|
||||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
|
||||||
}) {
|
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
|
||||||
)
|
|
||||||
const nodes = paths.map(
|
|
||||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
|
||||||
)
|
|
||||||
const varDecs = paths.map(
|
|
||||||
(pathToNode) =>
|
|
||||||
getNodeFromPath<VariableDeclarator>(
|
|
||||||
kclManager.ast,
|
|
||||||
pathToNode,
|
|
||||||
'VariableDeclarator'
|
|
||||||
)?.node
|
|
||||||
)
|
|
||||||
const primaryLine = varDecs[0]
|
|
||||||
const secondaryVarDecs = varDecs.slice(1)
|
|
||||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
|
||||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
[
|
|
||||||
...toolTips,
|
|
||||||
'startSketchAt', // TODO probably a better place for this to live
|
|
||||||
].includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(
|
const buttonLabels: Record<ButtonType, string> = {
|
||||||
{
|
setHorzDistance: 'Set Horizontal Distance',
|
||||||
...selectionRanges,
|
setVertDistance: 'Set Vertical Distance',
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
alignEndsHorizontally: 'Align Ends Horizontally',
|
||||||
},
|
alignEndsVertically: 'Align Ends Vertically',
|
||||||
kclManager.ast,
|
|
||||||
constraint
|
|
||||||
)
|
|
||||||
const _enableEqual =
|
|
||||||
secondaryVarDecs.length === 1 &&
|
|
||||||
isAllTooltips &&
|
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
theTransforms.every(Boolean)
|
|
||||||
return { enabled: _enableEqual, transforms: theTransforms }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyConstraintHorzVertDistance({
|
export const SetHorzVertDistance = ({
|
||||||
selectionRanges,
|
buttonType,
|
||||||
constraint,
|
|
||||||
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
|
|
||||||
isAlign = false,
|
|
||||||
}: {
|
}: {
|
||||||
selectionRanges: Selections
|
buttonType: ButtonType
|
||||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
}) => {
|
||||||
isAlign?: false
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
}): Promise<{
|
useStore((s) => ({
|
||||||
modifiedAst: Program
|
guiMode: s.guiMode,
|
||||||
pathToNodeMap: PathToNodeMap
|
ast: s.ast,
|
||||||
}> {
|
updateAst: s.updateAst,
|
||||||
const transformInfos = horzVertDistanceInfo({
|
selectionRanges: s.selectionRanges,
|
||||||
selectionRanges,
|
programMemory: s.programMemory,
|
||||||
constraint,
|
setCursor: s.setCursor,
|
||||||
}).transforms
|
}))
|
||||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
const constraint: ConstraintType =
|
||||||
transformSecondarySketchLinesTagFirst({
|
buttonType === 'setHorzDistance' || buttonType === 'setVertDistance'
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
? buttonType
|
||||||
selectionRanges,
|
: buttonType === 'alignEndsHorizontally'
|
||||||
transformInfos,
|
? 'setVertDistance'
|
||||||
programMemory: kclManager.programMemory,
|
: 'setHorzDistance'
|
||||||
})
|
const [enable, setEnable] = useState(false)
|
||||||
const {
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
segName,
|
useEffect(() => {
|
||||||
value,
|
if (!ast) return
|
||||||
valueNode,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
variableName,
|
getNodePathFromSourceRange(ast, range)
|
||||||
newVariableInsertIndex,
|
)
|
||||||
sign,
|
const nodes = paths.map(
|
||||||
} = await getModalInfo({
|
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
segName: tagInfo?.tag,
|
)
|
||||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
const varDecs = paths.map(
|
||||||
value: valueUsedInTransform,
|
(pathToNode) =>
|
||||||
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
getNodeFromPath<VariableDeclarator>(
|
||||||
} as any)
|
ast,
|
||||||
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
pathToNode,
|
||||||
return {
|
'VariableDeclarator'
|
||||||
modifiedAst,
|
)?.node
|
||||||
pathToNodeMap,
|
)
|
||||||
}
|
const primaryLine = varDecs[0]
|
||||||
} else {
|
const secondaryVarDecs = varDecs.slice(1)
|
||||||
let finalValue = isAlign
|
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||||
? createLiteral(0)
|
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||||
: removeDoubleNegatives(valueNode as BinaryPart, sign, variableName)
|
)
|
||||||
// transform again but forcing certain values
|
const isAllTooltips = nodes.every(
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
(node) =>
|
||||||
transformSecondarySketchLinesTagFirst({
|
node?.type === 'CallExpression' &&
|
||||||
ast: kclManager.ast,
|
[
|
||||||
selectionRanges,
|
...toolTips,
|
||||||
transformInfos,
|
'startSketchAt', // TODO probably a better place for this to live
|
||||||
programMemory: kclManager.programMemory,
|
].includes(node.callee.name as any)
|
||||||
forceSegName: segName,
|
)
|
||||||
forceValueUsedInTransform: finalValue,
|
|
||||||
})
|
|
||||||
if (variableName) {
|
|
||||||
const newBody = [..._modifiedAst.body]
|
|
||||||
newBody.splice(
|
|
||||||
newVariableInsertIndex,
|
|
||||||
0,
|
|
||||||
createVariableDeclaration(variableName, valueNode)
|
|
||||||
)
|
|
||||||
_modifiedAst.body = newBody
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
modifiedAst: _modifiedAst,
|
|
||||||
pathToNodeMap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyConstraintHorzVertAlign({
|
const theTransforms = getTransformInfos(
|
||||||
selectionRanges,
|
{
|
||||||
constraint,
|
...selectionRanges,
|
||||||
}: {
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
selectionRanges: Selections
|
},
|
||||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
ast,
|
||||||
}): {
|
constraint
|
||||||
modifiedAst: Program
|
)
|
||||||
pathToNodeMap: PathToNodeMap
|
setTransformInfos(theTransforms)
|
||||||
} {
|
|
||||||
const transformInfos = horzVertDistanceInfo({
|
const _enableEqual =
|
||||||
selectionRanges,
|
secondaryVarDecs.length === 1 &&
|
||||||
constraint,
|
isAllTooltips &&
|
||||||
}).transforms
|
isOthersLinkedToPrimary &&
|
||||||
let finalValue = createLiteral(0)
|
theTransforms.every(Boolean)
|
||||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
setEnable(_enableEqual)
|
||||||
ast: kclManager.ast,
|
}, [guiMode, selectionRanges])
|
||||||
selectionRanges,
|
if (guiMode.mode !== 'sketch') return null
|
||||||
transformInfos,
|
|
||||||
programMemory: kclManager.programMemory,
|
const isAlign =
|
||||||
forceValueUsedInTransform: finalValue,
|
buttonType === 'alignEndsHorizontally' ||
|
||||||
})
|
buttonType === 'alignEndsVertically'
|
||||||
return {
|
|
||||||
modifiedAst: modifiedAst,
|
return (
|
||||||
pathToNodeMap,
|
<button
|
||||||
}
|
onClick={async () => {
|
||||||
|
if (!(transformInfos && ast)) return
|
||||||
|
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
segName,
|
||||||
|
value,
|
||||||
|
valueNode,
|
||||||
|
variableName,
|
||||||
|
newVariableInsertIndex,
|
||||||
|
sign,
|
||||||
|
}: {
|
||||||
|
segName: string
|
||||||
|
value: number
|
||||||
|
valueNode: Value
|
||||||
|
variableName?: string
|
||||||
|
newVariableInsertIndex: number
|
||||||
|
sign: number
|
||||||
|
} = await (!isAlign &&
|
||||||
|
getModalInfo({
|
||||||
|
segName: tagInfo?.tag,
|
||||||
|
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||||
|
value: valueUsedInTransform,
|
||||||
|
initialVariableName:
|
||||||
|
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||||
|
} as any))
|
||||||
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
|
updateAst(modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let finalValue = isAlign
|
||||||
|
? createLiteral(0)
|
||||||
|
: removeDoubleNegatives(valueNode as BinaryPart, sign, variableName)
|
||||||
|
// transform again but forcing certain values
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformSecondarySketchLinesTagFirst({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
forceSegName: segName,
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
updateAst(_modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!enable}
|
||||||
|
title={buttonLabels[buttonType]}
|
||||||
|
>
|
||||||
|
{buttonLabels[buttonType]}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,17 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { useState, useEffect } from 'react'
|
||||||
import { Selections } from 'lib/selections'
|
import { create } from 'react-modal-promise'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { toolTips, useStore } from '../../useStore'
|
||||||
|
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
PathToNodeMap,
|
TransformInfo,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import {
|
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
||||||
SetAngleLengthModal,
|
|
||||||
createSetAngleLengthModal,
|
|
||||||
} from '../SetAngleLengthModal'
|
|
||||||
import {
|
import {
|
||||||
createBinaryExpressionWithUnary,
|
createBinaryExpressionWithUnary,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -21,123 +19,141 @@ import {
|
|||||||
} from '../../lang/modifyAst'
|
} from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { normaliseAngle } from '../../lib/utils'
|
import { normaliseAngle } from '../../lib/utils'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { updateCursors } from '../../lang/util'
|
||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
const getModalInfo = create(SetAngleLengthModal as any)
|
||||||
|
|
||||||
export function setAngleLengthInfo({
|
type ButtonType = 'setAngle' | 'setLength'
|
||||||
selectionRanges,
|
|
||||||
angleOrLength = 'setLength',
|
|
||||||
}: {
|
|
||||||
selectionRanges: Selections
|
|
||||||
angleOrLength?: 'setLength' | 'setAngle'
|
|
||||||
}) {
|
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
|
||||||
getNodePathFromSourceRange(kclManager.ast, range)
|
|
||||||
)
|
|
||||||
const nodes = paths.map(
|
|
||||||
(pathToNode) =>
|
|
||||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression').node
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
toolTips.includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const transforms = getTransformInfos(
|
const buttonLabels: Record<ButtonType, string> = {
|
||||||
selectionRanges,
|
setAngle: 'Set Angle',
|
||||||
kclManager.ast,
|
setLength: 'Set Length',
|
||||||
angleOrLength
|
|
||||||
)
|
|
||||||
const enabled = isAllTooltips && transforms.every(Boolean)
|
|
||||||
return { enabled, transforms }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyConstraintAngleLength({
|
export const SetAngleLength = ({
|
||||||
selectionRanges,
|
angleOrLength,
|
||||||
angleOrLength = 'setLength',
|
|
||||||
}: {
|
}: {
|
||||||
selectionRanges: Selections
|
angleOrLength: ButtonType
|
||||||
angleOrLength?: 'setLength' | 'setAngle'
|
}) => {
|
||||||
}): Promise<{
|
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||||
modifiedAst: Program
|
useStore((s) => ({
|
||||||
pathToNodeMap: PathToNodeMap
|
guiMode: s.guiMode,
|
||||||
}> {
|
ast: s.ast,
|
||||||
const { transforms } = setAngleLengthInfo({ selectionRanges, angleOrLength })
|
updateAst: s.updateAst,
|
||||||
const { valueUsedInTransform } = transformAstSketchLines({
|
selectionRanges: s.selectionRanges,
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
programMemory: s.programMemory,
|
||||||
selectionRanges,
|
setCursor: s.setCursor,
|
||||||
transformInfos: transforms,
|
}))
|
||||||
programMemory: kclManager.programMemory,
|
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||||
referenceSegName: '',
|
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||||
})
|
useEffect(() => {
|
||||||
try {
|
if (!ast) return
|
||||||
const isReferencingYAxis =
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
selectionRanges.otherSelections.length === 1 &&
|
getNodePathFromSourceRange(ast, range)
|
||||||
selectionRanges.otherSelections[0] === 'y-axis'
|
)
|
||||||
const isReferencingYAxisAngle =
|
const nodes = paths.map(
|
||||||
isReferencingYAxis && angleOrLength === 'setAngle'
|
(pathToNode) =>
|
||||||
|
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||||
const isReferencingXAxis =
|
)
|
||||||
selectionRanges.otherSelections.length === 1 &&
|
const isAllTooltips = nodes.every(
|
||||||
selectionRanges.otherSelections[0] === 'x-axis'
|
(node) =>
|
||||||
const isReferencingXAxisAngle =
|
node?.type === 'CallExpression' &&
|
||||||
isReferencingXAxis && angleOrLength === 'setAngle'
|
toolTips.includes(node.callee.name as any)
|
||||||
|
|
||||||
let forceVal = valueUsedInTransform || 0
|
|
||||||
let calcIdentifier = createIdentifier('_0')
|
|
||||||
if (isReferencingYAxisAngle) {
|
|
||||||
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
|
|
||||||
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
|
|
||||||
} else if (isReferencingXAxisAngle) {
|
|
||||||
calcIdentifier = createIdentifier(Math.abs(forceVal) > 90 ? '_180' : '_0')
|
|
||||||
forceVal =
|
|
||||||
Math.abs(forceVal) > 90 ? normaliseAngle(forceVal - 180) : forceVal
|
|
||||||
}
|
|
||||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
|
||||||
await getModalInfo({
|
|
||||||
value: forceVal,
|
|
||||||
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
|
|
||||||
shouldCreateVariable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
let finalValue = removeDoubleNegatives(
|
|
||||||
valueNode as BinaryPart,
|
|
||||||
sign,
|
|
||||||
variableName
|
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
isReferencingYAxisAngle ||
|
|
||||||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
|
||||||
) {
|
|
||||||
finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue])
|
|
||||||
}
|
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
const theTransforms = getTransformInfos(selectionRanges, ast, angleOrLength)
|
||||||
transformAstSketchLines({
|
setTransformInfos(theTransforms)
|
||||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
|
||||||
selectionRanges,
|
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||||
transformInfos: transforms,
|
setEnableAngLen(_enableHorz)
|
||||||
programMemory: kclManager.programMemory,
|
}, [guiMode, selectionRanges])
|
||||||
referenceSegName: '',
|
if (guiMode.mode !== 'sketch') return null
|
||||||
forceValueUsedInTransform: finalValue,
|
|
||||||
})
|
return (
|
||||||
if (variableName) {
|
<button
|
||||||
const newBody = [..._modifiedAst.body]
|
onClick={async () => {
|
||||||
newBody.splice(
|
if (!(transformInfos && ast)) return
|
||||||
newVariableInsertIndex,
|
const { valueUsedInTransform } = transformAstSketchLines({
|
||||||
0,
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
createVariableDeclaration(variableName, valueNode)
|
selectionRanges,
|
||||||
)
|
transformInfos,
|
||||||
_modifiedAst.body = newBody
|
programMemory,
|
||||||
}
|
referenceSegName: '',
|
||||||
return {
|
})
|
||||||
modifiedAst: _modifiedAst,
|
try {
|
||||||
pathToNodeMap,
|
const isReferencingYAxis =
|
||||||
}
|
selectionRanges.otherSelections.length === 1 &&
|
||||||
} catch (e) {
|
selectionRanges.otherSelections[0] === 'y-axis'
|
||||||
console.log('erorr', e)
|
const isReferencingYAxisAngle =
|
||||||
throw e
|
isReferencingYAxis && angleOrLength === 'setAngle'
|
||||||
}
|
|
||||||
|
const isReferencingXAxis =
|
||||||
|
selectionRanges.otherSelections.length === 1 &&
|
||||||
|
selectionRanges.otherSelections[0] === 'x-axis'
|
||||||
|
const isReferencingXAxisAngle =
|
||||||
|
isReferencingXAxis && angleOrLength === 'setAngle'
|
||||||
|
|
||||||
|
let forceVal = valueUsedInTransform || 0
|
||||||
|
let calcIdentifier = createIdentifier('_0')
|
||||||
|
if (isReferencingYAxisAngle) {
|
||||||
|
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
|
||||||
|
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
|
||||||
|
} else if (isReferencingXAxisAngle) {
|
||||||
|
calcIdentifier = createIdentifier(
|
||||||
|
Math.abs(forceVal) > 90 ? '_180' : '_0'
|
||||||
|
)
|
||||||
|
forceVal =
|
||||||
|
Math.abs(forceVal) > 90
|
||||||
|
? normaliseAngle(forceVal - 180)
|
||||||
|
: forceVal
|
||||||
|
}
|
||||||
|
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||||
|
await getModalInfo({
|
||||||
|
value: forceVal,
|
||||||
|
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
|
||||||
|
shouldCreateVariable: true,
|
||||||
|
} as any)
|
||||||
|
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
|
||||||
|
if (
|
||||||
|
isReferencingYAxisAngle ||
|
||||||
|
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||||
|
) {
|
||||||
|
finalValue = createBinaryExpressionWithUnary([
|
||||||
|
calcIdentifier,
|
||||||
|
finalValue,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(ast)),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAst(_modifiedAst, true, {
|
||||||
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log('e', e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!enableAngLen}
|
||||||
|
title={buttonLabels[angleOrLength]}
|
||||||
|
>
|
||||||
|
{buttonLabels[angleOrLength]}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,229 +0,0 @@
|
|||||||
/* Adapted from https://github.com/argyleink/gui-challenges/blob/main/tooltips/tool-tip.css */
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
/* internal CSS vars */
|
|
||||||
--_delay: 200ms;
|
|
||||||
--_p-inline: 1ch;
|
|
||||||
--_p-block: 4px;
|
|
||||||
--_triangle-size: 7px;
|
|
||||||
/* --_bg: hsl(0 0% 20%); */
|
|
||||||
--_bg: var(--chalkboard-10);
|
|
||||||
--_shadow-alpha: 20%;
|
|
||||||
|
|
||||||
/* Used to power spacing and layout for RTL languages */
|
|
||||||
--isRTL: -1;
|
|
||||||
|
|
||||||
/* Using conic gradients to get a clear tip triangle */
|
|
||||||
--_bottom-tip: conic-gradient(
|
|
||||||
from -30deg at bottom,
|
|
||||||
#0000,
|
|
||||||
#000 1deg 60deg,
|
|
||||||
#0000 61deg
|
|
||||||
)
|
|
||||||
bottom / 100% 50% no-repeat;
|
|
||||||
--_top-tip: conic-gradient(
|
|
||||||
from 150deg at top,
|
|
||||||
#0000,
|
|
||||||
#000 1deg 60deg,
|
|
||||||
#0000 61deg
|
|
||||||
)
|
|
||||||
top / 100% 50% no-repeat;
|
|
||||||
--_right-tip: conic-gradient(
|
|
||||||
from -120deg at right,
|
|
||||||
#0000,
|
|
||||||
#000 1deg 60deg,
|
|
||||||
#0000 61deg
|
|
||||||
)
|
|
||||||
right / 50% 100% no-repeat;
|
|
||||||
--_left-tip: conic-gradient(
|
|
||||||
from 60deg at left,
|
|
||||||
#0000,
|
|
||||||
#000 1deg 60deg,
|
|
||||||
#0000 61deg
|
|
||||||
)
|
|
||||||
left / 50% 100% no-repeat;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
/* The parts that will be transitioned */
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(var(--_x, 0), var(--_y, 0));
|
|
||||||
transition: transform 0.15s ease-out, opacity 0.11s ease-out;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
inline-size: max-content;
|
|
||||||
max-inline-size: 25ch;
|
|
||||||
text-align: start;
|
|
||||||
font-family: var(--mono-font-family);
|
|
||||||
text-transform: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: normal;
|
|
||||||
line-height: initial;
|
|
||||||
letter-spacing: 0;
|
|
||||||
padding: var(--_p-block) var(--_p-inline);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--_bg);
|
|
||||||
@apply text-chalkboard-110;
|
|
||||||
will-change: filter;
|
|
||||||
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
|
|
||||||
drop-shadow(0 6px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .tooltip {
|
|
||||||
--_bg: var(--chalkboard-110);
|
|
||||||
@apply text-chalkboard-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO we don't support a light theme yet */
|
|
||||||
/* @media (prefers-color-scheme: light) {
|
|
||||||
.tooltip {
|
|
||||||
--_bg: white;
|
|
||||||
--_shadow-alpha: 15%;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
.tooltip:dir(rtl) {
|
|
||||||
--isRTL: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* :has and :is are pretty fresh CSS pseudo-selectors, may not see full support */
|
|
||||||
:has(> .tooltip) {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(:hover, :focus-visible, :active) > .tooltip {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: var(--_delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(:focus, :focus-visible, :focus-within) > .tooltip {
|
|
||||||
--_delay: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* prepend some prose for screen readers only */
|
|
||||||
.tooltip::before {
|
|
||||||
content: '; Has tooltip: ';
|
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
|
||||||
clip-path: inset(50%);
|
|
||||||
height: 1px;
|
|
||||||
width: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip shape is a pseudo element so we can cast a shadow */
|
|
||||||
.tooltip::after {
|
|
||||||
content: '';
|
|
||||||
background: var(--_bg);
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
inset: 0;
|
|
||||||
mask: var(--_tip);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.top,
|
|
||||||
.tooltip.blockStart,
|
|
||||||
.tooltip.bottom,
|
|
||||||
.tooltip.blockEnd {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TOP || BLOCK-START */
|
|
||||||
.tooltip.top,
|
|
||||||
.tooltip.blockStart {
|
|
||||||
inset-inline-start: 50%;
|
|
||||||
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
|
|
||||||
--_x: calc(50% * var(--isRTL));
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.top::after,
|
|
||||||
.tooltip.tooltip.blockStart::after {
|
|
||||||
--_tip: var(--_bottom-tip);
|
|
||||||
inset-block-end: calc(var(--_triangle-size) * -1);
|
|
||||||
border-block-end: var(--_triangle-size) solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RIGHT || INLINE-END */
|
|
||||||
.tooltip.right,
|
|
||||||
.tooltip.inlineEnd {
|
|
||||||
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
|
|
||||||
inset-block-end: 50%;
|
|
||||||
--_y: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.right::after,
|
|
||||||
.tooltip.tooltip.inlineEnd::after {
|
|
||||||
--_tip: var(--_left-tip);
|
|
||||||
inset-inline-start: calc(var(--_triangle-size) * -1);
|
|
||||||
border-inline-start: var(--_triangle-size) solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.right:dir(rtl)::after,
|
|
||||||
.tooltip.inlineEnd:dir(rtl)::after {
|
|
||||||
--_tip: var(--_right-tip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BOTTOM || BLOCK-END */
|
|
||||||
.tooltip.bottom,
|
|
||||||
.tooltip.blockEnd {
|
|
||||||
inset-inline-start: 50%;
|
|
||||||
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
|
|
||||||
--_x: calc(50% * var(--isRTL));
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottom::after,
|
|
||||||
.tooltip.tooltip.blockEnd::after {
|
|
||||||
--_tip: var(--_top-tip);
|
|
||||||
inset-block-start: calc(var(--_triangle-size) * -1);
|
|
||||||
border-block-start: var(--_triangle-size) solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LEFT || INLINE-START */
|
|
||||||
.tooltip.left,
|
|
||||||
.tooltip.inlineStart {
|
|
||||||
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
|
|
||||||
inset-block-end: 50%;
|
|
||||||
--_y: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.left::after,
|
|
||||||
.tooltip.tooltip.inlineStart::after {
|
|
||||||
--_tip: var(--_right-tip);
|
|
||||||
inset-inline-end: calc(var(--_triangle-size) * -1);
|
|
||||||
border-inline-end: var(--_triangle-size) solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.left:dir(rtl)::after,
|
|
||||||
.tooltip.inlineStart:dir(rtl)::after {
|
|
||||||
--_tip: var(--_left-tip);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
/* TOP || BLOCK-START */
|
|
||||||
:has(> :is(.tooltip.top, .tooltip.blockStart)):not(:hover, :active) .tooltip {
|
|
||||||
--_y: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RIGHT || INLINE-END */
|
|
||||||
:has(> :is(.tooltip.right, .tooltip.inlineEnd)):not(:hover, :active)
|
|
||||||
.tooltip {
|
|
||||||
--_x: calc(var(--isRTL) * -3px * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BOTTOM || BLOCK-END */
|
|
||||||
:has(> :is(.tooltip.bottom, .tooltip.blockEnd)):not(:hover, :active)
|
|
||||||
.tooltip {
|
|
||||||
--_y: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BOTTOM || BLOCK-END */
|
|
||||||
:has(> :is(.tooltip.left, .tooltip.inlineStart)):not(:hover, :active)
|
|
||||||
.tooltip {
|
|
||||||
--_x: calc(var(--isRTL) * 3px * -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
// We do use all the classes in this file currently, but we
|
|
||||||
// index into them with styles[position], which CSS Modules doesn't pick up.
|
|
||||||
// eslint-disable-next-line css-modules/no-unused-class
|
|
||||||
import styles from './Tooltip.module.css'
|
|
||||||
|
|
||||||
interface TooltipProps extends React.PropsWithChildren {
|
|
||||||
position?:
|
|
||||||
| 'top'
|
|
||||||
| 'bottom'
|
|
||||||
| 'left'
|
|
||||||
| 'right'
|
|
||||||
| 'blockStart'
|
|
||||||
| 'blockEnd'
|
|
||||||
| 'inlineStart'
|
|
||||||
| 'inlineEnd'
|
|
||||||
className?: string
|
|
||||||
delay?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Tooltip({
|
|
||||||
children,
|
|
||||||
position = 'top',
|
|
||||||
className,
|
|
||||||
delay = 200,
|
|
||||||
}: TooltipProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
|
||||||
inert="true"
|
|
||||||
role="tooltip"
|
|
||||||
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
|
|
||||||
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,11 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import {
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
Route,
|
|
||||||
RouterProvider,
|
|
||||||
createMemoryRouter,
|
|
||||||
createRoutesFromElements,
|
|
||||||
} from 'react-router-dom'
|
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar'
|
||||||
@ -98,24 +93,11 @@ describe('UserSidebarMenu tests', () => {
|
|||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
// We have to use a memory router in the testing environment,
|
return (
|
||||||
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
|
<BrowserRouter>
|
||||||
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
<CommandBarProvider>
|
||||||
const router = createMemoryRouter(
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
createRoutesFromElements(
|
</CommandBarProvider>
|
||||||
<Route
|
</BrowserRouter>
|
||||||
path="/file/:id"
|
|
||||||
element={
|
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
initialEntries: ['/file/new'],
|
|
||||||
initialIndex: 0,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return <RouterProvider router={router} />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,24 @@
|
|||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import {
|
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
faBars,
|
|
||||||
faBug,
|
|
||||||
faGear,
|
|
||||||
faSignOutAlt,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
|
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const filePath = useAbsoluteFilePath()
|
|
||||||
const displayedName = getDisplayName(user)
|
const displayedName = getDisplayName(user)
|
||||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const send = useGlobalStateContext()?.auth?.send
|
const {
|
||||||
|
auth: { send },
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
// Fallback logic for displaying user's "name":
|
// Fallback logic for displaying user's "name":
|
||||||
// 1. user.name
|
// 1. user.name
|
||||||
@ -131,30 +127,23 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
// since /settings is a nested route the sidebar doesn't close
|
// since /settings is a nested route the sidebar doesn't close
|
||||||
// automatically when navigating to it
|
// automatically when navigating to it
|
||||||
close()
|
close()
|
||||||
const targetPath = location.pathname.includes(paths.FILE)
|
navigate(
|
||||||
? filePath + paths.SETTINGS
|
(location.pathname.endsWith('/')
|
||||||
: paths.HOME + paths.SETTINGS
|
? location.pathname.slice(0, -1)
|
||||||
navigate(targetPath)
|
: location.pathname) + paths.SETTINGS
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="externalLink"
|
Element="link"
|
||||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||||
icon={{ icon: faGithub }}
|
icon={{ icon: faGithub }}
|
||||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||||
>
|
>
|
||||||
Request a feature
|
Request a feature
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
|
||||||
Element="externalLink"
|
|
||||||
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
|
||||||
icon={{ icon: faBug }}
|
|
||||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
|
||||||
>
|
|
||||||
Report a bug
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send('Log out')}
|
onClick={() => send('Log out')}
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { Dialog } from '@headlessui/react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { ActionButton } from './ActionButton'
|
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { useKclContext } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
export function WasmErrBanner() {
|
|
||||||
const [isBannerDismissed, setBannerDismissed] = useState(false)
|
|
||||||
|
|
||||||
const { wasmInitFailed } = useKclContext()
|
|
||||||
|
|
||||||
if (!wasmInitFailed) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
|
|
||||||
open={!isBannerDismissed}
|
|
||||||
onClose={() => ({})}
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="max-w-3xl mx-auto">
|
|
||||||
<div className="flex gap-2 justify-between items-start">
|
|
||||||
<h2 className="text-xl font-bold mb-4">
|
|
||||||
Problem with our WASM blob :(
|
|
||||||
</h2>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={() => setBannerDismissed(true)}
|
|
||||||
icon={{
|
|
||||||
icon: faX,
|
|
||||||
bgClassName:
|
|
||||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
|
||||||
iconClassName:
|
|
||||||
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent !border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://webassembly.org/"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
|
||||||
>
|
|
||||||
WASM or web assembly
|
|
||||||
</a>{' '}
|
|
||||||
is core part of how our app works. It might because you OS is not
|
|
||||||
up-to-date. If you're able to update your OS to a later version, try
|
|
||||||
that. If not create an issue on{' '}
|
|
||||||
<a
|
|
||||||
href="https://github.com/KittyCAD/modeling-app"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
|
||||||
>
|
|
||||||
our Github
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -109,6 +109,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
messageString += message
|
messageString += message
|
||||||
|
// console.log(messageString)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { LanguageServerClient } from '.'
|
|||||||
import { kclPlugin } from './plugin'
|
import { kclPlugin } from './plugin'
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import { parser as jsParser } from '@lezer/javascript'
|
import { parser as jsParser } from '@lezer/javascript'
|
||||||
import { EditorState } from '@uiw/react-codemirror'
|
|
||||||
|
|
||||||
const data = defineLanguageFacet({})
|
const data = defineLanguageFacet({})
|
||||||
|
|
||||||
@ -23,25 +22,7 @@ export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
|||||||
// For now let's use the javascript parser.
|
// For now let's use the javascript parser.
|
||||||
// It works really well and has good syntax highlighting.
|
// It works really well and has good syntax highlighting.
|
||||||
// We can use our lsp for the rest.
|
// We can use our lsp for the rest.
|
||||||
const lang = new Language(
|
const lang = new Language(data, jsParser, [], 'kcl')
|
||||||
data,
|
|
||||||
jsParser,
|
|
||||||
[
|
|
||||||
EditorState.languageData.of(() => [
|
|
||||||
{
|
|
||||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
|
||||||
commentTokens: {
|
|
||||||
line: '//',
|
|
||||||
block: {
|
|
||||||
open: '/*',
|
|
||||||
close: '*/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
'kcl'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create our supporting extension.
|
// Create our supporting extension.
|
||||||
const kclLsp = kclPlugin({
|
const kclLsp = kclPlugin({
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
CompletionItemKind,
|
CompletionItemKind,
|
||||||
CompletionTriggerKind,
|
CompletionTriggerKind,
|
||||||
} from 'vscode-languageserver-protocol'
|
} from 'vscode-languageserver-protocol'
|
||||||
import debounce from 'debounce-promise'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Completion,
|
Completion,
|
||||||
@ -54,11 +53,14 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
private languageId: string
|
private languageId: string
|
||||||
private documentVersion: number
|
private documentVersion: number
|
||||||
|
|
||||||
|
private changesTimeout: number
|
||||||
|
|
||||||
constructor(private view: EditorView, private allowHTMLContent: boolean) {
|
constructor(private view: EditorView, private allowHTMLContent: boolean) {
|
||||||
this.client = this.view.state.facet(client)
|
this.client = this.view.state.facet(client)
|
||||||
this.documentUri = this.view.state.facet(documentUri)
|
this.documentUri = this.view.state.facet(documentUri)
|
||||||
this.languageId = this.view.state.facet(languageId)
|
this.languageId = this.view.state.facet(languageId)
|
||||||
this.documentVersion = 0
|
this.documentVersion = 0
|
||||||
|
this.changesTimeout = 0
|
||||||
|
|
||||||
this.client.attachPlugin(this)
|
this.client.attachPlugin(this)
|
||||||
|
|
||||||
@ -69,10 +71,12 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
|
|
||||||
update({ docChanged }: ViewUpdate) {
|
update({ docChanged }: ViewUpdate) {
|
||||||
if (!docChanged) return
|
if (!docChanged) return
|
||||||
|
if (this.changesTimeout) clearTimeout(this.changesTimeout)
|
||||||
this.sendChange({
|
this.changesTimeout = window.setTimeout(() => {
|
||||||
documentText: this.view.state.doc.toString(),
|
this.sendChange({
|
||||||
})
|
documentText: this.view.state.doc.toString(),
|
||||||
|
})
|
||||||
|
}, changesDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@ -95,32 +99,14 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
|
|
||||||
async sendChange({ documentText }: { documentText: string }) {
|
async sendChange({ documentText }: { documentText: string }) {
|
||||||
if (!this.client.ready) return
|
if (!this.client.ready) return
|
||||||
|
|
||||||
if (documentText.length > 5000) {
|
|
||||||
// Clear out the text it thinks we have, large documents will throw a stack error.
|
|
||||||
// This is obviously not a good fix but it works for now til we figure
|
|
||||||
// out the stack limits in wasm and also rewrite the parser.
|
|
||||||
// Since this is only for hover and completions it will be fine,
|
|
||||||
// completions will still work for stdlib but hover will not.
|
|
||||||
// That seems like a fine trade-off for a working editor for the time
|
|
||||||
// being.
|
|
||||||
documentText = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debounce(
|
await this.client.textDocumentDidChange({
|
||||||
() => {
|
textDocument: {
|
||||||
return this.client.textDocumentDidChange({
|
uri: this.documentUri,
|
||||||
textDocument: {
|
version: this.documentVersion++,
|
||||||
uri: this.documentUri,
|
|
||||||
version: this.documentVersion++,
|
|
||||||
},
|
|
||||||
contentChanges: [{ text: documentText }],
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
changesDelay,
|
contentChanges: [{ text: documentText }],
|
||||||
{ leading: true }
|
})
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
// env vars were centralised so they could be mocked in jest
|
// all web app environment variables are defined here, jest doesn't like import.meta.env so centralising them here
|
||||||
// but isn't needed anymore with vite, so is now just a convention
|
// allows us to mock them in one place, see src/setupTests.ts, it pulls the variable names and valuse from .env.development
|
||||||
|
// note the exported variable name must match the env var name for the jest mocks to work
|
||||||
|
// i.e. const VITE_MY_VAR = import.meta.env.VITE_MY_VAR
|
||||||
|
// Maybe this file should be generated in a GHA from .env.development?
|
||||||
|
|
||||||
export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
||||||
.VITE_KC_API_WS_MODELING_URL
|
.VITE_KC_API_WS_MODELING_URL
|
||||||
@ -9,4 +12,3 @@ export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
|||||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||||
export const TEST = import.meta.env.TEST
|
export const TEST = import.meta.env.TEST
|
||||||
export const DEV = import.meta.env.DEV
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { BROWSER_FILE_NAME, IndexLoaderData, paths } from 'Router'
|
|
||||||
import { useRouteLoaderData } from 'react-router-dom'
|
|
||||||
|
|
||||||
export function useAbsoluteFilePath() {
|
|
||||||
const routeData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
|
||||||
|
|
||||||
return (
|
|
||||||
paths.FILE +
|
|
||||||
'/' +
|
|
||||||
encodeURIComponent(routeData?.file?.path || BROWSER_FILE_NAME)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
297
src/hooks/useAppMode.ts
Normal file
297
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
// needed somewhere to dump this logic,
|
||||||
|
// Once we have xState this should be removed
|
||||||
|
|
||||||
|
import { useStore, Selections } from 'useStore'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import { isReducedMotion } from 'lang/util'
|
||||||
|
import { isOverlap } from 'lib/utils'
|
||||||
|
|
||||||
|
interface DefaultPlanes {
|
||||||
|
xy: string
|
||||||
|
// TODO re-enable
|
||||||
|
// yz: string
|
||||||
|
// xz: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppMode() {
|
||||||
|
const {
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
selectionRanges,
|
||||||
|
engineCommandManager,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||||
|
}))
|
||||||
|
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === 'selectFace' &&
|
||||||
|
engineCommandManager
|
||||||
|
) {
|
||||||
|
const createAndShowPlanes = async () => {
|
||||||
|
let localDefaultPlanes: DefaultPlanes
|
||||||
|
if (!defaultPlanes) {
|
||||||
|
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||||
|
setDefaultPlanes(localDefaultPlanes)
|
||||||
|
} else {
|
||||||
|
localDefaultPlanes = defaultPlanes
|
||||||
|
}
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, false)
|
||||||
|
}
|
||||||
|
createAndShowPlanes()
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === 'enterSketchEdit' &&
|
||||||
|
engineCommandManager
|
||||||
|
) {
|
||||||
|
const enableSketchMode = async () => {
|
||||||
|
let localDefaultPlanes: DefaultPlanes
|
||||||
|
if (!defaultPlanes) {
|
||||||
|
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||||
|
setDefaultPlanes(localDefaultPlanes)
|
||||||
|
} else {
|
||||||
|
localDefaultPlanes = defaultPlanes
|
||||||
|
}
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, true)
|
||||||
|
// TODO figure out the plane to use based on the sketch
|
||||||
|
// maybe it's easier to make a new plane than rely on the defaults
|
||||||
|
await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sketch_mode_enable',
|
||||||
|
plane_id: localDefaultPlanes.xy,
|
||||||
|
ortho: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const proms: any[] = []
|
||||||
|
proms.push(
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'edit_mode_enter',
|
||||||
|
target: guiMode.pathId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await Promise.all(proms)
|
||||||
|
}
|
||||||
|
enableSketchMode()
|
||||||
|
setGuiMode({
|
||||||
|
...guiMode,
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||||
|
}
|
||||||
|
if (guiMode.mode === 'default') {
|
||||||
|
const pathId =
|
||||||
|
engineCommandManager &&
|
||||||
|
isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
if (pathId) {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'canEditSketch',
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathToNode: [],
|
||||||
|
pathId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (guiMode.mode === 'canEditSketch') {
|
||||||
|
if (
|
||||||
|
!engineCommandManager ||
|
||||||
|
!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'default',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
guiMode,
|
||||||
|
guiMode.mode,
|
||||||
|
engineCommandManager,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unSub = engineCommandManager?.subscribeTo({
|
||||||
|
event: 'select_with_point',
|
||||||
|
callback: async ({ data }) => {
|
||||||
|
if (!data.entity_id) return
|
||||||
|
if (!defaultPlanes) return
|
||||||
|
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||||
|
// user clicked something else in the scene
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
|
||||||
|
{
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sketch_mode_enable',
|
||||||
|
plane_id: data.entity_id,
|
||||||
|
ortho: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||||
|
const sketchUuid = uuidv4()
|
||||||
|
const proms: any[] = []
|
||||||
|
proms.push(
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: sketchUuid,
|
||||||
|
cmd: {
|
||||||
|
type: 'start_path',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
proms.push(
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'edit_mode_enter',
|
||||||
|
target: sketchUuid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const res = await Promise.all(proms)
|
||||||
|
console.log('res', res)
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathToNode: [],
|
||||||
|
pathId: sketchUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('sketchModeResponse', sketchModeResponse)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return unSub
|
||||||
|
}, [engineCommandManager, defaultPlanes])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPlane(
|
||||||
|
engineCommandManager: EngineCommandManager,
|
||||||
|
{
|
||||||
|
x_axis,
|
||||||
|
y_axis,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
x_axis: Models['Point3d_type']
|
||||||
|
y_axis: Models['Point3d_type']
|
||||||
|
color: Models['Color_type']
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const planeId = uuidv4()
|
||||||
|
await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'make_plane',
|
||||||
|
size: 60,
|
||||||
|
origin: { x: 0, y: 0, z: 0 },
|
||||||
|
x_axis,
|
||||||
|
y_axis,
|
||||||
|
clobber: false,
|
||||||
|
},
|
||||||
|
cmd_id: planeId,
|
||||||
|
})
|
||||||
|
await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'plane_set_color',
|
||||||
|
plane_id: planeId,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
return planeId
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultPlanesHidden(
|
||||||
|
engineCommandManager: EngineCommandManager | undefined,
|
||||||
|
defaultPlanes: DefaultPlanes,
|
||||||
|
hidden: boolean
|
||||||
|
) {
|
||||||
|
Object.values(defaultPlanes).forEach((planeId) => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'object_visible',
|
||||||
|
object_id: planeId,
|
||||||
|
hidden: hidden,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initDefaultPlanes(
|
||||||
|
engineCommandManager: EngineCommandManager
|
||||||
|
): Promise<DefaultPlanes> {
|
||||||
|
const xy = await createPlane(engineCommandManager, {
|
||||||
|
x_axis: { x: 1, y: 0, z: 0 },
|
||||||
|
y_axis: { x: 0, y: 1, z: 0 },
|
||||||
|
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||||
|
})
|
||||||
|
// TODO re-enable
|
||||||
|
// const yz = createPlane(engineCommandManager, {
|
||||||
|
// x_axis: { x: 0, y: 1, z: 0 },
|
||||||
|
// y_axis: { x: 0, y: 0, z: 1 },
|
||||||
|
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||||
|
// })
|
||||||
|
// const xz = createPlane(engineCommandManager, {
|
||||||
|
// x_axis: { x: 1, y: 0, z: 0 },
|
||||||
|
// y_axis: { x: 0, y: 0, z: 1 },
|
||||||
|
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||||
|
// })
|
||||||
|
return { xy }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCursorInSketchCommandRange(
|
||||||
|
artifactMap: ArtifactMap,
|
||||||
|
selectionRanges: Selections
|
||||||
|
): string | false {
|
||||||
|
const overlapingEntries = Object.entries(artifactMap || {}).filter(
|
||||||
|
([id, artifact]) =>
|
||||||
|
selectionRanges.codeBasedSelections.some(
|
||||||
|
(selection) =>
|
||||||
|
Array.isArray(selection?.range) &&
|
||||||
|
Array.isArray(artifact?.range) &&
|
||||||
|
isOverlap(selection.range, artifact.range) &&
|
||||||
|
(artifact.commandType === 'start_path' ||
|
||||||
|
artifact.commandType === 'extend_path' ||
|
||||||
|
artifact.commandType === 'close_path')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return overlapingEntries.length && overlapingEntries[0][1].parentId
|
||||||
|
? overlapingEntries[0][1].parentId
|
||||||
|
: overlapingEntries.find(
|
||||||
|
([, artifact]) => artifact.commandType === 'start_path'
|
||||||
|
)?.[0] || false
|
||||||
|
}
|
||||||
@ -1,23 +1,18 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
import { useModelingContext } from './useModelingContext'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { SourceRange } from 'lang/wasm'
|
|
||||||
import { getEventForSelectWithPoint } from 'lib/selections'
|
|
||||||
|
|
||||||
export function useEngineConnectionSubscriptions() {
|
export function useEngineConnectionSubscriptions() {
|
||||||
const { setHighlightRange, highlightRange } = useStore((s) => ({
|
const {
|
||||||
|
engineCommandManager,
|
||||||
|
setCursor2,
|
||||||
|
setHighlightRange,
|
||||||
|
highlightRange,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
setCursor2: s.setCursor2,
|
||||||
setHighlightRange: s.setHighlightRange,
|
setHighlightRange: s.setHighlightRange,
|
||||||
highlightRange: s.highlightRange,
|
highlightRange: s.highlightRange,
|
||||||
}))
|
}))
|
||||||
const { send, context } = useModelingContext()
|
|
||||||
|
|
||||||
interface RangeAndId {
|
|
||||||
id: string
|
|
||||||
range: SourceRange
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!engineCommandManager) return
|
if (!engineCommandManager) return
|
||||||
|
|
||||||
@ -26,7 +21,7 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
callback: ({ data }) => {
|
callback: ({ data }) => {
|
||||||
if (data?.entity_id) {
|
if (data?.entity_id) {
|
||||||
const sourceRange =
|
const sourceRange =
|
||||||
engineCommandManager.artifactMap?.[data.entity_id]?.range
|
engineCommandManager.sourceRangeMap[data.entity_id]
|
||||||
setHighlightRange(sourceRange)
|
setHighlightRange(sourceRange)
|
||||||
} else if (
|
} else if (
|
||||||
!highlightRange ||
|
!highlightRange ||
|
||||||
@ -38,21 +33,18 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: async (engineEvent) => {
|
callback: ({ data }) => {
|
||||||
const event = await getEventForSelectWithPoint(engineEvent, {
|
if (!data?.entity_id) {
|
||||||
sketchEnginePathId: context.sketchEnginePathId,
|
setCursor2()
|
||||||
})
|
return
|
||||||
send(event)
|
}
|
||||||
|
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
|
||||||
|
setCursor2({ range: sourceRange, type: 'default' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
unSubHover()
|
unSubHover()
|
||||||
unSubClick()
|
unSubClick()
|
||||||
}
|
}
|
||||||
}, [
|
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
|
||||||
engineCommandManager,
|
|
||||||
setHighlightRange,
|
|
||||||
highlightRange,
|
|
||||||
context.sketchEnginePathId,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import { FileContext } from 'components/FileMachineProvider'
|
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
export const useFileContext = () => {
|
|
||||||
return useContext(FileContext)
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { ModelingMachineContext } from 'components/ModelingMachineProvider'
|
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
export const useModelingContext = () => {
|
|
||||||
return useContext(ModelingMachineContext)
|
|
||||||
}
|
|
||||||
@ -1,94 +1,53 @@
|
|||||||
import { useLayoutEffect, useEffect, useRef } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
import { _executor, parse } from '../lang/wasm'
|
import { _executor } from '../lang/executor'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { EngineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { deferExecution } from 'lib/utils'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
|
|
||||||
export function useSetupEngineManager(
|
export function useSetupEngineManager(
|
||||||
streamRef: React.RefObject<HTMLDivElement>,
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
token?: string
|
token?: string
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
|
setEngineCommandManager,
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
setStreamDimensions,
|
setStreamDimensions,
|
||||||
streamDimensions,
|
executeCode,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
|
setEngineCommandManager: s.setEngineCommandManager,
|
||||||
setMediaStream: s.setMediaStream,
|
setMediaStream: s.setMediaStream,
|
||||||
setIsStreamReady: s.setIsStreamReady,
|
setIsStreamReady: s.setIsStreamReady,
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
executeCode: s.executeCode,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const streamWidth = streamRef?.current?.offsetWidth
|
const streamWidth = streamRef?.current?.offsetWidth
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
const streamHeight = streamRef?.current?.offsetHeight
|
||||||
|
|
||||||
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
kclManager.executeCode()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// Load the engine command manager once with the initial width and height,
|
|
||||||
// then we do not want to reload it.
|
|
||||||
const { width: quadWidth, height: quadHeight } = getDimensions(
|
|
||||||
streamWidth,
|
|
||||||
streamHeight
|
|
||||||
)
|
|
||||||
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
|
|
||||||
engineCommandManager.start({
|
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
width: quadWidth,
|
|
||||||
height: quadHeight,
|
|
||||||
executeCode: (code?: string) => {
|
|
||||||
const _ast = parse(code || kclManager.code)
|
|
||||||
return kclManager.executeAst(_ast, true)
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
})
|
|
||||||
setStreamDimensions({
|
|
||||||
streamWidth: quadWidth,
|
|
||||||
streamHeight: quadHeight,
|
|
||||||
})
|
|
||||||
hasSetNonZeroDimensions.current = true
|
|
||||||
}
|
|
||||||
}, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = deferExecution(() => {
|
|
||||||
const { width, height } = getDimensions(
|
|
||||||
streamRef?.current?.offsetWidth,
|
|
||||||
streamRef?.current?.offsetHeight
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
streamDimensions.streamWidth !== width ||
|
|
||||||
streamDimensions.streamHeight !== height
|
|
||||||
) {
|
|
||||||
engineCommandManager.handleResize({
|
|
||||||
streamWidth: width,
|
|
||||||
streamHeight: height,
|
|
||||||
})
|
|
||||||
setStreamDimensions({
|
|
||||||
streamWidth: width,
|
|
||||||
streamHeight: height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDimensions(streamWidth?: number, streamHeight?: number) {
|
|
||||||
const width = streamWidth ? streamWidth : 0
|
const width = streamWidth ? streamWidth : 0
|
||||||
const quadWidth = Math.round(width / 4) * 4
|
const quadWidth = Math.round(width / 4) * 4
|
||||||
const height = streamHeight ? streamHeight : 0
|
const height = streamHeight ? streamHeight : 0
|
||||||
const quadHeight = Math.round(height / 4) * 4
|
const quadHeight = Math.round(height / 4) * 4
|
||||||
return { width: quadWidth, height: quadHeight }
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setStreamDimensions({
|
||||||
|
streamWidth: quadWidth,
|
||||||
|
streamHeight: quadHeight,
|
||||||
|
})
|
||||||
|
if (!width || !height) return
|
||||||
|
const eng = new EngineCommandManager({
|
||||||
|
setMediaStream,
|
||||||
|
setIsStreamReady,
|
||||||
|
width: quadWidth,
|
||||||
|
height: quadHeight,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
setEngineCommandManager(eng)
|
||||||
|
eng.waitForReady.then(() => {
|
||||||
|
executeCode()
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
eng?.tearDown()
|
||||||
|
}
|
||||||
|
}, [quadWidth, quadHeight])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +1,54 @@
|
|||||||
import {
|
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||||
SetVarNameModal,
|
|
||||||
createSetVarNameModal,
|
|
||||||
} from 'components/SetVarNameModal'
|
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
|
||||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { create } from 'react-modal-promise'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
const getModalInfo = createSetVarNameModal(SetVarNameModal)
|
const getModalInfo = create(SetVarNameModal as any)
|
||||||
|
|
||||||
export function useConvertToVariable() {
|
export function useConvertToVariable() {
|
||||||
const { context } = useModelingContext()
|
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||||
|
(s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
ast: s.ast,
|
||||||
|
updateAst: s.updateAst,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
})
|
||||||
|
)
|
||||||
const [enable, setEnabled] = useState(false)
|
const [enable, setEnabled] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!ast) return
|
||||||
|
|
||||||
const { isSafe, value } = isNodeSafeToReplace(
|
const { isSafe, value } = isNodeSafeToReplace(
|
||||||
kclManager.ast,
|
ast,
|
||||||
context.selectionRanges.codeBasedSelections?.[0]?.range || []
|
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||||
)
|
)
|
||||||
const canReplace = isSafe && value.type !== 'Identifier'
|
const canReplace = isSafe && value.type !== 'Identifier'
|
||||||
const isOnlyOneSelection =
|
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||||
context.selectionRanges.codeBasedSelections.length === 1
|
|
||||||
|
|
||||||
const _enableHorz = canReplace && isOnlyOneSelection
|
const _enableHorz = canReplace && isOnlyOneSelection
|
||||||
setEnabled(_enableHorz)
|
setEnabled(_enableHorz)
|
||||||
}, [context.selectionRanges])
|
}, [guiMode, selectionRanges])
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
|
if (!ast) return
|
||||||
try {
|
try {
|
||||||
const { variableName } = await getModalInfo({
|
const { variableName } = await getModalInfo({
|
||||||
valueName: 'var',
|
valueName: 'var',
|
||||||
})
|
} as any)
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||||
kclManager.ast,
|
ast,
|
||||||
kclManager.programMemory,
|
programMemory,
|
||||||
context.selectionRanges.codeBasedSelections[0].range,
|
selectionRanges.codeBasedSelections[0].range,
|
||||||
variableName
|
variableName
|
||||||
)
|
)
|
||||||
|
|
||||||
kclManager.updateAst(_modifiedAst, true)
|
updateAst(_modifiedAst, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('e', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,6 @@ import reportWebVitals from './reportWebVitals'
|
|||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { Router } from './Router'
|
import { Router } from './Router'
|
||||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||||
import { inspect } from '@xstate/inspect'
|
|
||||||
import { DEV } from 'env'
|
|
||||||
|
|
||||||
if (DEV)
|
|
||||||
inspect({
|
|
||||||
iframe: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
|
|
||||||
|
|||||||
@ -1,387 +0,0 @@
|
|||||||
import { executeAst, executeCode } from 'useStore'
|
|
||||||
import { Selections } from 'lib/selections'
|
|
||||||
import { KCLError } from './errors'
|
|
||||||
import {
|
|
||||||
EngineCommandManager,
|
|
||||||
engineCommandManager,
|
|
||||||
} from './std/engineConnection'
|
|
||||||
import { deferExecution } from 'lib/utils'
|
|
||||||
import {
|
|
||||||
initPromise,
|
|
||||||
parse,
|
|
||||||
PathToNode,
|
|
||||||
Program,
|
|
||||||
ProgramMemory,
|
|
||||||
recast,
|
|
||||||
} from 'lang/wasm'
|
|
||||||
import { bracket } from 'lib/exampleKcl'
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
|
||||||
import { getNodeFromPath } from './queryAst'
|
|
||||||
import { IndexLoaderData } from 'Router'
|
|
||||||
import { useLoaderData } from 'react-router-dom'
|
|
||||||
|
|
||||||
const PERSIST_CODE_TOKEN = 'persistCode'
|
|
||||||
|
|
||||||
class KclManager {
|
|
||||||
private _code = bracket
|
|
||||||
private _ast: Program = {
|
|
||||||
body: [],
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
nonCodeMeta: {
|
|
||||||
nonCodeNodes: {},
|
|
||||||
start: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
private _programMemory: ProgramMemory = {
|
|
||||||
root: {},
|
|
||||||
return: null,
|
|
||||||
}
|
|
||||||
private _logs: string[] = []
|
|
||||||
private _kclErrors: KCLError[] = []
|
|
||||||
private _isExecuting = false
|
|
||||||
private _wasmInitFailed = true
|
|
||||||
|
|
||||||
engineCommandManager: EngineCommandManager
|
|
||||||
private _defferer = deferExecution((code: string) => {
|
|
||||||
const ast = parse(code)
|
|
||||||
this.executeAst(ast)
|
|
||||||
}, 600)
|
|
||||||
|
|
||||||
private _isExecutingCallback: (arg: boolean) => void = () => {}
|
|
||||||
private _codeCallBack: (arg: string) => void = () => {}
|
|
||||||
private _astCallBack: (arg: Program) => void = () => {}
|
|
||||||
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
|
|
||||||
private _logsCallBack: (arg: string[]) => void = () => {}
|
|
||||||
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
|
|
||||||
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
|
||||||
private _executeCallback: () => void = () => {}
|
|
||||||
|
|
||||||
get ast() {
|
|
||||||
return this._ast
|
|
||||||
}
|
|
||||||
set ast(ast) {
|
|
||||||
this._ast = ast
|
|
||||||
this._astCallBack(ast)
|
|
||||||
}
|
|
||||||
|
|
||||||
get code() {
|
|
||||||
return this._code
|
|
||||||
}
|
|
||||||
set code(code) {
|
|
||||||
this._code = code
|
|
||||||
this._codeCallBack(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
get programMemory() {
|
|
||||||
return this._programMemory
|
|
||||||
}
|
|
||||||
set programMemory(programMemory) {
|
|
||||||
this._programMemory = programMemory
|
|
||||||
this._programMemoryCallBack(programMemory)
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultPlanes() {
|
|
||||||
return this?.engineCommandManager?.defaultPlanes
|
|
||||||
}
|
|
||||||
|
|
||||||
get logs() {
|
|
||||||
return this._logs
|
|
||||||
}
|
|
||||||
set logs(logs) {
|
|
||||||
this._logs = logs
|
|
||||||
this._logsCallBack(logs)
|
|
||||||
}
|
|
||||||
|
|
||||||
get kclErrors() {
|
|
||||||
return this._kclErrors
|
|
||||||
}
|
|
||||||
set kclErrors(kclErrors) {
|
|
||||||
this._kclErrors = kclErrors
|
|
||||||
this._kclErrorsCallBack(kclErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
get isExecuting() {
|
|
||||||
return this._isExecuting
|
|
||||||
}
|
|
||||||
set isExecuting(isExecuting) {
|
|
||||||
this._isExecuting = isExecuting
|
|
||||||
this._isExecutingCallback(isExecuting)
|
|
||||||
}
|
|
||||||
|
|
||||||
get wasmInitFailed() {
|
|
||||||
return this._wasmInitFailed
|
|
||||||
}
|
|
||||||
set wasmInitFailed(wasmInitFailed) {
|
|
||||||
this._wasmInitFailed = wasmInitFailed
|
|
||||||
this._wasmInitFailedCallback(wasmInitFailed)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(engineCommandManager: EngineCommandManager) {
|
|
||||||
this.engineCommandManager = engineCommandManager
|
|
||||||
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
|
|
||||||
// TODO #819 remove zustand persistance logic in a few months
|
|
||||||
// short term migration, shouldn't make a difference for tauri app users
|
|
||||||
// anyway since that's filesystem based.
|
|
||||||
const zustandStore = JSON.parse(localStorage.getItem('store') || '{}')
|
|
||||||
if (storedCode === null && zustandStore?.state?.code) {
|
|
||||||
this.code = zustandStore.state.code
|
|
||||||
localStorage.setItem(PERSIST_CODE_TOKEN, this._code)
|
|
||||||
zustandStore.state.code = ''
|
|
||||||
localStorage.setItem('store', JSON.stringify(zustandStore))
|
|
||||||
} else if (storedCode === null) {
|
|
||||||
this.code = bracket
|
|
||||||
} else {
|
|
||||||
this.code = storedCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerCallBacks({
|
|
||||||
setCode,
|
|
||||||
setProgramMemory,
|
|
||||||
setAst,
|
|
||||||
setLogs,
|
|
||||||
setKclErrors,
|
|
||||||
setIsExecuting,
|
|
||||||
setWasmInitFailed,
|
|
||||||
}: {
|
|
||||||
setCode: (arg: string) => void
|
|
||||||
setProgramMemory: (arg: ProgramMemory) => void
|
|
||||||
setAst: (arg: Program) => void
|
|
||||||
setLogs: (arg: string[]) => void
|
|
||||||
setKclErrors: (arg: KCLError[]) => void
|
|
||||||
setIsExecuting: (arg: boolean) => void
|
|
||||||
setWasmInitFailed: (arg: boolean) => void
|
|
||||||
}) {
|
|
||||||
this._codeCallBack = setCode
|
|
||||||
this._programMemoryCallBack = setProgramMemory
|
|
||||||
this._astCallBack = setAst
|
|
||||||
this._logsCallBack = setLogs
|
|
||||||
this._kclErrorsCallBack = setKclErrors
|
|
||||||
this._isExecutingCallback = setIsExecuting
|
|
||||||
this._wasmInitFailedCallback = setWasmInitFailed
|
|
||||||
}
|
|
||||||
registerExecuteCallback(callback: () => void) {
|
|
||||||
this._executeCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureWasmInit() {
|
|
||||||
try {
|
|
||||||
await initPromise
|
|
||||||
if (this.wasmInitFailed) {
|
|
||||||
this.wasmInitFailed = false
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.wasmInitFailed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeAst(ast: Program = this._ast, updateCode = false) {
|
|
||||||
await this.ensureWasmInit()
|
|
||||||
this.isExecuting = true
|
|
||||||
const { logs, errors, programMemory } = await executeAst({
|
|
||||||
ast,
|
|
||||||
engineCommandManager: this.engineCommandManager,
|
|
||||||
defaultPlanes: this.defaultPlanes,
|
|
||||||
})
|
|
||||||
this.isExecuting = false
|
|
||||||
this._logs = logs
|
|
||||||
this._kclErrors = errors
|
|
||||||
this._programMemory = programMemory
|
|
||||||
this._ast = { ...ast }
|
|
||||||
if (updateCode) {
|
|
||||||
this._code = recast(ast)
|
|
||||||
this._codeCallBack(this._code)
|
|
||||||
}
|
|
||||||
this._executeCallback()
|
|
||||||
}
|
|
||||||
async executeAstMock(ast: Program = this._ast, updateCode = false) {
|
|
||||||
await this.ensureWasmInit()
|
|
||||||
const newCode = recast(ast)
|
|
||||||
const newAst = parse(newCode)
|
|
||||||
await this?.engineCommandManager?.waitForReady
|
|
||||||
if (updateCode) {
|
|
||||||
this.setCode(recast(ast))
|
|
||||||
}
|
|
||||||
this._ast = { ...newAst }
|
|
||||||
|
|
||||||
const { logs, errors, programMemory } = await executeAst({
|
|
||||||
ast: newAst,
|
|
||||||
engineCommandManager: this.engineCommandManager,
|
|
||||||
defaultPlanes: this.defaultPlanes,
|
|
||||||
useFakeExecutor: true,
|
|
||||||
})
|
|
||||||
this._logs = logs
|
|
||||||
this._kclErrors = errors
|
|
||||||
this._programMemory = programMemory
|
|
||||||
}
|
|
||||||
async executeCode(code?: string) {
|
|
||||||
await this.ensureWasmInit()
|
|
||||||
await this?.engineCommandManager?.waitForReady
|
|
||||||
if (!this?.engineCommandManager?.planesInitialized()) return
|
|
||||||
const result = await executeCode({
|
|
||||||
engineCommandManager,
|
|
||||||
code: code || this._code,
|
|
||||||
lastAst: this._ast,
|
|
||||||
defaultPlanes: this.defaultPlanes,
|
|
||||||
force: false,
|
|
||||||
})
|
|
||||||
if (!result.isChange) return
|
|
||||||
const { logs, errors, programMemory, ast } = result
|
|
||||||
this.logs = logs
|
|
||||||
this.kclErrors = errors
|
|
||||||
this.programMemory = programMemory
|
|
||||||
this.ast = ast
|
|
||||||
if (code) this.code = code
|
|
||||||
}
|
|
||||||
setCode(code: string) {
|
|
||||||
this._code = code
|
|
||||||
this._codeCallBack(code)
|
|
||||||
localStorage.setItem(PERSIST_CODE_TOKEN, code)
|
|
||||||
}
|
|
||||||
setCodeAndExecute(code: string) {
|
|
||||||
this.setCode(code)
|
|
||||||
if (code.trim()) {
|
|
||||||
this._defferer(code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._ast = {
|
|
||||||
body: [],
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
nonCodeMeta: {
|
|
||||||
nonCodeNodes: {},
|
|
||||||
start: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this._programMemory = {
|
|
||||||
root: {},
|
|
||||||
return: null,
|
|
||||||
}
|
|
||||||
this.engineCommandManager.endSession()
|
|
||||||
}
|
|
||||||
format() {
|
|
||||||
this.code = recast(parse(kclManager.code))
|
|
||||||
}
|
|
||||||
// There's overlapping resposibility between updateAst and executeAst.
|
|
||||||
// updateAst was added as it was used a lot before xState migration so makes the port easier.
|
|
||||||
// but should probably have think about which of the function to keep
|
|
||||||
async updateAst(
|
|
||||||
ast: Program,
|
|
||||||
execute: boolean,
|
|
||||||
optionalParams?: {
|
|
||||||
focusPath?: PathToNode
|
|
||||||
callBack?: (ast: Program) => void
|
|
||||||
}
|
|
||||||
): Promise<Selections | null> {
|
|
||||||
const newCode = recast(ast)
|
|
||||||
const astWithUpdatedSource = parse(newCode)
|
|
||||||
optionalParams?.callBack?.(astWithUpdatedSource)
|
|
||||||
let returnVal: Selections | null = null
|
|
||||||
|
|
||||||
this.code = newCode
|
|
||||||
if (optionalParams?.focusPath) {
|
|
||||||
const { node } = getNodeFromPath<any>(
|
|
||||||
astWithUpdatedSource,
|
|
||||||
optionalParams?.focusPath
|
|
||||||
)
|
|
||||||
const { start, end } = node
|
|
||||||
if (!start || !end) return null
|
|
||||||
returnVal = {
|
|
||||||
codeBasedSelections: [
|
|
||||||
{
|
|
||||||
type: 'default',
|
|
||||||
range: [start, end],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (execute) {
|
|
||||||
// Call execute on the set ast.
|
|
||||||
await this.executeAst(astWithUpdatedSource)
|
|
||||||
} else {
|
|
||||||
// When we don't re-execute, we still want to update the program
|
|
||||||
// memory with the new ast. So we will hit the mock executor
|
|
||||||
// instead.
|
|
||||||
await this.executeAstMock(astWithUpdatedSource)
|
|
||||||
}
|
|
||||||
return returnVal
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
|
|
||||||
return this.defaultPlanes[axis]
|
|
||||||
}
|
|
||||||
|
|
||||||
showPlanes() {
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
hidePlanes() {
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
|
||||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const kclManager = new KclManager(engineCommandManager)
|
|
||||||
|
|
||||||
const KclContext = createContext({
|
|
||||||
code: kclManager.code,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
ast: kclManager.ast,
|
|
||||||
isExecuting: kclManager.isExecuting,
|
|
||||||
errors: kclManager.kclErrors,
|
|
||||||
logs: kclManager.logs,
|
|
||||||
wasmInitFailed: kclManager.wasmInitFailed,
|
|
||||||
})
|
|
||||||
|
|
||||||
export function useKclContext() {
|
|
||||||
return useContext(KclContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KclContextProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
// If we try to use this component anywhere but under the paths.FILE route it will fail
|
|
||||||
// Because useLoaderData assumes we are on within it's context.
|
|
||||||
const { code: loadedCode } = useLoaderData() as IndexLoaderData
|
|
||||||
const [code, setCode] = useState(loadedCode || kclManager.code)
|
|
||||||
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
|
|
||||||
const [ast, setAst] = useState(kclManager.ast)
|
|
||||||
const [isExecuting, setIsExecuting] = useState(false)
|
|
||||||
const [errors, setErrors] = useState<KCLError[]>([])
|
|
||||||
const [logs, setLogs] = useState<string[]>([])
|
|
||||||
const [wasmInitFailed, setWasmInitFailed] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
kclManager.registerCallBacks({
|
|
||||||
setCode,
|
|
||||||
setProgramMemory,
|
|
||||||
setAst,
|
|
||||||
setLogs,
|
|
||||||
setKclErrors: setErrors,
|
|
||||||
setIsExecuting,
|
|
||||||
setWasmInitFailed,
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
return (
|
|
||||||
<KclContext.Provider
|
|
||||||
value={{
|
|
||||||
code,
|
|
||||||
programMemory,
|
|
||||||
ast,
|
|
||||||
isExecuting,
|
|
||||||
errors,
|
|
||||||
logs,
|
|
||||||
wasmInitFailed,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</KclContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,11 +1,12 @@
|
|||||||
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { initPromise, parse } from './wasm'
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
describe('testing AST', () => {
|
describe('testing AST', () => {
|
||||||
test('5 + 6', () => {
|
test('5 + 6', () => {
|
||||||
const result = parse('5 +6')
|
const result = parser_wasm('5 +6')
|
||||||
delete (result as any).nonCodeMeta
|
delete (result as any).nonCodeMeta
|
||||||
expect(result.body).toEqual([
|
expect(result.body).toEqual([
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@ describe('testing AST', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
test('const myVar = 5', () => {
|
test('const myVar = 5', () => {
|
||||||
const { body } = parse('const myVar = 5')
|
const { body } = parser_wasm('const myVar = 5')
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -70,7 +71,7 @@ describe('testing AST', () => {
|
|||||||
const code = `const myVar = 5
|
const code = `const myVar = 5
|
||||||
const newVar = myVar + 1
|
const newVar = myVar + 1
|
||||||
`
|
`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -141,8 +142,44 @@ const newVar = myVar + 1
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('testing function declaration', () => {
|
describe('testing function declaration', () => {
|
||||||
|
test('fn funcN = () => {}', () => {
|
||||||
|
const { body } = parser_wasm('fn funcN = () => {}')
|
||||||
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
|
expect(body).toEqual([
|
||||||
|
{
|
||||||
|
type: 'VariableDeclaration',
|
||||||
|
start: 0,
|
||||||
|
end: 19,
|
||||||
|
kind: 'fn',
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
type: 'VariableDeclarator',
|
||||||
|
start: 3,
|
||||||
|
end: 19,
|
||||||
|
id: {
|
||||||
|
type: 'Identifier',
|
||||||
|
start: 3,
|
||||||
|
end: 8,
|
||||||
|
name: 'funcN',
|
||||||
|
},
|
||||||
|
init: {
|
||||||
|
type: 'FunctionExpression',
|
||||||
|
start: 11,
|
||||||
|
end: 19,
|
||||||
|
params: [],
|
||||||
|
body: {
|
||||||
|
start: 17,
|
||||||
|
end: 19,
|
||||||
|
body: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
test('fn funcN = (a, b) => {return a + b}', () => {
|
test('fn funcN = (a, b) => {return a + b}', () => {
|
||||||
const { body } = parse(
|
const { body } = parser_wasm(
|
||||||
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
|
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
|
||||||
)
|
)
|
||||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
@ -219,7 +256,7 @@ describe('testing function declaration', () => {
|
|||||||
test('call expression assignment', () => {
|
test('call expression assignment', () => {
|
||||||
const code = `fn funcN = (a, b) => { return a + b }
|
const code = `fn funcN = (a, b) => { return a + b }
|
||||||
const myVar = funcN(1, 2)`
|
const myVar = funcN(1, 2)`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -351,7 +388,7 @@ describe('testing pipe operator special', () => {
|
|||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
|> rx(45, %)
|
|> rx(45, %)
|
||||||
`
|
`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
delete (body[0] as any).declarations[0].init.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -367,7 +404,7 @@ describe('testing pipe operator special', () => {
|
|||||||
id: { type: 'Identifier', start: 6, end: 14, name: 'mySketch' },
|
id: { type: 'Identifier', start: 6, end: 14, name: 'mySketch' },
|
||||||
init: {
|
init: {
|
||||||
type: 'PipeExpression',
|
type: 'PipeExpression',
|
||||||
start: 17,
|
start: 15,
|
||||||
end: 145,
|
end: 145,
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
@ -587,7 +624,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('pipe operator with binary expression', () => {
|
test('pipe operator with binary expression', () => {
|
||||||
let code = `const myVar = 5 + 6 |> myFunc(45, %)`
|
let code = `const myVar = 5 + 6 |> myFunc(45, %)`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
delete (body as any)[0].declarations[0].init.nonCodeMeta
|
delete (body as any)[0].declarations[0].init.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -608,7 +645,7 @@ describe('testing pipe operator special', () => {
|
|||||||
},
|
},
|
||||||
init: {
|
init: {
|
||||||
type: 'PipeExpression',
|
type: 'PipeExpression',
|
||||||
start: 14,
|
start: 12,
|
||||||
end: 36,
|
end: 36,
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
@ -669,7 +706,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('array expression', () => {
|
test('array expression', () => {
|
||||||
let code = `const yo = [1, '2', three, 4 + 5]`
|
let code = `const yo = [1, '2', three, 4 + 5]`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -744,7 +781,7 @@ describe('testing pipe operator special', () => {
|
|||||||
'const three = 3',
|
'const three = 3',
|
||||||
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -888,7 +925,7 @@ describe('testing pipe operator special', () => {
|
|||||||
const code = `const yo = {key: {
|
const code = `const yo = {key: {
|
||||||
key2: 'value'
|
key2: 'value'
|
||||||
}}`
|
}}`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -956,7 +993,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object expression with array ast', () => {
|
test('object expression with array ast', () => {
|
||||||
const code = `const yo = {key: [1, '2']}`
|
const code = `const yo = {key: [1, '2']}`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1020,7 +1057,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression simple', () => {
|
test('object memberExpression simple', () => {
|
||||||
const code = `const prop = yo.one.two`
|
const code = `const prop = yo.one.two`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1075,7 +1112,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression with square braces', () => {
|
test('object memberExpression with square braces', () => {
|
||||||
const code = `const prop = yo.one["two"]`
|
const code = `const prop = yo.one["two"]`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1131,7 +1168,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression with two square braces literal and identifier', () => {
|
test('object memberExpression with two square braces literal and identifier', () => {
|
||||||
const code = `const prop = yo["one"][two]`
|
const code = `const prop = yo["one"][two]`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1190,7 +1227,7 @@ describe('testing pipe operator special', () => {
|
|||||||
describe('nests binary expressions correctly', () => {
|
describe('nests binary expressions correctly', () => {
|
||||||
it('works with the simple case', () => {
|
it('works with the simple case', () => {
|
||||||
const code = `const yo = 1 + 2`
|
const code = `const yo = 1 + 2`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1234,7 +1271,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
it('should nest according to precedence with multiply first', () => {
|
it('should nest according to precedence with multiply first', () => {
|
||||||
// should be binExp { binExp { lit-1 * lit-2 } + lit}
|
// should be binExp { binExp { lit-1 * lit-2 } + lit}
|
||||||
const code = `const yo = 1 * 2 + 3`
|
const code = `const yo = 1 * 2 + 3`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1291,7 +1328,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
it('should nest according to precedence with sum first', () => {
|
it('should nest according to precedence with sum first', () => {
|
||||||
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
|
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
|
||||||
const code = `const yo = 1 + 2 * 3`
|
const code = `const yo = 1 + 2 * 3`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1347,7 +1384,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with two opperators of equal precedence', () => {
|
it('should nest properly with two opperators of equal precedence', () => {
|
||||||
const code = `const yo = 1 + 2 - 3`
|
const code = `const yo = 1 + 2 - 3`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect((body[0] as any).declarations[0].init).toEqual({
|
expect((body[0] as any).declarations[0].init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
@ -1384,7 +1421,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with two opperators of equal (but higher) precedence', () => {
|
it('should nest properly with two opperators of equal (but higher) precedence', () => {
|
||||||
const code = `const yo = 1 * 2 / 3`
|
const code = `const yo = 1 * 2 / 3`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
expect((body[0] as any).declarations[0].init).toEqual({
|
expect((body[0] as any).declarations[0].init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
@ -1421,7 +1458,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with longer example', () => {
|
it('should nest properly with longer example', () => {
|
||||||
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
|
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const init = (body[0] as any).declarations[0].init
|
const init = (body[0] as any).declarations[0].init
|
||||||
expect(init).toEqual({
|
expect(init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
@ -1477,23 +1514,24 @@ const key = 'c'`
|
|||||||
const nonCodeMetaInstance = {
|
const nonCodeMetaInstance = {
|
||||||
type: 'NonCodeNode',
|
type: 'NonCodeNode',
|
||||||
start: code.indexOf('\n// this is a comment'),
|
start: code.indexOf('\n// this is a comment'),
|
||||||
end: code.indexOf('const key') - 1,
|
end: code.indexOf('const key'),
|
||||||
value: {
|
value: {
|
||||||
type: 'blockComment',
|
type: 'blockComment',
|
||||||
style: 'line',
|
|
||||||
value: 'this is a comment',
|
value: 'this is a comment',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const { nonCodeMeta } = parse(code)
|
const { nonCodeMeta } = parser_wasm(code)
|
||||||
expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance)
|
expect(nonCodeMeta.nonCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||||
|
|
||||||
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
||||||
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
||||||
const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace)
|
const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
||||||
expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual(
|
codeWithExtraStartWhitespace
|
||||||
|
)
|
||||||
|
expect(nonCodeMeta2.nonCodeNodes[0].value).toStrictEqual(
|
||||||
nonCodeMetaInstance.value
|
nonCodeMetaInstance.value
|
||||||
)
|
)
|
||||||
expect(nonCodeMeta2.nonCodeNodes[0][0].start).not.toBe(
|
expect(nonCodeMeta2.nonCodeNodes[0].start).not.toBe(
|
||||||
nonCodeMetaInstance.start
|
nonCodeMetaInstance.start
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1507,17 +1545,16 @@ const key = 'c'`
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
`
|
`
|
||||||
|
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const indexOfSecondLineToExpression = 2
|
const indexOfSecondLineToExpression = 2
|
||||||
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
||||||
.nonCodeNodes
|
.nonCodeNodes
|
||||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression][0]).toEqual({
|
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||||
type: 'NonCodeNode',
|
type: 'NonCodeNode',
|
||||||
start: 106,
|
start: 106,
|
||||||
end: 163,
|
end: 166,
|
||||||
value: {
|
value: {
|
||||||
type: 'inlineComment',
|
type: 'blockComment',
|
||||||
style: 'block',
|
|
||||||
value: 'this is\n a comment\n spanning a few lines',
|
value: 'this is\n a comment\n spanning a few lines',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1532,17 +1569,16 @@ const key = 'c'`
|
|||||||
' |> rx(90, %)',
|
' |> rx(90, %)',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
||||||
.nonCodeNodes[3][0]
|
.nonCodeNodes
|
||||||
expect(sketchNonCodeMeta).toEqual({
|
expect(sketchNonCodeMeta[3]).toEqual({
|
||||||
type: 'NonCodeNode',
|
type: 'NonCodeNode',
|
||||||
start: 125,
|
start: 125,
|
||||||
end: 138,
|
end: 141,
|
||||||
value: {
|
value: {
|
||||||
type: 'blockComment',
|
type: 'blockComment',
|
||||||
value: 'a comment',
|
value: 'a comment',
|
||||||
style: 'line',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1551,7 +1587,7 @@ const key = 'c'`
|
|||||||
describe('test UnaryExpression', () => {
|
describe('test UnaryExpression', () => {
|
||||||
it('should parse a unary expression in simple var dec situation', () => {
|
it('should parse a unary expression in simple var dec situation', () => {
|
||||||
const code = `const myVar = -min(4, 100)`
|
const code = `const myVar = -min(4, 100)`
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
||||||
expect(myVarInit).toEqual({
|
expect(myVarInit).toEqual({
|
||||||
type: 'UnaryExpression',
|
type: 'UnaryExpression',
|
||||||
@ -1577,7 +1613,7 @@ describe('test UnaryExpression', () => {
|
|||||||
describe('testing nested call expressions', () => {
|
describe('testing nested call expressions', () => {
|
||||||
it('callExp in a binExp in a callExp', () => {
|
it('callExp in a binExp in a callExp', () => {
|
||||||
const code = 'const myVar = min(100, 1 + legLen(5, 3))'
|
const code = 'const myVar = min(100, 1 + legLen(5, 3))'
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
||||||
expect(myVarInit).toEqual({
|
expect(myVarInit).toEqual({
|
||||||
type: 'CallExpression',
|
type: 'CallExpression',
|
||||||
@ -1615,7 +1651,7 @@ describe('testing nested call expressions', () => {
|
|||||||
describe('should recognise callExpresions in binaryExpressions', () => {
|
describe('should recognise callExpresions in binaryExpressions', () => {
|
||||||
const code = "xLineTo(segEndX('seg02', %) + 1, %)"
|
const code = "xLineTo(segEndX('seg02', %) + 1, %)"
|
||||||
it('should recognise the callExp', () => {
|
it('should recognise the callExp', () => {
|
||||||
const { body } = parse(code)
|
const { body } = parser_wasm(code)
|
||||||
const callExpArgs = (body?.[0] as any).expression?.arguments
|
const callExpArgs = (body?.[0] as any).expression?.arguments
|
||||||
expect(callExpArgs).toEqual([
|
expect(callExpArgs).toEqual([
|
||||||
{
|
{
|
||||||
@ -1654,13 +1690,18 @@ describe('parsing errors', () => {
|
|||||||
|
|
||||||
let _theError
|
let _theError
|
||||||
try {
|
try {
|
||||||
const result = expect(parse(code))
|
const result = expect(parser_wasm(code))
|
||||||
|
console.log('result', result)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_theError = e
|
_theError = e
|
||||||
}
|
}
|
||||||
const theError = _theError as any
|
const theError = _theError as any
|
||||||
expect(theError).toEqual(
|
expect(theError).toEqual(
|
||||||
new KCLError('syntax', 'Unexpected token', [[27, 28]])
|
new KCLError(
|
||||||
|
'unexpected',
|
||||||
|
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
|
||||||
|
[[29, 30]]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
48
src/lang/abstractSyntaxTree.ts
Normal file
48
src/lang/abstractSyntaxTree.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
|
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/kcl/bindings/KclError'
|
||||||
|
|
||||||
|
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
|
||||||
|
ranges.map(([start, end]) => [start, end])
|
||||||
|
|
||||||
|
export const parser_wasm = (code: string): Program => {
|
||||||
|
try {
|
||||||
|
const program: Program = parse_js(code)
|
||||||
|
return program
|
||||||
|
} catch (e: any) {
|
||||||
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
|
const kclError = new KCLError(
|
||||||
|
parsed.kind,
|
||||||
|
parsed.msg,
|
||||||
|
rangeTypeFix(parsed.sourceRanges)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(kclError)
|
||||||
|
throw kclError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function asyncParser(code: string): Promise<Program> {
|
||||||
|
await initPromise
|
||||||
|
try {
|
||||||
|
const program: Program = parse_js(code)
|
||||||
|
return program
|
||||||
|
} catch (e: any) {
|
||||||
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
|
const kclError = new KCLError(
|
||||||
|
parsed.kind,
|
||||||
|
parsed.msg,
|
||||||
|
rangeTypeFix(parsed.sourceRanges)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(kclError)
|
||||||
|
throw kclError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rangeOfToken(token: Token | undefined): [number, number][] {
|
||||||
|
return token === undefined ? [] : [[token.start, token.end]]
|
||||||
|
}
|
||||||
37
src/lang/abstractSyntaxTreeTypes.ts
Normal file
37
src/lang/abstractSyntaxTreeTypes.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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'
|
||||||
|
| 'ExpressionStatement'
|
||||||
|
| 'BinaryExpression'
|
||||||
|
| 'CallExpression'
|
||||||
|
| 'Identifier'
|
||||||
|
| 'ReturnStatement'
|
||||||
|
| 'VariableDeclaration'
|
||||||
|
| 'VariableDeclarator'
|
||||||
|
| 'MemberExpression'
|
||||||
|
| 'ArrayExpression'
|
||||||
|
| 'ObjectExpression'
|
||||||
|
| 'ObjectProperty'
|
||||||
|
| 'FunctionExpression'
|
||||||
|
| 'PipeExpression'
|
||||||
|
| 'PipeSubstitution'
|
||||||
|
| 'Literal'
|
||||||
|
| 'NonCodeNode'
|
||||||
|
| 'UnaryExpression'
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { parse, initPromise } from './wasm'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -7,13 +8,12 @@ describe('testing artifacts', () => {
|
|||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
test('sketch artifacts', async () => {
|
test('sketch artifacts', async () => {
|
||||||
const code = `
|
const code = `
|
||||||
const mySketch001 = startSketchOn('XY')
|
const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const shown = programMemory?.return?.map(
|
const shown = programMemory?.return?.map(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -28,7 +28,7 @@ show(mySketch001)`
|
|||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
sourceRange: [46, 71],
|
sourceRange: [21, 42],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
@ -38,7 +38,7 @@ show(mySketch001)`
|
|||||||
to: [-1.59, -1.54],
|
to: [-1.59, -1.54],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [77, 102],
|
sourceRange: [48, 73],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -48,7 +48,7 @@ show(mySketch001)`
|
|||||||
from: [-1.59, -1.54],
|
from: [-1.59, -1.54],
|
||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [108, 132],
|
sourceRange: [79, 103],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -56,22 +56,20 @@ show(mySketch001)`
|
|||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
planeId: expect.any(String),
|
__meta: [{ sourceRange: [21, 42] }],
|
||||||
__meta: [{ sourceRange: [46, 71] }],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
test('extrude artifacts', async () => {
|
test('extrude artifacts', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = `
|
const code = `
|
||||||
const mySketch001 = startSketchOn('XY')
|
const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const shown = programMemory?.return?.map(
|
const shown = programMemory?.return?.map(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -85,7 +83,7 @@ show(mySketch001)`
|
|||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [{ sourceRange: [46, 71] }],
|
__meta: [{ sourceRange: [21, 42] }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -93,8 +91,7 @@ show(mySketch001)`
|
|||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
// TODO #153 in order for getExtrudeWallTransform to work we need to query the engine for the location of a face.
|
// TODO #153 in order for getExtrudeWallTransform to work we need to query the engine for the location of a face.
|
||||||
const code = `
|
const code = `
|
||||||
const sk1 = startSketchOn('XY')
|
const sk1 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-2.5, 0], %)
|
|> lineTo([-2.5, 0], %)
|
||||||
|> lineTo({ to: [0, 10], tag: "p" }, %)
|
|> lineTo({ to: [0, 10], tag: "p" }, %)
|
||||||
|> lineTo([2.5, 0], %)
|
|> lineTo([2.5, 0], %)
|
||||||
@ -103,8 +100,7 @@ const sk1 = startSketchOn('XY')
|
|||||||
// |> ry(5, %)
|
// |> ry(5, %)
|
||||||
const theExtrude = extrude(2, sk1)
|
const theExtrude = extrude(2, sk1)
|
||||||
// const theTransf = getExtrudeWallTransform('p', theExtrude)
|
// const theTransf = getExtrudeWallTransform('p', theExtrude)
|
||||||
const sk2 = startSketchOn('XY')
|
const sk2 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([-2.5, 0], %)
|
|> lineTo([-2.5, 0], %)
|
||||||
|> lineTo({ to: [0, 3], tag: "p" }, %)
|
|> lineTo({ to: [0, 3], tag: "p" }, %)
|
||||||
|> lineTo([2.5, 0], %)
|
|> lineTo([2.5, 0], %)
|
||||||
@ -113,7 +109,7 @@ const sk2 = startSketchOn('XY')
|
|||||||
|
|
||||||
|
|
||||||
show(theExtrude, sk2)`
|
show(theExtrude, sk2)`
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const geos = programMemory?.return?.map(
|
const geos = programMemory?.return?.map(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -127,7 +123,7 @@ show(theExtrude, sk2)`
|
|||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [{ sourceRange: [38, 63] }],
|
__meta: [{ sourceRange: [13, 34] }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ExtrudeGroup',
|
type: 'ExtrudeGroup',
|
||||||
@ -136,7 +132,7 @@ show(theExtrude, sk2)`
|
|||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [{ sourceRange: [356, 381] }],
|
__meta: [{ sourceRange: [302, 323] }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export class KCLUndefinedValueError extends KCLError {
|
|||||||
* Currently the diagnostics are all errors, but in the future they could include lints.
|
* Currently the diagnostics are all errors, but in the future they could include lints.
|
||||||
* */
|
* */
|
||||||
export function kclErrToDiagnostic(errors: KCLError[]): Diagnostic[] {
|
export function kclErrToDiagnostic(errors: KCLError[]): Diagnostic[] {
|
||||||
return errors?.flatMap((err) => {
|
return errors.flatMap((err) => {
|
||||||
return err.sourceRanges.map(([from, to]) => {
|
return err.sourceRanges.map(([from, to]) => {
|
||||||
return { from, to, message: err.msg, severity: 'error' }
|
return { from, to, message: err.msg, severity: 'error' }
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { parse, ProgramMemory, SketchGroup, initPromise } from './wasm'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
|
import { ProgramMemory, SketchGroup } from './executor'
|
||||||
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
|
import { vi } from 'vitest'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -41,8 +44,7 @@ const newVar = myVar + 1`
|
|||||||
expect(root.magicNum.value).toBe(69)
|
expect(root.magicNum.value).toBe(69)
|
||||||
})
|
})
|
||||||
it('sketch declaration', async () => {
|
it('sketch declaration', async () => {
|
||||||
let code = `const mySketch = startSketchOn('XY')
|
let code = `const mySketch = startSketchAt([0,0])
|
||||||
|> startProfileAt([0,0], %)
|
|
||||||
|> lineTo({to: [0,2], tag: "myPath"}, %)
|
|> lineTo({to: [0,2], tag: "myPath"}, %)
|
||||||
|> lineTo([2,3], %)
|
|> lineTo([2,3], %)
|
||||||
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
||||||
@ -58,7 +60,7 @@ show(mySketch)
|
|||||||
to: [0, 2],
|
to: [0, 2],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [72, 109],
|
sourceRange: [43, 80],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
name: 'myPath',
|
name: 'myPath',
|
||||||
@ -69,7 +71,7 @@ show(mySketch)
|
|||||||
from: [0, 2],
|
from: [0, 2],
|
||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [115, 131],
|
sourceRange: [86, 102],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -78,7 +80,7 @@ show(mySketch)
|
|||||||
to: [5, -1],
|
to: [5, -1],
|
||||||
from: [2, 3],
|
from: [2, 3],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [137, 180],
|
sourceRange: [108, 151],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
name: 'rightPath',
|
name: 'rightPath',
|
||||||
@ -88,8 +90,8 @@ show(mySketch)
|
|||||||
expect(_return).toEqual([
|
expect(_return).toEqual([
|
||||||
{
|
{
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
start: 203,
|
start: 174,
|
||||||
end: 211,
|
end: 182,
|
||||||
name: 'mySketch',
|
name: 'mySketch',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@ -133,8 +135,7 @@ show(mySketch)
|
|||||||
it('execute pipe sketch into call expression', async () => {
|
it('execute pipe sketch into call expression', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = [
|
const code = [
|
||||||
"const mySk1 = startSketchOn('XY')",
|
'const mySk1 = startSketchAt([0,0])',
|
||||||
' |> startProfileAt([0,0], %)',
|
|
||||||
' |> lineTo([1,1], %)',
|
' |> lineTo([1,1], %)',
|
||||||
' |> lineTo({to: [0, 1], tag: "myPath"}, %)',
|
' |> lineTo({to: [0, 1], tag: "myPath"}, %)',
|
||||||
' |> lineTo([1,1], %)',
|
' |> lineTo([1,1], %)',
|
||||||
@ -149,7 +150,7 @@ show(mySketch)
|
|||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
sourceRange: [39, 63],
|
sourceRange: [14, 34],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
@ -159,7 +160,7 @@ show(mySketch)
|
|||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [69, 85],
|
sourceRange: [40, 56],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -168,7 +169,7 @@ show(mySketch)
|
|||||||
to: [0, 1],
|
to: [0, 1],
|
||||||
from: [1, 1],
|
from: [1, 1],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [91, 129],
|
sourceRange: [62, 100],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
name: 'myPath',
|
name: 'myPath',
|
||||||
@ -179,7 +180,7 @@ show(mySketch)
|
|||||||
from: [0, 1],
|
from: [0, 1],
|
||||||
name: '',
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [135, 151],
|
sourceRange: [106, 122],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -187,8 +188,7 @@ show(mySketch)
|
|||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
planeId: expect.any(String),
|
__meta: [{ sourceRange: [14, 34] }],
|
||||||
__meta: [{ sourceRange: [39, 63] }],
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('execute array expression', async () => {
|
it('execute array expression', async () => {
|
||||||
@ -332,8 +332,7 @@ describe('testing math operators', () => {
|
|||||||
})
|
})
|
||||||
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
|
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
|
||||||
const code = [
|
const code = [
|
||||||
"const part001 = startSketchOn('XY')",
|
'const part001 = startSketchAt([0, 0])',
|
||||||
' |> startProfileAt([0, 0], %)',
|
|
||||||
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
|
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
@ -345,8 +344,7 @@ describe('testing math operators', () => {
|
|||||||
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
||||||
const code = [
|
const code = [
|
||||||
`const myVar = 3`,
|
`const myVar = 3`,
|
||||||
`const part001 = startSketchOn('XY')`,
|
`const part001 = startSketchAt([0, 0])`,
|
||||||
` |> startProfileAt([0, 0], %)`,
|
|
||||||
` |> line({ to: [3, 4], tag: 'seg01' }, %)`,
|
` |> line({ to: [3, 4], tag: 'seg01' }, %)`,
|
||||||
` |> line([`,
|
` |> line([`,
|
||||||
` min(segLen('seg01', %), myVar),`,
|
` min(segLen('seg01', %), myVar),`,
|
||||||
@ -382,8 +380,7 @@ describe('testing math operators', () => {
|
|||||||
describe('Testing Errors', () => {
|
describe('Testing Errors', () => {
|
||||||
it('should throw an error when a variable is not defined', async () => {
|
it('should throw an error when a variable is not defined', async () => {
|
||||||
const code = `const myVar = 5
|
const code = `const myVar = 5
|
||||||
const theExtrude = startSketchOn('XY')
|
const theExtrude = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line([-2.4, 5], %)
|
|> line([-2.4, 5], %)
|
||||||
|> line([-0.76], myVarZ, %)
|
|> line([-0.76], myVarZ, %)
|
||||||
|> line([5,5], %)
|
|> line([5,5], %)
|
||||||
@ -394,7 +391,7 @@ show(theExtrude)`
|
|||||||
new KCLError(
|
new KCLError(
|
||||||
'undefined_value',
|
'undefined_value',
|
||||||
'memory item key `myVarZ` is not defined',
|
'memory item key `myVarZ` is not defined',
|
||||||
[[129, 135]]
|
[[100, 106]]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -406,7 +403,7 @@ async function exe(
|
|||||||
code: string,
|
code: string,
|
||||||
programMemory: ProgramMemory = { root: {}, return: null }
|
programMemory: ProgramMemory = { root: {}, return: null }
|
||||||
) {
|
) {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
|
|
||||||
const result = await enginelessExecutor(ast, programMemory)
|
const result = await enginelessExecutor(ast, programMemory)
|
||||||
return result
|
return result
|
||||||
|
|||||||
81
src/lang/executor.ts
Normal file
81
src/lang/executor.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
|
import {
|
||||||
|
EngineCommandManager,
|
||||||
|
ArtifactMap,
|
||||||
|
SourceRangeMap,
|
||||||
|
} from './std/engineConnection'
|
||||||
|
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||||
|
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||||
|
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
import { KCLError } from './errors'
|
||||||
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
|
import { rangeTypeFix } from './abstractSyntaxTree'
|
||||||
|
|
||||||
|
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
|
||||||
|
export type { Position } from '../wasm-lib/kcl/bindings/Position'
|
||||||
|
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
|
||||||
|
export type { Path } from '../wasm-lib/kcl/bindings/Path'
|
||||||
|
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||||
|
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||||
|
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||||
|
|
||||||
|
export type PathToNode = [string | number, string][]
|
||||||
|
|
||||||
|
interface Memory {
|
||||||
|
[key: string]: MemoryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramMemory {
|
||||||
|
root: Memory
|
||||||
|
return: ProgramReturn | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const executor = async (
|
||||||
|
node: Program,
|
||||||
|
programMemory: ProgramMemory = { root: {}, return: null },
|
||||||
|
engineCommandManager: EngineCommandManager,
|
||||||
|
// work around while the gemotry is still be stored on the frontend
|
||||||
|
// will be removed when the stream UI is added.
|
||||||
|
tempMapCallback: (a: {
|
||||||
|
artifactMap: ArtifactMap
|
||||||
|
sourceRangeMap: SourceRangeMap
|
||||||
|
}) => void = () => {}
|
||||||
|
): Promise<ProgramMemory> => {
|
||||||
|
engineCommandManager.startNewSession()
|
||||||
|
const _programMemory = await _executor(
|
||||||
|
node,
|
||||||
|
programMemory,
|
||||||
|
engineCommandManager
|
||||||
|
)
|
||||||
|
const { artifactMap, sourceRangeMap } =
|
||||||
|
await engineCommandManager.waitForAllCommands(node, _programMemory)
|
||||||
|
tempMapCallback({ artifactMap, sourceRangeMap })
|
||||||
|
|
||||||
|
engineCommandManager.endSession()
|
||||||
|
return _programMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _executor = async (
|
||||||
|
node: Program,
|
||||||
|
programMemory: ProgramMemory = { root: {}, return: null },
|
||||||
|
engineCommandManager: EngineCommandManager
|
||||||
|
): Promise<ProgramMemory> => {
|
||||||
|
try {
|
||||||
|
const memory: ProgramMemory = await execute_wasm(
|
||||||
|
JSON.stringify(node),
|
||||||
|
JSON.stringify(programMemory),
|
||||||
|
engineCommandManager
|
||||||
|
)
|
||||||
|
return memory
|
||||||
|
} catch (e: any) {
|
||||||
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
|
const kclError = new KCLError(
|
||||||
|
parsed.kind,
|
||||||
|
parsed.msg,
|
||||||
|
rangeTypeFix(parsed.sourceRanges)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(kclError)
|
||||||
|
throw kclError
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
||||||
import { Identifier, parse, initPromise } from './wasm'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -19,88 +20,11 @@ const sk3 = startSketchAt([0, 0])
|
|||||||
lineToSubstringIndex + subStr.length,
|
lineToSubstringIndex + subStr.length,
|
||||||
]
|
]
|
||||||
|
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||||
const { node } = getNodeFromPath<any>(ast, nodePath)
|
const { node } = getNodeFromPath<any>(ast, nodePath)
|
||||||
|
|
||||||
expect([node.start, node.end]).toEqual(sourceRange)
|
expect([node.start, node.end]).toEqual(sourceRange)
|
||||||
expect(node.type).toBe('CallExpression')
|
expect(node.type).toBe('CallExpression')
|
||||||
})
|
})
|
||||||
it('gets path right for function definition params', () => {
|
|
||||||
const code = `fn cube = (pos, scale) => {
|
|
||||||
const sg = startSketchAt(pos)
|
|
||||||
|> line([0, scale], %)
|
|
||||||
|> line([scale, 0], %)
|
|
||||||
|> line([0, -scale], %)
|
|
||||||
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const b1 = cube([0,0], 10)`
|
|
||||||
const subStr = 'pos, scale'
|
|
||||||
const subStrIndex = code.indexOf(subStr)
|
|
||||||
const sourceRange: [number, number] = [
|
|
||||||
subStrIndex,
|
|
||||||
subStrIndex + 'pos'.length,
|
|
||||||
]
|
|
||||||
|
|
||||||
const ast = parse(code)
|
|
||||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
|
||||||
const node = getNodeFromPath<Identifier>(ast, nodePath).node
|
|
||||||
|
|
||||||
expect(nodePath).toEqual([
|
|
||||||
['body', ''],
|
|
||||||
[0, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', ''],
|
|
||||||
['params', 'FunctionExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
])
|
|
||||||
expect(node.type).toBe('Identifier')
|
|
||||||
expect(node.name).toBe('pos')
|
|
||||||
})
|
|
||||||
it('gets path right for deep within function definition body', () => {
|
|
||||||
const code = `fn cube = (pos, scale) => {
|
|
||||||
const sg = startSketchAt(pos)
|
|
||||||
|> line([0, scale], %)
|
|
||||||
|> line([scale, 0], %)
|
|
||||||
|> line([0, -scale], %)
|
|
||||||
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const b1 = cube([0,0], 10)`
|
|
||||||
const subStr = 'scale, 0'
|
|
||||||
const subStrIndex = code.indexOf(subStr)
|
|
||||||
const sourceRange: [number, number] = [
|
|
||||||
subStrIndex,
|
|
||||||
subStrIndex + 'scale'.length,
|
|
||||||
]
|
|
||||||
|
|
||||||
const ast = parse(code)
|
|
||||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
|
||||||
const node = getNodeFromPath<Identifier>(ast, nodePath).node
|
|
||||||
expect(nodePath).toEqual([
|
|
||||||
['body', ''],
|
|
||||||
[0, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', ''],
|
|
||||||
['body', 'FunctionExpression'],
|
|
||||||
['body', 'FunctionExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', ''],
|
|
||||||
['body', 'PipeExpression'],
|
|
||||||
[2, 'index'],
|
|
||||||
['arguments', 'CallExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
['elements', 'ArrayExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
])
|
|
||||||
expect(node.type).toBe('Identifier')
|
|
||||||
expect(node.name).toBe('scale')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { parse, recast, initPromise } from './wasm'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
giveSketchFnCallTag,
|
giveSketchFnCallTag,
|
||||||
moveValueIntoNewVariable,
|
moveValueIntoNewVariable,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
|
import { recast } from './recast'
|
||||||
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -104,13 +106,13 @@ describe('Testing addSketchTo', () => {
|
|||||||
body: [],
|
body: [],
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
nonCodeMeta: { nonCodeNodes: {}, start: [] },
|
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||||
},
|
},
|
||||||
'yz'
|
'yz'
|
||||||
)
|
)
|
||||||
const str = recast(result.modifiedAst)
|
const str = recast(result.modifiedAst)
|
||||||
expect(str).toBe(`const part001 = startSketchOn('YZ')
|
expect(str).toBe(`const part001 = startSketchAt('default')
|
||||||
|> startProfileAt('default', %)
|
|> ry(90, %)
|
||||||
|> line('default', %)
|
|> line('default', %)
|
||||||
show(part001)
|
show(part001)
|
||||||
`)
|
`)
|
||||||
@ -124,7 +126,7 @@ function giveSketchFnCallTagTestHelper(
|
|||||||
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
|
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
|
||||||
// this wrapper changes the input and output to code
|
// this wrapper changes the input and output to code
|
||||||
// making it more of an integration test, but easier to read the test intention is the goal
|
// making it more of an integration test, but easier to read the test intention is the goal
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const start = code.indexOf(searchStr)
|
const start = code.indexOf(searchStr)
|
||||||
const range: [number, number] = [start, start + searchStr.length]
|
const range: [number, number] = [start, start + searchStr.length]
|
||||||
const { modifiedAst, tag, isTagExisting } = giveSketchFnCallTag(ast, range)
|
const { modifiedAst, tag, isTagExisting } = giveSketchFnCallTag(ast, range)
|
||||||
@ -133,8 +135,7 @@ function giveSketchFnCallTagTestHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Testing giveSketchFnCallTag', () => {
|
describe('Testing giveSketchFnCallTag', () => {
|
||||||
const code = `const part001 = startSketchOn('XY')
|
const code = `const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line([-2.57, -0.13], %)
|
|> line([-2.57, -0.13], %)
|
||||||
|> line([0, 0.83], %)
|
|> line([0, 0.83], %)
|
||||||
|> line([0.82, 0.34], %)
|
|> line([0.82, 0.34], %)
|
||||||
@ -186,8 +187,7 @@ fn ghi = (x) => {
|
|||||||
const abc = 3
|
const abc = 3
|
||||||
const identifierGuy = 5
|
const identifierGuy = 5
|
||||||
const yo = 5 + 6
|
const yo = 5 + 6
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([-1.2, 4.83])
|
||||||
|> startProfileAt([-1.2, 4.83], %)
|
|
||||||
|> line([2.8, 0], %)
|
|> line([2.8, 0], %)
|
||||||
|> angledLine([100 + 100, 3.09], %)
|
|> angledLine([100 + 100, 3.09], %)
|
||||||
|> angledLine([abc, 3.09], %)
|
|> angledLine([abc, 3.09], %)
|
||||||
@ -197,7 +197,7 @@ const part001 = startSketchOn('XY')
|
|||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('should move a binary expression into a new variable', async () => {
|
it('should move a binary expression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('100 + 100') + 1
|
const startIndex = code.indexOf('100 + 100') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -211,7 +211,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a value into a new variable', async () => {
|
it('should move a value into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('2.8') + 1
|
const startIndex = code.indexOf('2.8') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -225,7 +225,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`line([newVar, 0], %)`)
|
expect(newCode).toContain(`line([newVar, 0], %)`)
|
||||||
})
|
})
|
||||||
it('should move a callExpression into a new variable', async () => {
|
it('should move a callExpression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('def(')
|
const startIndex = code.indexOf('def(')
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -239,7 +239,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a binary expression with call expression into a new variable', async () => {
|
it('should move a binary expression with call expression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('jkl(') + 1
|
const startIndex = code.indexOf('jkl(') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -253,7 +253,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a identifier into a new variable', async () => {
|
it('should move a identifier into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('identifierGuy +') + 1
|
const startIndex = code.indexOf('identifierGuy +') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { ToolTip } from '../useStore'
|
import { Selection, ToolTip } from '../useStore'
|
||||||
import { Selection } from 'lib/selections'
|
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -15,15 +14,14 @@ import {
|
|||||||
ObjectExpression,
|
ObjectExpression,
|
||||||
UnaryExpression,
|
UnaryExpression,
|
||||||
BinaryExpression,
|
BinaryExpression,
|
||||||
PathToNode,
|
} from './abstractSyntaxTreeTypes'
|
||||||
ProgramMemory,
|
|
||||||
} from './wasm'
|
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
|
import { PathToNode, ProgramMemory } from './executor'
|
||||||
import {
|
import {
|
||||||
addTagForSketchOnFace,
|
addTagForSketchOnFace,
|
||||||
getFirstArg,
|
getFirstArg,
|
||||||
@ -33,26 +31,21 @@ import { isLiteralArrayOrStatic } from './std/sketchcombos'
|
|||||||
|
|
||||||
export function addStartSketch(
|
export function addStartSketch(
|
||||||
node: Program,
|
node: Program,
|
||||||
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz',
|
|
||||||
start: [number, number],
|
start: [number, number],
|
||||||
end: [number, number]
|
end: [number, number]
|
||||||
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const _name = findUniqueName(node, 'part')
|
const _name = findUniqueName(node, 'part')
|
||||||
|
|
||||||
const startSketchOn = createCallExpressionStdLib('startSketchOn', [
|
const startSketchAt = createCallExpression('startSketchAt', [
|
||||||
createLiteral(axis.toUpperCase()),
|
|
||||||
])
|
|
||||||
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
|
||||||
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
|
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
|
||||||
createPipeSubstitution(),
|
|
||||||
])
|
])
|
||||||
const initialLineTo = createCallExpression('line', [
|
const initialLineTo = createCallExpression('line', [
|
||||||
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
|
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const pipeBody = [startSketchOn, startProfileAt, initialLineTo]
|
const pipeBody = [startSketchAt, initialLineTo]
|
||||||
|
|
||||||
const variableDeclaration = createVariableDeclaration(
|
const variableDeclaration = createVariableDeclaration(
|
||||||
_name,
|
_name,
|
||||||
@ -85,11 +78,11 @@ export function addSketchTo(
|
|||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const _name = name || findUniqueName(node, 'part')
|
const _name = name || findUniqueName(node, 'part')
|
||||||
|
|
||||||
const startSketchOn = createCallExpressionStdLib('startSketchOn', [
|
const startSketchAt = createCallExpressionStdLib('startSketchAt', [
|
||||||
createLiteral(axis.toUpperCase()),
|
|
||||||
])
|
|
||||||
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
|
||||||
createLiteral('default'),
|
createLiteral('default'),
|
||||||
|
])
|
||||||
|
const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
|
||||||
|
createLiteral(90),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
const initialLineTo = createCallExpressionStdLib('line', [
|
const initialLineTo = createCallExpressionStdLib('line', [
|
||||||
@ -97,7 +90,10 @@ export function addSketchTo(
|
|||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const pipeBody = [startSketchOn, startProfileAt, initialLineTo]
|
const pipeBody =
|
||||||
|
axis !== 'xy'
|
||||||
|
? [startSketchAt, rotate, initialLineTo]
|
||||||
|
: [startSketchAt, initialLineTo]
|
||||||
|
|
||||||
const variableDeclaration = createVariableDeclaration(
|
const variableDeclaration = createVariableDeclaration(
|
||||||
_name,
|
_name,
|
||||||
@ -325,7 +321,7 @@ export function extrudeSketch(
|
|||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
modifiedAst: node,
|
modifiedAst: addToShow(_node, name),
|
||||||
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
||||||
pathToExtrudeArg,
|
pathToExtrudeArg,
|
||||||
}
|
}
|
||||||
@ -541,7 +537,7 @@ export function createPipeExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
body,
|
body,
|
||||||
nonCodeMeta: { nonCodeNodes: {}, start: [] },
|
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { parse, recast, initPromise } from './wasm'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
isTypeInValue,
|
isTypeInValue,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
doesPipeHaveCallExp,
|
|
||||||
hasExtrudeSketchGroup,
|
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
createLiteral,
|
createLiteral,
|
||||||
createPipeSubstitution,
|
createPipeSubstitution,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
|
import { recast } from './recast'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -28,8 +28,7 @@ const halfArmAngle = armAngle / 2
|
|||||||
const arrExpShouldNotBeIncluded = [1, 2, 3]
|
const arrExpShouldNotBeIncluded = [1, 2, 3]
|
||||||
const objExpShouldNotBeIncluded = { a: 1, b: 2, c: 3 }
|
const objExpShouldNotBeIncluded = { a: 1, b: 2, c: 3 }
|
||||||
|
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> yLineTo(1, %)
|
|> yLineTo(1, %)
|
||||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ const variableBelowShouldNotBeIncluded = 3
|
|||||||
|
|
||||||
show(part001)`
|
show(part001)`
|
||||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
|
|
||||||
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
||||||
@ -60,8 +59,7 @@ show(part001)`
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('testing argIsNotIdentifier', () => {
|
describe('testing argIsNotIdentifier', () => {
|
||||||
const code = `const part001 = startSketchOn('XY')
|
const code = `const part001 = startSketchAt([-1.2, 4.83])
|
||||||
|> startProfileAt([-1.2, 4.83], %)
|
|
||||||
|> line([2.8, 0], %)
|
|> line([2.8, 0], %)
|
||||||
|> angledLine([100 + 100, 3.09], %)
|
|> angledLine([100 + 100, 3.09], %)
|
||||||
|> angledLine([abc, 3.09], %)
|
|> angledLine([abc, 3.09], %)
|
||||||
@ -72,7 +70,7 @@ const yo = 5 + 6
|
|||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('find a safe binaryExpression', () => {
|
it('find a safe binaryExpression', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('100 + 100') + 2
|
const rangeStart = code.indexOf('100 + 100') + 2
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -86,7 +84,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find a safe Identifier', () => {
|
it('find a safe Identifier', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('abc')
|
const rangeStart = code.indexOf('abc')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -94,7 +92,7 @@ show(part001)`
|
|||||||
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
|
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
|
||||||
})
|
})
|
||||||
it('find a safe CallExpression', () => {
|
it('find a safe CallExpression', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('def')
|
const rangeStart = code.indexOf('def')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -108,7 +106,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('ghi')
|
const rangeStart = code.indexOf('ghi')
|
||||||
const range: [number, number] = [rangeStart, rangeStart]
|
const range: [number, number] = [rangeStart, rangeStart]
|
||||||
const result = isNodeSafeToReplace(ast, range)
|
const result = isNodeSafeToReplace(ast, range)
|
||||||
@ -117,7 +115,7 @@ show(part001)`
|
|||||||
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
|
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
|
||||||
})
|
})
|
||||||
it('find an UNsafe Identifier, as it is a callee', () => {
|
it('find an UNsafe Identifier, as it is a callee', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('ine([2.8,')
|
const rangeStart = code.indexOf('ine([2.8,')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(false)
|
expect(result.isSafe).toBe(false)
|
||||||
@ -127,7 +125,7 @@ show(part001)`
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('5 + 6') + 1
|
const rangeStart = code.indexOf('5 + 6') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -141,7 +139,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`const yo = replaceName`)
|
expect(outCode).toContain(`const yo = replaceName`)
|
||||||
})
|
})
|
||||||
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('jkl') + 1
|
const rangeStart = code.indexOf('jkl') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -157,7 +155,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find a safe BinaryExpression within a CallExpression', () => {
|
it('find a safe BinaryExpression within a CallExpression', () => {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('identifierGuy') + 1
|
const rangeStart = code.indexOf('identifierGuy') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -198,15 +196,14 @@ show(part001)`
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('testing getNodePathFromSourceRange', () => {
|
describe('testing getNodePathFromSourceRange', () => {
|
||||||
const code = `const part001 = startSketchOn('XY')
|
const code = `const part001 = startSketchAt([0.39, -0.05])
|
||||||
|> startProfileAt([0.39, -0.05], %)
|
|
||||||
|> line([0.94, 2.61], %)
|
|> line([0.94, 2.61], %)
|
||||||
|> line([-0.21, -1.4], %)
|
|> line([-0.21, -1.4], %)
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('finds the second line when cursor is put at the end', () => {
|
it('finds the second line when cursor is put at the end', () => {
|
||||||
const searchLn = `line([0.94, 2.61], %)`
|
const searchLn = `line([0.94, 2.61], %)`
|
||||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
['body', ''],
|
['body', ''],
|
||||||
@ -215,13 +212,13 @@ show(part001)`
|
|||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
['init', ''],
|
['init', ''],
|
||||||
['body', 'PipeExpression'],
|
['body', 'PipeExpression'],
|
||||||
[2, 'index'],
|
[1, 'index'],
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
it('finds the last line when cursor is put at the end', () => {
|
it('finds the last line when cursor is put at the end', () => {
|
||||||
const searchLn = `line([-0.21, -1.4], %)`
|
const searchLn = `line([-0.21, -1.4], %)`
|
||||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
const expected = [
|
const expected = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
@ -230,7 +227,7 @@ show(part001)`
|
|||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
['init', ''],
|
['init', ''],
|
||||||
['body', 'PipeExpression'],
|
['body', 'PipeExpression'],
|
||||||
[3, 'index'],
|
[2, 'index'],
|
||||||
]
|
]
|
||||||
expect(result).toEqual(expected)
|
expect(result).toEqual(expected)
|
||||||
// expect similar result for start of line
|
// expect similar result for start of line
|
||||||
@ -248,114 +245,3 @@ show(part001)`
|
|||||||
expect(selectWholeThing).toEqual(expected)
|
expect(selectWholeThing).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing doesPipeHave', () => {
|
|
||||||
it('finds close', () => {
|
|
||||||
const exampleCode = `const length001 = 2
|
|
||||||
const part001 = startSketchAt([-1.41, 3.46])
|
|
||||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
|
||||||
|> angledLine([-35, length001], %)
|
|
||||||
|> line([-3.22, -7.36], %)
|
|
||||||
|> angledLine([-175, segLen('seg01', %)], %)
|
|
||||||
|> close(%)
|
|
||||||
`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const result = doesPipeHaveCallExp({
|
|
||||||
calleeName: 'close',
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [100, 101] },
|
|
||||||
})
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
it('finds extrude', () => {
|
|
||||||
const exampleCode = `const length001 = 2
|
|
||||||
const part001 = startSketchAt([-1.41, 3.46])
|
|
||||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
|
||||||
|> angledLine([-35, length001], %)
|
|
||||||
|> line([-3.22, -7.36], %)
|
|
||||||
|> angledLine([-175, segLen('seg01', %)], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(1, %)
|
|
||||||
`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const result = doesPipeHaveCallExp({
|
|
||||||
calleeName: 'extrude',
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [100, 101] },
|
|
||||||
})
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
it('does NOT find close', () => {
|
|
||||||
const exampleCode = `const length001 = 2
|
|
||||||
const part001 = startSketchAt([-1.41, 3.46])
|
|
||||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
|
||||||
|> angledLine([-35, length001], %)
|
|
||||||
|> line([-3.22, -7.36], %)
|
|
||||||
|> angledLine([-175, segLen('seg01', %)], %)
|
|
||||||
`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const result = doesPipeHaveCallExp({
|
|
||||||
calleeName: 'close',
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [100, 101] },
|
|
||||||
})
|
|
||||||
expect(result).toEqual(false)
|
|
||||||
})
|
|
||||||
it('returns false if not a pipe', () => {
|
|
||||||
const exampleCode = `const length001 = 2`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const result = doesPipeHaveCallExp({
|
|
||||||
calleeName: 'close',
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [9, 10] },
|
|
||||||
})
|
|
||||||
expect(result).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('testing hasExtrudeSketchGroup', () => {
|
|
||||||
it('find sketch group', async () => {
|
|
||||||
const exampleCode = `const length001 = 2
|
|
||||||
const part001 = startSketchAt([-1.41, 3.46])
|
|
||||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
|
||||||
|> angledLine([-35, length001], %)
|
|
||||||
|> line([-3.22, -7.36], %)
|
|
||||||
|> angledLine([-175, segLen('seg01', %)], %)`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
|
||||||
const result = hasExtrudeSketchGroup({
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [100, 101] },
|
|
||||||
programMemory,
|
|
||||||
})
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
it('find extrude group', async () => {
|
|
||||||
const exampleCode = `const length001 = 2
|
|
||||||
const part001 = startSketchAt([-1.41, 3.46])
|
|
||||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
|
||||||
|> angledLine([-35, length001], %)
|
|
||||||
|> line([-3.22, -7.36], %)
|
|
||||||
|> angledLine([-175, segLen('seg01', %)], %)
|
|
||||||
|> extrude(1, %)`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
|
||||||
const result = hasExtrudeSketchGroup({
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [100, 101] },
|
|
||||||
programMemory,
|
|
||||||
})
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
it('finds nothing', async () => {
|
|
||||||
const exampleCode = `const length001 = 2`
|
|
||||||
const ast = parse(exampleCode)
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
|
||||||
const result = hasExtrudeSketchGroup({
|
|
||||||
ast,
|
|
||||||
selection: { type: 'default', range: [10, 11] },
|
|
||||||
programMemory,
|
|
||||||
})
|
|
||||||
expect(result).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ToolTip } from '../useStore'
|
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
|
||||||
import { Selection } from 'lib/selections'
|
import { Selection, ToolTip } from '../useStore'
|
||||||
import {
|
import {
|
||||||
BinaryExpression,
|
BinaryExpression,
|
||||||
Program,
|
Program,
|
||||||
@ -10,12 +10,7 @@ import {
|
|||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
ReturnStatement,
|
ReturnStatement,
|
||||||
ArrayExpression,
|
ArrayExpression,
|
||||||
PathToNode,
|
} from './abstractSyntaxTreeTypes'
|
||||||
ProgramMemory,
|
|
||||||
SketchGroup,
|
|
||||||
SourceRange,
|
|
||||||
PipeExpression,
|
|
||||||
} from './wasm'
|
|
||||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||||
import { getAngle } from '../lib/utils'
|
import { getAngle } from '../lib/utils'
|
||||||
@ -244,29 +239,7 @@ function moreNodePathFromSourceRange(
|
|||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
if (_node.type === 'FunctionExpression' && isInRange) {
|
console.error('not implemented')
|
||||||
for (let i = 0; i < _node.params.length; i++) {
|
|
||||||
const param = _node.params[i]
|
|
||||||
if (param.start <= start && param.end >= end) {
|
|
||||||
path.push(['params', 'FunctionExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(param, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_node.body.start <= start && _node.body.end >= end) {
|
|
||||||
path.push(['body', 'FunctionExpression'])
|
|
||||||
const fnBody = _node.body.body
|
|
||||||
for (let i = 0; i < fnBody.length; i++) {
|
|
||||||
const statement = fnBody[i]
|
|
||||||
if (statement.start <= start && statement.end >= end) {
|
|
||||||
path.push(['body', 'FunctionExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.error('not implemented: ' + node.type)
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,47 +486,3 @@ export function isLinesParallelAndConstrained(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesPipeHaveCallExp({
|
|
||||||
ast,
|
|
||||||
selection,
|
|
||||||
calleeName,
|
|
||||||
}: {
|
|
||||||
calleeName: string
|
|
||||||
ast: Program
|
|
||||||
selection: Selection
|
|
||||||
}): boolean {
|
|
||||||
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
|
|
||||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
'PipeExpression'
|
|
||||||
).node
|
|
||||||
if (pipeExpression.type !== 'PipeExpression') return false
|
|
||||||
return pipeExpression.body.some(
|
|
||||||
(expression) =>
|
|
||||||
expression.type === 'CallExpression' &&
|
|
||||||
expression.callee.name === calleeName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasExtrudeSketchGroup({
|
|
||||||
ast,
|
|
||||||
selection,
|
|
||||||
programMemory,
|
|
||||||
}: {
|
|
||||||
ast: Program
|
|
||||||
selection: Selection
|
|
||||||
programMemory: ProgramMemory
|
|
||||||
}): boolean {
|
|
||||||
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
|
|
||||||
const varDec = getNodeFromPath<VariableDeclaration>(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
'VariableDeclaration'
|
|
||||||
).node
|
|
||||||
if (varDec.type !== 'VariableDeclaration') return false
|
|
||||||
const varName = varDec.declarations[0].id.name
|
|
||||||
const varValue = programMemory?.root[varName]
|
|
||||||
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { parse, Program, recast, initPromise } from './wasm'
|
import { recast } from './recast'
|
||||||
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -272,20 +275,21 @@ const mySk1 = startSketchAt([0, 0])
|
|||||||
`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(`/* comment at start */
|
expect(recasted).toBe(`// comment at start
|
||||||
|
|
||||||
const mySk1 = startSketchAt([0, 0])
|
const mySk1 = startSketchAt([0, 0])
|
||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
// comment here
|
// comment here
|
||||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||||
|> lineTo([1, 1], %) /* and
|
|> lineTo([1, 1], %)
|
||||||
here */
|
/* and
|
||||||
// a comment between pipe expression statements
|
here
|
||||||
|
|
||||||
|
a comment between pipe expression statements */
|
||||||
|> rx(90, %)
|
|> rx(90, %)
|
||||||
// and another with just white space between others below
|
// and another with just white space between others below
|
||||||
|> ry(45, %)
|
|> ry(45, %)
|
||||||
|> rx(45, %)
|
|> rx(45, %)
|
||||||
/* one more for good measure */
|
// one more for good measure
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -362,6 +366,6 @@ describe('it recasts binary expression using brackets where needed', () => {
|
|||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
function code2ast(code: string): { ast: Program } {
|
function code2ast(code: string): { ast: Program } {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
return { ast }
|
return { ast }
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/lang/recast.ts
Normal file
13
src/lang/recast.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
|
import { recast_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
|
||||||
|
export const recast = (ast: Program): string => {
|
||||||
|
try {
|
||||||
|
const s: string = recast_wasm(JSON.stringify(ast))
|
||||||
|
return s
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: do something real with the error.
|
||||||
|
console.log('recast', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/lang/rust.ts
Normal file
20
src/lang/rust.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import init from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
|
||||||
|
const initialise = async () => {
|
||||||
|
const baseUrl =
|
||||||
|
typeof window === 'undefined'
|
||||||
|
? 'http://127.0.0.1:3000'
|
||||||
|
: window.location.origin.includes('tauri://localhost')
|
||||||
|
? 'tauri://localhost'
|
||||||
|
: window.location.origin.includes('localhost')
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: window.location.origin && window.location.origin !== 'null'
|
||||||
|
? window.location.origin
|
||||||
|
: 'http://localhost:3000'
|
||||||
|
const fullUrl = baseUrl + '/wasm_lib_bg.wasm'
|
||||||
|
const input = await fetch(fullUrl)
|
||||||
|
const buffer = await input.arrayBuffer()
|
||||||
|
return init(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initPromise = initialise()
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { SourceRange } from 'lang/wasm'
|
import { ProgramMemory, 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 } from 'env'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { exportSave } from 'lib/exportSave'
|
import { exportSave } from 'lib/exportSave'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||||
|
|
||||||
let lastMessage = ''
|
let lastMessage = ''
|
||||||
|
|
||||||
@ -20,11 +22,6 @@ interface ResultCommand extends CommandInfo {
|
|||||||
type: 'result'
|
type: 'result'
|
||||||
data: any
|
data: any
|
||||||
raw: WebSocketResponse
|
raw: WebSocketResponse
|
||||||
headVertexId?: string
|
|
||||||
}
|
|
||||||
interface FailedCommand extends CommandInfo {
|
|
||||||
type: 'failed'
|
|
||||||
errors: Models['FailureWebSocketResponse_type']['errors']
|
|
||||||
}
|
}
|
||||||
interface PendingCommand extends CommandInfo {
|
interface PendingCommand extends CommandInfo {
|
||||||
type: 'pending'
|
type: 'pending'
|
||||||
@ -33,7 +30,10 @@ interface PendingCommand extends CommandInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtifactMap {
|
export interface ArtifactMap {
|
||||||
[key: string]: ResultCommand | PendingCommand | FailedCommand
|
[key: string]: ResultCommand | PendingCommand
|
||||||
|
}
|
||||||
|
export interface SourceRangeMap {
|
||||||
|
[key: string]: SourceRange
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NewTrackArgs {
|
interface NewTrackArgs {
|
||||||
@ -41,11 +41,6 @@ interface NewTrackArgs {
|
|||||||
mediaStream: MediaStream
|
mediaStream: MediaStream
|
||||||
}
|
}
|
||||||
|
|
||||||
// This looks funny, I know. This is needed because node and the browser
|
|
||||||
// disagree as to the type. In a browser it's a number, but in node it's a
|
|
||||||
// "Timeout".
|
|
||||||
type Timeout = ReturnType<typeof setTimeout>
|
|
||||||
|
|
||||||
type ClientMetrics = Models['ClientMetrics_type']
|
type ClientMetrics = Models['ClientMetrics_type']
|
||||||
|
|
||||||
// EngineConnection encapsulates the connection(s) to the Engine
|
// EngineConnection encapsulates the connection(s) to the Engine
|
||||||
@ -57,9 +52,6 @@ export class EngineConnection {
|
|||||||
unreliableDataChannel?: RTCDataChannel
|
unreliableDataChannel?: RTCDataChannel
|
||||||
|
|
||||||
private ready: boolean
|
private ready: boolean
|
||||||
private connecting: boolean
|
|
||||||
private dead: boolean
|
|
||||||
private failedConnTimeout: Timeout | null
|
|
||||||
|
|
||||||
readonly url: string
|
readonly url: string
|
||||||
private readonly token?: string
|
private readonly token?: string
|
||||||
@ -95,9 +87,6 @@ export class EngineConnection {
|
|||||||
this.url = url
|
this.url = url
|
||||||
this.token = token
|
this.token = token
|
||||||
this.ready = false
|
this.ready = false
|
||||||
this.connecting = false
|
|
||||||
this.dead = false
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
this.onWebsocketOpen = onWebsocketOpen
|
this.onWebsocketOpen = onWebsocketOpen
|
||||||
this.onDataChannelOpen = onDataChannelOpen
|
this.onDataChannelOpen = onDataChannelOpen
|
||||||
this.onEngineConnectionOpen = onEngineConnectionOpen
|
this.onEngineConnectionOpen = onEngineConnectionOpen
|
||||||
@ -108,10 +97,7 @@ export class EngineConnection {
|
|||||||
// TODO(paultag): This ought to be tweakable.
|
// TODO(paultag): This ought to be tweakable.
|
||||||
const pingIntervalMs = 10000
|
const pingIntervalMs = 10000
|
||||||
|
|
||||||
let pingInterval = setInterval(() => {
|
setInterval(() => {
|
||||||
if (this.dead) {
|
|
||||||
clearInterval(pingInterval)
|
|
||||||
}
|
|
||||||
if (this.isReady()) {
|
if (this.isReady()) {
|
||||||
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
// When we're online, every 10 seconds, we'll attempt to put a 'ping'
|
||||||
// command through the WebSocket connection. This will help both ends
|
// command through the WebSocket connection. This will help both ends
|
||||||
@ -120,24 +106,6 @@ export class EngineConnection {
|
|||||||
this.send({ type: 'ping' })
|
this.send({ type: 'ping' })
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
|
||||||
let connectInterval = setInterval(() => {
|
|
||||||
if (this.dead) {
|
|
||||||
clearInterval(connectInterval)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.isReady()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('connecting via retry')
|
|
||||||
this.connect()
|
|
||||||
}, connectionTimeoutMs)
|
|
||||||
}
|
|
||||||
// isConnecting will return true when connect has been called, but the full
|
|
||||||
// WebRTC is not online.
|
|
||||||
isConnecting() {
|
|
||||||
return this.connecting
|
|
||||||
}
|
}
|
||||||
// isReady will return true only when the WebRTC *and* WebSocket connection
|
// isReady will return true only when the WebRTC *and* WebSocket connection
|
||||||
// are connected. During setup, the WebSocket connection comes online first,
|
// are connected. During setup, the WebSocket connection comes online first,
|
||||||
@ -146,10 +114,6 @@ export class EngineConnection {
|
|||||||
isReady() {
|
isReady() {
|
||||||
return this.ready
|
return this.ready
|
||||||
}
|
}
|
||||||
tearDown() {
|
|
||||||
this.dead = true
|
|
||||||
this.close()
|
|
||||||
}
|
|
||||||
// shouldTrace will return true when Sentry should be used to instrument
|
// shouldTrace will return true when Sentry should be used to instrument
|
||||||
// the Engine.
|
// the Engine.
|
||||||
shouldTrace() {
|
shouldTrace() {
|
||||||
@ -161,10 +125,8 @@ export class EngineConnection {
|
|||||||
// This will attempt the full handshake, and retry if the connection
|
// This will attempt the full handshake, and retry if the connection
|
||||||
// did not establish.
|
// did not establish.
|
||||||
connect() {
|
connect() {
|
||||||
console.log('connect was called')
|
// TODO(paultag): make this safe to call multiple times, and figure out
|
||||||
if (this.isConnecting() || this.isReady()) {
|
// when a connection is in progress (state: connecting or something).
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Information on the connect transaction
|
// Information on the connect transaction
|
||||||
|
|
||||||
@ -278,18 +240,14 @@ export class EngineConnection {
|
|||||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||||
|
|
||||||
if (!message.success) {
|
if (!message.success) {
|
||||||
const errorsString = message?.errors
|
|
||||||
?.map((error) => {
|
|
||||||
return ` - ${error.error_code}: ${error.message}`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
if (message.request_id) {
|
if (message.request_id) {
|
||||||
console.error(
|
console.error(`Error in response to request ${message.request_id}:`)
|
||||||
`Error in response to request ${message.request_id}:\n${errorsString}`
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error from server:\n${errorsString}`)
|
console.error(`Error from server:`)
|
||||||
}
|
}
|
||||||
|
message?.errors?.forEach((error) => {
|
||||||
|
console.error(` - ${error.error_code}: ${error.message}`)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,11 +314,6 @@ export class EngineConnection {
|
|||||||
if (this.shouldTrace()) {
|
if (this.shouldTrace()) {
|
||||||
iceSpan.resolve?.()
|
iceSpan.resolve?.()
|
||||||
}
|
}
|
||||||
} else if (this.pc?.iceConnectionState === 'failed') {
|
|
||||||
// failed is a terminal state; let's explicitly kill the
|
|
||||||
// connection to the server at this point.
|
|
||||||
console.log('failed to negotiate ice connection; restarting')
|
|
||||||
this.close()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -405,6 +358,20 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(paultag): This ought to be both controllable, as well as something
|
||||||
|
// like exponential backoff to have some grace on the backend, as well as
|
||||||
|
// fix responsiveness for clients that had a weird network hiccup.
|
||||||
|
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isReady()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('engine connection timeout on connection, retrying')
|
||||||
|
this.close()
|
||||||
|
this.connect()
|
||||||
|
}, connectionTimeoutMs)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('track', (event) => {
|
this.pc.addEventListener('track', (event) => {
|
||||||
@ -450,18 +417,18 @@ export class EngineConnection {
|
|||||||
videoTrackStats.forEach((videoTrackReport) => {
|
videoTrackStats.forEach((videoTrackReport) => {
|
||||||
if (videoTrackReport.type === 'inbound-rtp') {
|
if (videoTrackReport.type === 'inbound-rtp') {
|
||||||
client_metrics.rtc_frames_decoded =
|
client_metrics.rtc_frames_decoded =
|
||||||
videoTrackReport.framesDecoded || 0
|
videoTrackReport.framesDecoded
|
||||||
client_metrics.rtc_frames_dropped =
|
client_metrics.rtc_frames_dropped =
|
||||||
videoTrackReport.framesDropped || 0
|
videoTrackReport.framesDropped
|
||||||
client_metrics.rtc_frames_received =
|
client_metrics.rtc_frames_received =
|
||||||
videoTrackReport.framesReceived || 0
|
videoTrackReport.framesReceived
|
||||||
client_metrics.rtc_frames_per_second =
|
client_metrics.rtc_frames_per_second =
|
||||||
videoTrackReport.framesPerSecond || 0
|
videoTrackReport.framesPerSecond || 0
|
||||||
client_metrics.rtc_freeze_count =
|
client_metrics.rtc_freeze_count =
|
||||||
videoTrackReport.freezeCount || 0
|
videoTrackReport.freezeCount || 0
|
||||||
client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0
|
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
|
||||||
client_metrics.rtc_keyframes_decoded =
|
client_metrics.rtc_keyframes_decoded =
|
||||||
videoTrackReport.keyFramesDecoded || 0
|
videoTrackReport.keyFramesDecoded
|
||||||
client_metrics.rtc_total_freezes_duration_sec =
|
client_metrics.rtc_total_freezes_duration_sec =
|
||||||
videoTrackReport.totalFreezesDuration || 0
|
videoTrackReport.totalFreezesDuration || 0
|
||||||
} else if (videoTrackReport.type === 'transport') {
|
} else if (videoTrackReport.type === 'transport') {
|
||||||
@ -492,11 +459,8 @@ export class EngineConnection {
|
|||||||
|
|
||||||
this.onDataChannelOpen(this)
|
this.onDataChannelOpen(this)
|
||||||
|
|
||||||
this.ready = true
|
|
||||||
this.connecting = false
|
|
||||||
// Do this after we set the connection is ready to avoid errors when
|
|
||||||
// we try to send messages before the connection is ready.
|
|
||||||
this.onEngineConnectionOpen(this)
|
this.onEngineConnectionOpen(this)
|
||||||
|
this.ready = true
|
||||||
})
|
})
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||||
@ -510,22 +474,6 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
|
||||||
|
|
||||||
if (this.failedConnTimeout) {
|
|
||||||
console.log('clearing timeout before set')
|
|
||||||
clearTimeout(this.failedConnTimeout)
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
}
|
|
||||||
console.log('timeout set')
|
|
||||||
this.failedConnTimeout = setTimeout(() => {
|
|
||||||
if (this.isReady()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('engine connection timeout on connection, closing')
|
|
||||||
this.close()
|
|
||||||
}, connectionTimeoutMs)
|
|
||||||
|
|
||||||
this.onConnectionStarted(this)
|
this.onConnectionStarted(this)
|
||||||
}
|
}
|
||||||
unreliableSend(message: object | string) {
|
unreliableSend(message: object | string) {
|
||||||
@ -550,15 +498,9 @@ export class EngineConnection {
|
|||||||
this.pc = undefined
|
this.pc = undefined
|
||||||
this.unreliableDataChannel = undefined
|
this.unreliableDataChannel = undefined
|
||||||
this.webrtcStatsCollector = undefined
|
this.webrtcStatsCollector = undefined
|
||||||
if (this.failedConnTimeout) {
|
|
||||||
console.log('closed timeout in close')
|
|
||||||
clearTimeout(this.failedConnTimeout)
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onClose(this)
|
this.onClose(this)
|
||||||
this.ready = false
|
this.ready = false
|
||||||
this.connecting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -585,17 +527,12 @@ interface Subscription<T extends ModelTypes> {
|
|||||||
|
|
||||||
export class EngineCommandManager {
|
export class EngineCommandManager {
|
||||||
artifactMap: ArtifactMap = {}
|
artifactMap: ArtifactMap = {}
|
||||||
|
sourceRangeMap: SourceRangeMap = {}
|
||||||
outSequence = 1
|
outSequence = 1
|
||||||
inSequence = 1
|
inSequence = 1
|
||||||
engineConnection?: EngineConnection
|
engineConnection?: EngineConnection
|
||||||
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
|
waitForReady: Promise<void> = new Promise(() => {})
|
||||||
// Folks should realize that wait for ready does not get called _everytime_
|
|
||||||
// the connection resets and restarts, it only gets called the first time.
|
|
||||||
// Be careful what you put here.
|
|
||||||
private resolveReady = () => {}
|
private resolveReady = () => {}
|
||||||
waitForReady: Promise<void> = new Promise((resolve) => {
|
|
||||||
this.resolveReady = resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
[event: string]: {
|
[event: string]: {
|
||||||
@ -607,36 +544,22 @@ export class EngineCommandManager {
|
|||||||
[localUnsubscribeId: string]: (a: any) => void
|
[localUnsubscribeId: string]: (a: any) => void
|
||||||
}
|
}
|
||||||
} = {} as any
|
} = {} as any
|
||||||
|
constructor({
|
||||||
constructor() {
|
|
||||||
this.engineConnection = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
start({
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
executeCode,
|
|
||||||
token,
|
token,
|
||||||
}: {
|
}: {
|
||||||
setMediaStream: (stream: MediaStream) => void
|
setMediaStream: (stream: MediaStream) => void
|
||||||
setIsStreamReady: (isStreamReady: boolean) => void
|
setIsStreamReady: (isStreamReady: boolean) => void
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
executeCode: (code?: string, force?: boolean) => void
|
|
||||||
token?: string
|
token?: string
|
||||||
}) {
|
}) {
|
||||||
if (width === 0 || height === 0) {
|
this.waitForReady = new Promise((resolve) => {
|
||||||
return
|
this.resolveReady = resolve
|
||||||
}
|
})
|
||||||
|
|
||||||
// If we already have an engine connection, just need to resize the stream.
|
|
||||||
if (this.engineConnection) {
|
|
||||||
this.handleResize({ streamWidth: width, streamHeight: height })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
||||||
this.engineConnection = new EngineConnection({
|
this.engineConnection = new EngineConnection({
|
||||||
url,
|
url,
|
||||||
@ -644,35 +567,6 @@ export class EngineCommandManager {
|
|||||||
onEngineConnectionOpen: () => {
|
onEngineConnectionOpen: () => {
|
||||||
this.resolveReady()
|
this.resolveReady()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
|
|
||||||
// Make the axis gizmo.
|
|
||||||
// We do this after the connection opened to avoid a race condition.
|
|
||||||
// Connected opened is the last thing that happens when the stream
|
|
||||||
// is ready.
|
|
||||||
// We also do this here because we want to ensure we create the gizmo
|
|
||||||
// and execute the code everytime the stream is restarted.
|
|
||||||
const gizmoId = uuidv4()
|
|
||||||
this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: gizmoId,
|
|
||||||
cmd: {
|
|
||||||
type: 'make_axes_gizmo',
|
|
||||||
clobber: false,
|
|
||||||
// If true, axes gizmo will be placed in the corner of the screen.
|
|
||||||
// If false, it will be placed at the origin of the scene.
|
|
||||||
gizmo_mode: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Inisialize the planes.
|
|
||||||
this.initPlanes().then(() => {
|
|
||||||
// We execute the code here to make sure if the stream was to
|
|
||||||
// restart in a session, we want to make sure to execute the code.
|
|
||||||
// We force it to re-execute the code because we want to make sure
|
|
||||||
// the code is executed everytime the stream is restarted.
|
|
||||||
// We pass undefined for the code so it reads from the current state.
|
|
||||||
executeCode(undefined, true)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setIsStreamReady(false)
|
setIsStreamReady(false)
|
||||||
@ -723,12 +617,6 @@ export class EngineCommandManager {
|
|||||||
message.request_id
|
message.request_id
|
||||||
) {
|
) {
|
||||||
this.handleModelingCommand(message.resp, message.request_id)
|
this.handleModelingCommand(message.resp, message.request_id)
|
||||||
} else if (
|
|
||||||
!message.success &&
|
|
||||||
message.request_id &&
|
|
||||||
this.artifactMap[message.request_id]
|
|
||||||
) {
|
|
||||||
this.handleFailedModelingCommand(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -748,29 +636,6 @@ export class EngineCommandManager {
|
|||||||
|
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect()
|
||||||
}
|
}
|
||||||
handleResize({
|
|
||||||
streamWidth,
|
|
||||||
streamHeight,
|
|
||||||
}: {
|
|
||||||
streamWidth: number
|
|
||||||
streamHeight: number
|
|
||||||
}) {
|
|
||||||
if (!this.engineConnection?.isReady()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const resizeCmd: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'reconfigure_stream',
|
|
||||||
width: streamWidth,
|
|
||||||
height: streamHeight,
|
|
||||||
fps: 60,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.engineConnection?.send(resizeCmd)
|
|
||||||
}
|
|
||||||
handleModelingCommand(message: WebSocketResponse, id: string) {
|
handleModelingCommand(message: WebSocketResponse, id: string) {
|
||||||
if (message.type !== 'modeling') {
|
if (message.type !== 'modeling') {
|
||||||
return
|
return
|
||||||
@ -808,43 +673,12 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleFailedModelingCommand({
|
|
||||||
request_id,
|
|
||||||
errors,
|
|
||||||
}: Models['FailureWebSocketResponse_type']) {
|
|
||||||
const id = request_id
|
|
||||||
if (!id) return
|
|
||||||
const command = this.artifactMap[id]
|
|
||||||
if (command && command.type === 'pending') {
|
|
||||||
const resolve = command.resolve
|
|
||||||
this.artifactMap[id] = {
|
|
||||||
type: 'failed',
|
|
||||||
range: command.range,
|
|
||||||
commandType: command.commandType,
|
|
||||||
parentId: command.parentId ? command.parentId : undefined,
|
|
||||||
errors,
|
|
||||||
}
|
|
||||||
resolve({
|
|
||||||
id,
|
|
||||||
commandType: command.commandType,
|
|
||||||
range: command.range,
|
|
||||||
errors,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.artifactMap[id] = {
|
|
||||||
type: 'failed',
|
|
||||||
range: command.range,
|
|
||||||
commandType: command.commandType,
|
|
||||||
parentId: command.parentId ? command.parentId : undefined,
|
|
||||||
errors,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tearDown() {
|
tearDown() {
|
||||||
this.engineConnection?.tearDown()
|
this.engineConnection?.close()
|
||||||
}
|
}
|
||||||
startNewSession() {
|
startNewSession() {
|
||||||
this.artifactMap = {}
|
this.artifactMap = {}
|
||||||
|
this.sourceRangeMap = {}
|
||||||
}
|
}
|
||||||
subscribeTo<T extends ModelTypes>({
|
subscribeTo<T extends ModelTypes>({
|
||||||
event,
|
event,
|
||||||
@ -910,15 +744,31 @@ export class EngineCommandManager {
|
|||||||
this.engineConnection?.send(deletCmd)
|
this.engineConnection?.send(deletCmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
cusorsSelected(selections: {
|
||||||
if (this.engineConnection === undefined) {
|
otherSelections: Selections['otherSelections']
|
||||||
return Promise.resolve()
|
idBasedSelections: { type: string; id: string }[]
|
||||||
}
|
}) {
|
||||||
|
|
||||||
if (!this.engineConnection?.isReady()) {
|
if (!this.engineConnection?.isReady()) {
|
||||||
return Promise.resolve()
|
console.log('engine connection isnt ready')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'select_clear',
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'select_add',
|
||||||
|
entities: selections.idBasedSelections.map((s) => s.id),
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||||
if (
|
if (
|
||||||
command.type === 'modeling_cmd_req' &&
|
command.type === 'modeling_cmd_req' &&
|
||||||
command.cmd.type !== lastMessage
|
command.cmd.type !== lastMessage
|
||||||
@ -926,6 +776,10 @@ export class EngineCommandManager {
|
|||||||
console.log('sending command', command.cmd.type)
|
console.log('sending command', command.cmd.type)
|
||||||
lastMessage = command.cmd.type
|
lastMessage = command.cmd.type
|
||||||
}
|
}
|
||||||
|
if (!this.engineConnection?.isReady()) {
|
||||||
|
console.log('socket not ready')
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||||
const cmd = command.cmd
|
const cmd = command.cmd
|
||||||
if (
|
if (
|
||||||
@ -967,11 +821,10 @@ export class EngineCommandManager {
|
|||||||
range: SourceRange
|
range: SourceRange
|
||||||
command: EngineCommand | string
|
command: EngineCommand | string
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
if (this.engineConnection === undefined) {
|
this.sourceRangeMap[id] = range
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.engineConnection?.isReady()) {
|
if (!this.engineConnection?.isReady()) {
|
||||||
|
console.log('socket not ready')
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
this.engineConnection?.send(command)
|
this.engineConnection?.send(command)
|
||||||
@ -1014,9 +867,6 @@ export class EngineCommandManager {
|
|||||||
rangeStr: string,
|
rangeStr: string,
|
||||||
commandStr: string
|
commandStr: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (this.engineConnection === undefined) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
throw new Error('id is undefined')
|
throw new Error('id is undefined')
|
||||||
}
|
}
|
||||||
@ -1040,120 +890,107 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
if (command.type === 'result') {
|
if (command.type === 'result') {
|
||||||
return command.data
|
return command.data
|
||||||
} else if (command.type === 'failed') {
|
|
||||||
return Promise.resolve(command.errors)
|
|
||||||
}
|
}
|
||||||
return command.promise
|
return command.promise
|
||||||
}
|
}
|
||||||
async waitForAllCommands(): Promise<{
|
async waitForAllCommands(
|
||||||
|
ast?: Program,
|
||||||
|
programMemory?: ProgramMemory
|
||||||
|
): Promise<{
|
||||||
artifactMap: ArtifactMap
|
artifactMap: ArtifactMap
|
||||||
|
sourceRangeMap: SourceRangeMap
|
||||||
}> {
|
}> {
|
||||||
const pendingCommands = Object.values(this.artifactMap).filter(
|
const pendingCommands = Object.values(this.artifactMap).filter(
|
||||||
({ type }) => type === 'pending'
|
({ type }) => type === 'pending'
|
||||||
) as PendingCommand[]
|
) as PendingCommand[]
|
||||||
const proms = pendingCommands.map(({ promise }) => promise)
|
const proms = pendingCommands.map(({ promise }) => promise)
|
||||||
await Promise.all(proms)
|
await Promise.all(proms)
|
||||||
|
if (ast && programMemory) {
|
||||||
|
await this.fixIdMappings(ast, programMemory)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artifactMap: this.artifactMap,
|
artifactMap: this.artifactMap,
|
||||||
|
sourceRangeMap: this.sourceRangeMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async initPlanes() {
|
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
|
||||||
const [xy, yz, xz] = [
|
/* This is a temporary solution since the cmd_ids that are sent through when
|
||||||
await this.createPlane({
|
sending 'extend_path' ids are not used as the segment ids.
|
||||||
x_axis: { x: 1, y: 0, z: 0 },
|
|
||||||
y_axis: { x: 0, y: 1, z: 0 },
|
|
||||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
|
||||||
}),
|
|
||||||
await this.createPlane({
|
|
||||||
x_axis: { x: 0, y: 1, z: 0 },
|
|
||||||
y_axis: { x: 0, y: 0, z: 1 },
|
|
||||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
|
||||||
}),
|
|
||||||
await this.createPlane({
|
|
||||||
x_axis: { x: 1, y: 0, z: 0 },
|
|
||||||
y_axis: { x: 0, y: 0, z: 1 },
|
|
||||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
this.defaultPlanes = { xy, yz, xz }
|
|
||||||
|
|
||||||
this.subscribeTo({
|
We have a way to back fill them with 'path_get_info', however this relies on one
|
||||||
event: 'select_with_point',
|
the sketchGroup array and the segements array returned from the server to be in
|
||||||
callback: ({ data }) => {
|
the same length and order. plus it's super hacky, we first use the path_id to get
|
||||||
if (!data?.entity_id) return
|
the source range of the pipe expression then use the name of the variable to get
|
||||||
if (
|
the sketchGroup from programMemory.
|
||||||
![
|
|
||||||
this.defaultPlanes.xy,
|
I feel queezy about relying on all these steps to always line up.
|
||||||
this.defaultPlanes.yz,
|
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
|
||||||
this.defaultPlanes.xz,
|
We should get the cmd_ids to match with the segment ids and delete this method.
|
||||||
].includes(data.entity_id)
|
*/
|
||||||
|
const pathInfoProms = []
|
||||||
|
for (const [id, artifact] of Object.entries(this.artifactMap)) {
|
||||||
|
if (artifact.commandType === 'start_path') {
|
||||||
|
pathInfoProms.push(
|
||||||
|
this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'path_get_info',
|
||||||
|
path_id: id,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => ({
|
||||||
|
originalId: id,
|
||||||
|
segments: data?.data?.segments,
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
return
|
}
|
||||||
this.onPlaneSelectCallback(data.entity_id)
|
}
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
planesInitialized(): boolean {
|
|
||||||
return (
|
|
||||||
this.defaultPlanes.xy !== '' &&
|
|
||||||
this.defaultPlanes.yz !== '' &&
|
|
||||||
this.defaultPlanes.xz !== ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlaneSelectCallback = (id: string) => {}
|
const pathInfos = await Promise.all(pathInfoProms)
|
||||||
onPlaneSelected(callback: (id: string) => void) {
|
pathInfos.forEach(({ originalId, segments }) => {
|
||||||
this.onPlaneSelectCallback = callback
|
const originalArtifact = this.artifactMap[originalId]
|
||||||
}
|
if (!originalArtifact || originalArtifact.type === 'pending') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const pipeExpPath = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
originalArtifact.range
|
||||||
|
)
|
||||||
|
const pipeExp = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
pipeExpPath,
|
||||||
|
'VariableDeclarator'
|
||||||
|
).node
|
||||||
|
if (pipeExp.type !== 'VariableDeclarator') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const variableName = pipeExp.id.name
|
||||||
|
const memoryItem = programMemory.root[variableName]
|
||||||
|
if (!memoryItem) {
|
||||||
|
return
|
||||||
|
} else if (memoryItem.type !== 'SketchGroup') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
|
const relevantSegments = segments.filter(
|
||||||
return await this.sendSceneCommand({
|
({ command_id }: { command_id: string | null }) => command_id
|
||||||
type: 'modeling_cmd_req',
|
)
|
||||||
cmd_id: uuidv4(),
|
if (memoryItem.value.length !== relevantSegments.length) {
|
||||||
cmd: {
|
return
|
||||||
type: 'object_visible',
|
}
|
||||||
object_id: id,
|
for (let i = 0; i < relevantSegments.length; i++) {
|
||||||
hidden: hidden,
|
const engineSegment = relevantSegments[i]
|
||||||
},
|
const memorySegment = memoryItem.value[i]
|
||||||
|
const oldId = memorySegment.__geoMeta.id
|
||||||
|
const artifact = this.artifactMap[oldId]
|
||||||
|
delete this.artifactMap[oldId]
|
||||||
|
delete this.sourceRangeMap[oldId]
|
||||||
|
if (artifact) {
|
||||||
|
this.artifactMap[engineSegment.command_id] = artifact
|
||||||
|
this.sourceRangeMap[engineSegment.command_id] = artifact.range
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createPlane({
|
|
||||||
x_axis,
|
|
||||||
y_axis,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
x_axis: Models['Point3d_type']
|
|
||||||
y_axis: Models['Point3d_type']
|
|
||||||
color: Models['Color_type']
|
|
||||||
}): Promise<string> {
|
|
||||||
const planeId = uuidv4()
|
|
||||||
await this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'make_plane',
|
|
||||||
size: 60,
|
|
||||||
origin: { x: 0, y: 0, z: 0 },
|
|
||||||
x_axis,
|
|
||||||
y_axis,
|
|
||||||
clobber: false,
|
|
||||||
hide: true,
|
|
||||||
},
|
|
||||||
cmd_id: planeId,
|
|
||||||
})
|
|
||||||
await this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'plane_set_color',
|
|
||||||
plane_id: planeId,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
await this.setPlaneHidden(planeId, true)
|
|
||||||
return planeId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const engineCommandManager = new EngineCommandManager()
|
|
||||||
|
|||||||
@ -6,9 +6,13 @@ import {
|
|||||||
getXComponent,
|
getXComponent,
|
||||||
addCloseToPipe,
|
addCloseToPipe,
|
||||||
} from './sketch'
|
} from './sketch'
|
||||||
import { parse, recast, initPromise } from '../wasm'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
import { getNodePathFromSourceRange } from '../queryAst'
|
import { getNodePathFromSourceRange } from '../queryAst'
|
||||||
|
import { recast } from '../recast'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
import { initPromise } from '../rust'
|
||||||
|
|
||||||
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
const eachQuad: [number, [number, number]][] = [
|
const eachQuad: [number, [number, number]][] = [
|
||||||
[-315, [1, 1]],
|
[-315, [1, 1]],
|
||||||
@ -25,8 +29,6 @@ const eachQuad: [number, [number, number]][] = [
|
|||||||
[675, [1, -1]],
|
[675, [1, -1]],
|
||||||
]
|
]
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
|
||||||
|
|
||||||
describe('testing getYComponent', () => {
|
describe('testing getYComponent', () => {
|
||||||
it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {
|
it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {
|
||||||
const expected: [number, number][] = []
|
const expected: [number, number][] = []
|
||||||
@ -96,16 +98,15 @@ describe('testing changeSketchArguments', () => {
|
|||||||
const lineAfterChange = 'lineTo([2, 3], %)'
|
const lineAfterChange = 'lineTo([2, 3], %)'
|
||||||
test('changeSketchArguments', async () => {
|
test('changeSketchArguments', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const genCode = (line: string) => `const mySketch001 = startSketchOn('XY')
|
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
show(mySketch001)
|
show(mySketch001)
|
||||||
`
|
`
|
||||||
const code = genCode(lineToChange)
|
const code = genCode(lineToChange)
|
||||||
const expectedCode = genCode(lineAfterChange)
|
const expectedCode = genCode(lineAfterChange)
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
const { modifiedAst } = changeSketchArguments(
|
const { modifiedAst } = changeSketchArguments(
|
||||||
@ -113,6 +114,20 @@ show(mySketch001)
|
|||||||
programMemory,
|
programMemory,
|
||||||
[sourceStart, sourceStart + lineToChange.length],
|
[sourceStart, sourceStart + lineToChange.length],
|
||||||
[2, 3],
|
[2, 3],
|
||||||
|
{
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
pathId: '',
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathToNode: [
|
||||||
|
['body', ''],
|
||||||
|
[0, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
[0, 'index'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
],
|
||||||
|
},
|
||||||
[0, 0]
|
[0, 0]
|
||||||
)
|
)
|
||||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||||
@ -124,21 +139,19 @@ describe('testing addNewSketchLn', () => {
|
|||||||
test('addNewSketchLn', async () => {
|
test('addNewSketchLn', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = `
|
const code = `
|
||||||
const mySketch001 = startSketchOn('XY')
|
const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
expect(sourceStart).toBe(95)
|
expect(sourceStart).toBe(66)
|
||||||
let { modifiedAst } = addNewSketchLn({
|
let { modifiedAst } = addNewSketchLn({
|
||||||
node: ast,
|
node: ast,
|
||||||
programMemory,
|
programMemory,
|
||||||
to: [2, 3],
|
to: [2, 3],
|
||||||
from: [0, 0],
|
|
||||||
fnName: 'lineTo',
|
fnName: 'lineTo',
|
||||||
pathToNode: [
|
pathToNode: [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
@ -149,8 +162,7 @@ show(mySketch001)`
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
let expectedCode = `const mySketch001 = startSketchOn('XY')
|
let expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
@ -171,8 +183,7 @@ show(mySketch001)
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
expectedCode = `const mySketch001 = startSketchOn('XY')
|
expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
@ -187,15 +198,14 @@ describe('testing addTagForSketchOnFace', () => {
|
|||||||
it('needs to be in it', async () => {
|
it('needs to be in it', async () => {
|
||||||
const originalLine = 'lineTo([-1.59, -1.54], %)'
|
const originalLine = 'lineTo([-1.59, -1.54], %)'
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const genCode = (line: string) => `const mySketch001 = startSketchOn('XY')
|
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)
|
show(mySketch001)
|
||||||
`
|
`
|
||||||
const code = genCode(originalLine)
|
const code = genCode(originalLine)
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(originalLine)
|
const sourceStart = code.indexOf(originalLine)
|
||||||
const sourceRange: [number, number] = [
|
const sourceRange: [number, number] = [
|
||||||
|
|||||||
@ -4,6 +4,9 @@ import {
|
|||||||
SketchGroup,
|
SketchGroup,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
|
MemoryItem,
|
||||||
|
} from '../executor'
|
||||||
|
import {
|
||||||
Program,
|
Program,
|
||||||
PipeExpression,
|
PipeExpression,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -11,14 +14,14 @@ import {
|
|||||||
Value,
|
Value,
|
||||||
Literal,
|
Literal,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
} from '../wasm'
|
} from '../abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodeFromPathCurry,
|
getNodeFromPathCurry,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
} from '../queryAst'
|
} from '../queryAst'
|
||||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||||
import { toolTips, ToolTip } from '../../useStore'
|
import { GuiModes, toolTips, ToolTip } from '../../useStore'
|
||||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||||
|
|
||||||
@ -35,6 +38,7 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
} from '../modifyAst'
|
} from '../modifyAst'
|
||||||
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
||||||
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import { perpendicularDistance } from 'sketch-helpers'
|
import { perpendicularDistance } from 'sketch-helpers'
|
||||||
|
|
||||||
export type Coords2d = [number, number]
|
export type Coords2d = [number, number]
|
||||||
@ -193,6 +197,9 @@ export const line: SketchLineHelper = {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
|
const variableName = varDec.id.name
|
||||||
|
const sketch = previousProgramMemory?.root?.[variableName]
|
||||||
|
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||||
|
|
||||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||||
@ -206,11 +213,7 @@ export const line: SketchLineHelper = {
|
|||||||
pipe.body[callIndex] = callExp
|
pipe.body[callIndex] = callExp
|
||||||
return {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: _node,
|
||||||
pathToNode: [
|
pathToNode,
|
||||||
...pathToNode,
|
|
||||||
['body', 'PipeExpression'],
|
|
||||||
[callIndex, 'CallExpression'],
|
|
||||||
],
|
|
||||||
valueUsedInTransform,
|
valueUsedInTransform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,14 +224,6 @@ export const line: SketchLineHelper = {
|
|||||||
])
|
])
|
||||||
if (pipe.type === 'PipeExpression') {
|
if (pipe.type === 'PipeExpression') {
|
||||||
pipe.body = [...pipe.body, callExp]
|
pipe.body = [...pipe.body, callExp]
|
||||||
return {
|
|
||||||
modifiedAst: _node,
|
|
||||||
pathToNode: [
|
|
||||||
...pathToNode,
|
|
||||||
['body', 'PipeExpression'],
|
|
||||||
[pipe.body.length - 1, 'CallExpression'],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||||
}
|
}
|
||||||
@ -917,14 +912,16 @@ export function changeSketchArguments(
|
|||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
sourceRange: SourceRange,
|
sourceRange: SourceRange,
|
||||||
args: [number, number],
|
args: [number, number],
|
||||||
|
guiMode: GuiModes,
|
||||||
from: [number, number]
|
from: [number, number]
|
||||||
): { modifiedAst: Program; pathToNode: PathToNode } {
|
): { modifiedAst: Program } {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
||||||
const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>(
|
const { node: callExpression, shallowPath } = getNodeFromPath<CallExpression>(
|
||||||
_node,
|
_node,
|
||||||
thePath
|
thePath
|
||||||
)
|
)
|
||||||
|
if (guiMode.mode !== 'sketch') throw new Error('not in sketch mode')
|
||||||
|
|
||||||
if (callExpression?.callee?.name in sketchLineHelperMap) {
|
if (callExpression?.callee?.name in sketchLineHelperMap) {
|
||||||
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
|
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
|
||||||
@ -938,7 +935,7 @@ export function changeSketchArguments(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
throw new Error('not a sketch line helper')
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateLineFnCallArgs {
|
interface CreateLineFnCallArgs {
|
||||||
@ -966,10 +963,8 @@ export function addNewSketchLn({
|
|||||||
to,
|
to,
|
||||||
fnName,
|
fnName,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
from,
|
}: Omit<CreateLineFnCallArgs, 'from'>): {
|
||||||
}: CreateLineFnCallArgs): {
|
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNode: PathToNode
|
|
||||||
} {
|
} {
|
||||||
const node = JSON.parse(JSON.stringify(_node))
|
const node = JSON.parse(JSON.stringify(_node))
|
||||||
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
||||||
@ -982,6 +977,12 @@ export function addNewSketchLn({
|
|||||||
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
||||||
PipeExpression | CallExpression
|
PipeExpression | CallExpression
|
||||||
>(node, pathToNode, 'PipeExpression')
|
>(node, pathToNode, 'PipeExpression')
|
||||||
|
const variableName = varDec.id.name
|
||||||
|
const sketch = previousProgramMemory?.root?.[variableName]
|
||||||
|
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||||
|
|
||||||
|
const last = sketch.value[sketch.value.length - 1] || sketch.start
|
||||||
|
const from = last.to
|
||||||
return add({
|
return add({
|
||||||
node,
|
node,
|
||||||
previousProgramMemory,
|
previousProgramMemory,
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { parse, SketchGroup, recast, initPromise } from '../wasm'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
|
import { SketchGroup } from '../executor'
|
||||||
import {
|
import {
|
||||||
ConstraintType,
|
ConstraintType,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
|
import { recast } from '../recast'
|
||||||
|
import { initPromise } from '../rust'
|
||||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import { Selection } from 'lib/selections'
|
import { Selection } from '../../useStore'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -28,7 +31,7 @@ async function testingSwapSketchFnCall({
|
|||||||
type: 'default',
|
type: 'default',
|
||||||
range: [startIndex, startIndex + callToSwap.length],
|
range: [startIndex, startIndex + callToSwap.length],
|
||||||
}
|
}
|
||||||
const ast = parse(inputCode)
|
const ast = parser_wasm(inputCode)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const selections = {
|
const selections = {
|
||||||
codeBasedSelections: [range],
|
codeBasedSelections: [range],
|
||||||
@ -52,8 +55,7 @@ async function testingSwapSketchFnCall({
|
|||||||
|
|
||||||
describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
||||||
const bigExampleArr = [
|
const bigExampleArr = [
|
||||||
`const part001 = startSketchOn('XY')`,
|
`const part001 = startSketchAt([0, 0])`,
|
||||||
` |> startProfileAt([0, 0], %)`,
|
|
||||||
` |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
|
` |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
|
||||||
` |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
|
` |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
|
||||||
` |> angledLine({`,
|
` |> angledLine({`,
|
||||||
@ -278,8 +280,7 @@ describe('testing swaping out sketch calls with xLine/xLineTo while keeping vari
|
|||||||
`const angledLineOfYLengthY = 0.89`,
|
`const angledLineOfYLengthY = 0.89`,
|
||||||
`const angledLineToXx = -1.86`,
|
`const angledLineToXx = -1.86`,
|
||||||
`const angledLineToYy = -0.76`,
|
`const angledLineToYy = -0.76`,
|
||||||
`const part001 = startSketchOn('XY')`,
|
`const part001 = startSketchAt([0, 0])`,
|
||||||
` |> startProfileAt([0, 0], %)`,
|
|
||||||
// ` |> rx(90, %)`,
|
// ` |> rx(90, %)`,
|
||||||
` |> lineTo([1, 1], %)`,
|
` |> lineTo([1, 1], %)`,
|
||||||
` |> line([lineX, 2.13], %)`,
|
` |> line([lineX, 2.13], %)`,
|
||||||
@ -373,15 +374,14 @@ describe('testing swaping out sketch calls with xLine/xLineTo while keeping vari
|
|||||||
|
|
||||||
describe('testing getSketchSegmentIndexFromSourceRange', () => {
|
describe('testing getSketchSegmentIndexFromSourceRange', () => {
|
||||||
const code = `
|
const code = `
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0.04]) // segment-in-start
|
||||||
|> startProfileAt([0, 0.04], %) // segment-in-start
|
|
||||||
|> line([0, 0.4], %)
|
|> line([0, 0.4], %)
|
||||||
|> xLine(3.48, %)
|
|> xLine(3.48, %)
|
||||||
|> line([2.14, 1.35], %) // normal-segment
|
|> line([2.14, 1.35], %) // normal-segment
|
||||||
|> xLine(3.54, %)
|
|> xLine(3.54, %)
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('normal case works', async () => {
|
it('normal case works', async () => {
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
const index = code.indexOf('// normal-segment') - 7
|
const index = code.indexOf('// normal-segment') - 7
|
||||||
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
||||||
programMemory.root['part001'] as SketchGroup,
|
programMemory.root['part001'] as SketchGroup,
|
||||||
@ -395,7 +395,7 @@ show(part001)`
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('verify it works when the segment is in the `start` property', async () => {
|
it('verify it works when the segment is in the `start` property', async () => {
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
const index = code.indexOf('// segment-in-start') - 7
|
const index = code.indexOf('// segment-in-start') - 7
|
||||||
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
||||||
programMemory.root['part001'] as SketchGroup,
|
programMemory.root['part001'] as SketchGroup,
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import {
|
|||||||
Program,
|
Program,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
SketchGroup,
|
} from '../abstractSyntaxTreeTypes'
|
||||||
SourceRange,
|
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||||
Path,
|
|
||||||
} from '../wasm'
|
|
||||||
|
|
||||||
export function getSketchSegmentFromSourceRange(
|
export function getSketchSegmentFromSourceRange(
|
||||||
sketchGroup: SketchGroup,
|
sketchGroup: SketchGroup,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { parse, Value, recast, initPromise } from '../wasm'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
|
import { Value } from '../abstractSyntaxTreeTypes'
|
||||||
import {
|
import {
|
||||||
getConstraintType,
|
getConstraintType,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
@ -7,9 +8,10 @@ import {
|
|||||||
ConstraintType,
|
ConstraintType,
|
||||||
getConstraintLevelFromSourceRange,
|
getConstraintLevelFromSourceRange,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
import { ToolTip } from '../../useStore'
|
import { initPromise } from '../rust'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections, ToolTip } from '../../useStore'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
import { recast } from '../../lang/recast'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ describe('testing getConstraintType', () => {
|
|||||||
function getConstraintTypeFromSourceHelper(
|
function getConstraintTypeFromSourceHelper(
|
||||||
code: string
|
code: string
|
||||||
): ReturnType<typeof getConstraintType> {
|
): ReturnType<typeof getConstraintType> {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const args = (ast.body[0] as any).expression.arguments[0].elements as [
|
const args = (ast.body[0] as any).expression.arguments[0].elements as [
|
||||||
Value,
|
Value,
|
||||||
Value
|
Value
|
||||||
@ -72,7 +74,7 @@ function getConstraintTypeFromSourceHelper(
|
|||||||
function getConstraintTypeFromSourceHelper2(
|
function getConstraintTypeFromSourceHelper2(
|
||||||
code: string
|
code: string
|
||||||
): ReturnType<typeof getConstraintType> {
|
): ReturnType<typeof getConstraintType> {
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const arg = (ast.body[0] as any).expression.arguments[0] as Value
|
const arg = (ast.body[0] as any).expression.arguments[0] as Value
|
||||||
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
||||||
return getConstraintType(arg, fnName)
|
return getConstraintType(arg, fnName)
|
||||||
@ -93,8 +95,7 @@ const myVar2 = 5
|
|||||||
const myVar3 = 6
|
const myVar3 = 6
|
||||||
const myAng = 40
|
const myAng = 40
|
||||||
const myAng2 = 134
|
const myAng2 = 134
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line([1, 3.82], %) // ln-should-get-tag
|
|> line([1, 3.82], %) // ln-should-get-tag
|
||||||
|> lineTo([myVar, 1], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|
|> lineTo([myVar, 1], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|
||||||
|> lineTo([1, myVar], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|
|> lineTo([1, myVar], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|
||||||
@ -130,8 +131,7 @@ const myVar2 = 5
|
|||||||
const myVar3 = 6
|
const myVar3 = 6
|
||||||
const myAng = 40
|
const myAng = 40
|
||||||
const myAng2 = 134
|
const myAng2 = 134
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
|
|> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
|
||||||
|> angledLineToX([
|
|> angledLineToX([
|
||||||
-angleToMatchLengthX('seg01', myVar, %),
|
-angleToMatchLengthX('seg01', myVar, %),
|
||||||
@ -199,7 +199,7 @@ const part001 = startSketchOn('XY')
|
|||||||
show(part001)
|
show(part001)
|
||||||
`
|
`
|
||||||
it('should transform the ast', async () => {
|
it('should transform the ast', async () => {
|
||||||
const ast = parse(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((ln) => ln.includes('//'))
|
.filter((ln) => ln.includes('//'))
|
||||||
@ -234,8 +234,7 @@ describe('testing transformAstForSketchLines for vertical and horizontal constra
|
|||||||
const inputScript = `const myVar = 2
|
const inputScript = `const myVar = 2
|
||||||
const myVar2 = 12
|
const myVar2 = 12
|
||||||
const myVar3 = -10
|
const myVar3 = -10
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
|> line([-6.28, 1.4], %) // select for horizontal constraint 1
|
|> line([-6.28, 1.4], %) // select for horizontal constraint 1
|
||||||
|> line([-1.07, myVar], %) // select for vertical constraint 1
|
|> line([-1.07, myVar], %) // select for vertical constraint 1
|
||||||
@ -263,8 +262,7 @@ show(part001)
|
|||||||
const expectModifiedScript = `const myVar = 2
|
const expectModifiedScript = `const myVar = 2
|
||||||
const myVar2 = 12
|
const myVar2 = 12
|
||||||
const myVar3 = -10
|
const myVar3 = -10
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
|> xLine(-6.28, %) // select for horizontal constraint 1
|
|> xLine(-6.28, %) // select for horizontal constraint 1
|
||||||
|> line([-1.07, myVar], %) // select for vertical constraint 1
|
|> line([-1.07, myVar], %) // select for vertical constraint 1
|
||||||
@ -288,7 +286,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||||
show(part001)
|
show(part001)
|
||||||
`
|
`
|
||||||
const ast = parse(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((ln) => ln.includes('// select for horizontal constraint'))
|
.filter((ln) => ln.includes('// select for horizontal constraint'))
|
||||||
@ -322,8 +320,7 @@ show(part001)
|
|||||||
const expectModifiedScript = `const myVar = 2
|
const expectModifiedScript = `const myVar = 2
|
||||||
const myVar2 = 12
|
const myVar2 = 12
|
||||||
const myVar3 = -10
|
const myVar3 = -10
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
|> line([-6.28, 1.4], %) // select for horizontal constraint 1
|
|> line([-6.28, 1.4], %) // select for horizontal constraint 1
|
||||||
|> yLine(myVar, %) // select for vertical constraint 1
|
|> yLine(myVar, %) // select for vertical constraint 1
|
||||||
@ -347,7 +344,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||||
show(part001)
|
show(part001)
|
||||||
`
|
`
|
||||||
const ast = parse(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((ln) => ln.includes('// select for vertical constraint'))
|
.filter((ln) => ln.includes('// select for vertical constraint'))
|
||||||
@ -382,8 +379,7 @@ show(part001)
|
|||||||
describe('testing transformAstForSketchLines for vertical and horizontal distance constraints', () => {
|
describe('testing transformAstForSketchLines for vertical and horizontal distance constraints', () => {
|
||||||
describe('testing setHorzDistance for line', () => {
|
describe('testing setHorzDistance for line', () => {
|
||||||
const inputScript = `const myVar = 1
|
const inputScript = `const myVar = 1
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line([0.31, 1.67], %) // base selection
|
|> line([0.31, 1.67], %) // base selection
|
||||||
|> line([0.45, 1.46], %)
|
|> line([0.45, 1.46], %)
|
||||||
|> line([0.45, 1.46], %) // free
|
|> line([0.45, 1.46], %) // free
|
||||||
@ -439,7 +435,7 @@ async function helperThing(
|
|||||||
linesOfInterest: string[],
|
linesOfInterest: string[],
|
||||||
constraint: ConstraintType
|
constraint: ConstraintType
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const ast = parse(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((ln) =>
|
.filter((ln) =>
|
||||||
@ -484,8 +480,7 @@ const baseThickHalf = baseThick / 2
|
|||||||
const halfHeight = totalHeight / 2
|
const halfHeight = totalHeight / 2
|
||||||
const halfArmAngle = armAngle / 2
|
const halfArmAngle = armAngle / 2
|
||||||
|
|
||||||
const part001 = startSketchOn('XY')
|
const part001 = startSketchAt([-0.01, -0.05])
|
||||||
|> startProfileAt([-0.01, -0.05], %)
|
|
||||||
|> line([0.01, 0.94 + 0], %) // partial
|
|> line([0.01, 0.94 + 0], %) // partial
|
||||||
|> xLine(3.03, %) // partial
|
|> xLine(3.03, %) // partial
|
||||||
|> angledLine({
|
|> angledLine({
|
||||||
@ -503,7 +498,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> xLine(-3.43 + 0, %) // full
|
|> xLine(-3.43 + 0, %) // full
|
||||||
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full
|
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full
|
||||||
show(part001)`
|
show(part001)`
|
||||||
const ast = parse(code)
|
const ast = parser_wasm(code)
|
||||||
const constraintLevels: ReturnType<
|
const constraintLevels: ReturnType<
|
||||||
typeof getConstraintLevelFromSourceRange
|
typeof getConstraintLevelFromSourceRange
|
||||||
>[] = ['full', 'partial', 'free']
|
>[] = ['full', 'partial', 'free']
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { TransformCallback } from './stdTypes'
|
import { TransformCallback } from './stdTypes'
|
||||||
import { toolTips, ToolTip } from '../../useStore'
|
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
|
||||||
import { Selections, Selection } from 'lib/selections'
|
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
Program,
|
Program,
|
||||||
Value,
|
Value,
|
||||||
BinaryPart,
|
BinaryPart,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
PathToNode,
|
} from '../abstractSyntaxTreeTypes'
|
||||||
ProgramMemory,
|
|
||||||
} from '../wasm'
|
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodeFromPathCurry,
|
getNodeFromPathCurry,
|
||||||
@ -28,8 +25,10 @@ import {
|
|||||||
giveSketchFnCallTag,
|
giveSketchFnCallTag,
|
||||||
} from '../modifyAst'
|
} from '../modifyAst'
|
||||||
import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
||||||
|
import { PathToNode, ProgramMemory } from '../executor'
|
||||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||||
|
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
|
||||||
|
|
||||||
type LineInputsType =
|
type LineInputsType =
|
||||||
| 'xAbsolute'
|
| 'xAbsolute'
|
||||||
@ -1280,7 +1279,7 @@ export function getTransformInfos(
|
|||||||
}) as TransformInfo[]
|
}) as TransformInfo[]
|
||||||
return theTransforms
|
return theTransforms
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error', error)
|
console.log(error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1307,7 +1306,7 @@ export function getRemoveConstraintsTransforms(
|
|||||||
return theTransforms
|
return theTransforms
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PathToNodeMap = { [key: number]: PathToNode }
|
type PathToNodeMap = { [key: number]: PathToNode }
|
||||||
|
|
||||||
export function transformSecondarySketchLinesTagFirst({
|
export function transformSecondarySketchLinesTagFirst({
|
||||||
ast,
|
ast,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { parse, initPromise } from '../wasm'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
import { initPromise } from '../rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
describe('testing angledLineThatIntersects', () => {
|
describe('testing angledLineThatIntersects', () => {
|
||||||
it('angledLineThatIntersects should intersect with another line', async () => {
|
it('angledLineThatIntersects should intersect with another line', async () => {
|
||||||
const code = (offset: string) => `const part001 = startSketchOn('XY')
|
const code = (offset: string) => `const part001 = startSketchAt([0, 0])
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo({to:[2, 2], tag: "yo"}, %)
|
|> lineTo({to:[2, 2], tag: "yo"}, %)
|
||||||
|> lineTo([3, 1], %)
|
|> lineTo([3, 1], %)
|
||||||
|> angledLineThatIntersects({
|
|> angledLineThatIntersects({
|
||||||
@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
|
|||||||
}, %)
|
}, %)
|
||||||
const intersect = segEndX('yo2', part001)
|
const intersect = segEndX('yo2', part001)
|
||||||
show(part001)`
|
show(part001)`
|
||||||
const { root } = await enginelessExecutor(parse(code('-1')))
|
const { root } = await enginelessExecutor(parser_wasm(code('-1')))
|
||||||
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
||||||
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
const { root: noOffset } = await enginelessExecutor(parser_wasm(code('0')))
|
||||||
expect(noOffset.intersect.value).toBeCloseTo(1)
|
expect(noOffset.intersect.value).toBeCloseTo(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user