Compare commits
307 Commits
mike/relat
...
achalmers/
| Author | SHA1 | Date | |
|---|---|---|---|
| 99083db731 | |||
| e456bde16c | |||
| 5b8ad29e7d | |||
| b01357b49e | |||
| 793e3510cc | |||
| 04ae8141c3 | |||
| 3ae5393dd7 | |||
| 38119d5a3b | |||
| b453b4b453 | |||
| 3972431cb4 | |||
| 884545fcde | |||
| 6deb242eb5 | |||
| 77fa9af71e | |||
| 6a9a0a8bd7 | |||
| 90e432b10e | |||
| 90499e086f | |||
| 8b398a8dd5 | |||
| 23d2dc8dc8 | |||
| 764a73ec8b | |||
| b69451d2fe | |||
| 173d50517c | |||
| 3b63632005 | |||
| 2bd3b06178 | |||
| 9c58cde35f | |||
| 3eb92bb0c4 | |||
| f3083eb59d | |||
| cef29013b8 | |||
| 58d1303468 | |||
| 7c11b7b739 | |||
| 9f27f3c1ce | |||
| f6cbc752d7 | |||
| 6df1ae7161 | |||
| 159ec08211 | |||
| 6aab9c6e23 | |||
| afd8daae15 | |||
| 1132779b4b | |||
| e3a65f5b3f | |||
| d53665a12a | |||
| 447f4f9f8f | |||
| 859927c06d | |||
| b88425efc8 | |||
| c37dfc61ef | |||
| b170ac739f | |||
| d712add4da | |||
| d8aad4bd4f | |||
| 1f1c44e598 | |||
| b20e685eea | |||
| 3690d986c1 | |||
| 9a7f434ede | |||
| 6afacd7427 | |||
| 957001ee88 | |||
| 8b4cc306af | |||
| 52d88171ca | |||
| 9142cf3af7 | |||
| 361500058c | |||
| 198479a71a | |||
| 905784c1e5 | |||
| c33aaad800 | |||
| d175c75780 | |||
| ba348d1222 | |||
| 1f49ddfc29 | |||
| 58659652c1 | |||
| 251971238d | |||
| 381d0b3bc8 | |||
| fa7943d06a | |||
| 7a384251d4 | |||
| 8e07ea32a6 | |||
| 23adf9d905 | |||
| 9f0ac5f6fd | |||
| 08dbd2e9c3 | |||
| 2e2ba5adbd | |||
| a21dbf1055 | |||
| 5ecb176467 | |||
| 66135636ec | |||
| 685a16545c | |||
| 9adb15ee93 | |||
| a8c4c97d79 | |||
| 39e8e1f259 | |||
| 1672c1fd1f | |||
| 6ec5881985 | |||
| 7272cc9fbd | |||
| b925ed9b65 | |||
| 0db5db2181 | |||
| 898e3db9d1 | |||
| d337ac2546 | |||
| 371d8e08f7 | |||
| 338c43a29d | |||
| 52bb5a2657 | |||
| 1b6a06d266 | |||
| c68d4778a5 | |||
| a8abea4fb5 | |||
| a0678d22a8 | |||
| acbfae2e65 | |||
| 1e1bec6a8a | |||
| 06462b5a65 | |||
| 2f292fb1be | |||
| 8184e7b376 | |||
| b1084cbf80 | |||
| 548b45905e | |||
| 141fd2f3f1 | |||
| 604d931962 | |||
| b1668410f8 | |||
| 13176cec38 | |||
| 3a59ae13b6 | |||
| 57c2481943 | |||
| a1c555c51e | |||
| 4d520541be | |||
| 82586f002b | |||
| 4bd08f7444 | |||
| 6b2603b1c4 | |||
| af49bebde3 | |||
| ca056996fd | |||
| 34163da361 | |||
| 7c22bac638 | |||
| 37a65b166b | |||
| 1189f272ba | |||
| ca5bc880dc | |||
| 828daba304 | |||
| 0b9ba55bb4 | |||
| 2d2a85ae7d | |||
| cc57a302cc | |||
| fdbfd0c4b6 | |||
| 2e419907e6 | |||
| 3d0c5c10b0 | |||
| 4d47c067b7 | |||
| 3b3b5371eb | |||
| 3ea77f8e1e | |||
| 4fa7c07e54 | |||
| c66a96a333 | |||
| 4196ff91ac | |||
| cf66b93963 | |||
| 0b0219b810 | |||
| 36c7fcf6d7 | |||
| 023c3cbb90 | |||
| 387f7e0912 | |||
| 9b55b1fd12 | |||
| 4b6662169c | |||
| d36abfcb3d | |||
| 9002ae9efb | |||
| 4deea25394 | |||
| b5940d2cb7 | |||
| 932b467c1e | |||
| 7c7f5c81c4 | |||
| 066b4f3e06 | |||
| c6067bfc7a | |||
| 2018f0d517 | |||
| 74aae3d15f | |||
| 812f419e75 | |||
| 5ec8cc69db | |||
| a5302b6e0e | |||
| 2114cc0d94 | |||
| 2471ce1aba | |||
| 35772475b9 | |||
| 86c592c0f6 | |||
| 0e98973cfa | |||
| 7dd16fe6de | |||
| 478b636049 | |||
| c779311a56 | |||
| ca02ec1151 | |||
| b271d5060e | |||
| 19f11fe55a | |||
| f6f1574982 | |||
| 6dc4fbc808 | |||
| 8843d02380 | |||
| 3578ec07e6 | |||
| db35f73e41 | |||
| 5cfc2b7941 | |||
| 318e4a0cc7 | |||
| 1e23be8f08 | |||
| ef547e7db8 | |||
| 71b48bbd89 | |||
| c825eac27e | |||
| 82e8a491c4 | |||
| 93e806fc99 | |||
| f1a14f1e3d | |||
| 57c01ec3a2 | |||
| ce951d7c12 | |||
| 0aa2a6cee7 | |||
| ba8f5d9785 | |||
| 50a133b2fa | |||
| 3b15bc12f7 | |||
| 8eedee328b | |||
| 49b321feb5 | |||
| 35b5ad7d9b | |||
| 8fad9ef3c2 | |||
| b257b202c3 | |||
| c6af62797d | |||
| 16a9acad56 | |||
| 8a80a88ad3 | |||
| 71d1bb70ef | |||
| 4853872614 | |||
| 1ca5204a1a | |||
| 7baed0b5bd | |||
| e4969857bd | |||
| 9b7cc7afa4 | |||
| 714917429e | |||
| 5af9c6b22d | |||
| 396a994fe6 | |||
| 872da51da5 | |||
| 05cd8cfec9 | |||
| 2a02f6e039 | |||
| 5b90686e5e | |||
| 298269d117 | |||
| b379f6518f | |||
| 6b22c8789d | |||
| cb4683e70b | |||
| 0a020d9959 | |||
| 7aae3dccdc | |||
| 818bf96d0b | |||
| 03bc2eaf22 | |||
| 8ad1476c13 | |||
| 6c15a743a2 | |||
| d0930477ad | |||
| e5e30d231b | |||
| 9822576077 | |||
| 629f326f4c | |||
| 89b880d9ae | |||
| f6de0de1bf | |||
| 65ebb86b67 | |||
| cce8274902 | |||
| c515bef8e4 | |||
| b17e61d963 | |||
| d31d07d9c8 | |||
| 7aa2d63c21 | |||
| e1081b0ee6 | |||
| 59223279b7 | |||
| 8a4e717565 | |||
| 80b542ca18 | |||
| e4bfc863ea | |||
| 77ef255de4 | |||
| 64c3841079 | |||
| c7bb6bc845 | |||
| 1af8a8c64f | |||
| eb4776826b | |||
| f3dd0469d5 | |||
| deea74754d | |||
| 3fd798c704 | |||
| cc9eaf2991 | |||
| 6f24031220 | |||
| 672bcd297f | |||
| 3bc182fe16 | |||
| 589cd39eec | |||
| 63feebef5c | |||
| 65037abd9a | |||
| 97bc339a62 | |||
| 4e9a6375a5 | |||
| 3d19dfb800 | |||
| d2a7b84292 | |||
| 9e02bab155 | |||
| 7352de5a70 | |||
| 9797d0cb81 | |||
| 83907fa9db | |||
| a367be4e2b | |||
| 056fa00adc | |||
| 4759fb2e6f | |||
| 45f497d9cd | |||
| dc61bdebdf | |||
| 61943055e5 | |||
| 416fe0f644 | |||
| 708465d818 | |||
| e706fb02d6 | |||
| 1bf7daa474 | |||
| ffc47f8f40 | |||
| 768aaa84f6 | |||
| f3a700eec8 | |||
| c853637a9a | |||
| 9af30d9ef6 | |||
| 6164714a6b | |||
| 64ceb98eba | |||
| 2cbf260900 | |||
| cfaaedf602 | |||
| 12b3717eb5 | |||
| 0bc685b0c4 | |||
| 9ee032771a | |||
| c307ddd1b1 | |||
| a30818ff2b | |||
| 53e763d938 | |||
| 8f74cd1d0c | |||
| c271942897 | |||
| a03d09b41d | |||
| 2971b7752b | |||
| 70e99eb00b | |||
| 5c66af59d2 | |||
| 6dda6daeef | |||
| b5387f1220 | |||
| fd5921b366 | |||
| 716ad938fc | |||
| 40136eb392 | |||
| 8d2b89fcd1 | |||
| ad9fba3390 | |||
| 911c43af50 | |||
| ab4e04f6c2 | |||
| 94aef05f74 | |||
| d820cf2446 | |||
| 0c724c4971 | |||
| b54ac4a694 | |||
| 27227092b1 | |||
| 04e1b92a5b | |||
| 0553cd4621 | |||
| 61a0c88af4 | |||
| d5b0544437 | |||
| 6cc8af5c23 | |||
| 888104080e | |||
| b6769889e3 | |||
| a32258dac4 | |||
| 18dbbad244 | |||
| b67c16cc9d |
3
.codespellrc
Normal file
3
.codespellrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[codespell]
|
||||||
|
ignore-words-list: crate,everytime
|
||||||
|
skip: **/target,node_modules,build
|
||||||
18
.eslintrc
18
.eslintrc
@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"css-modules"
|
"css-modules"
|
||||||
],
|
],
|
||||||
@ -11,6 +15,16 @@
|
|||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"never"
|
"never"
|
||||||
]
|
],
|
||||||
}
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||||
|
"rules": {
|
||||||
|
"testing-library/prefer-screen-queries": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
3
.github/workflows/cargo-build.yml
vendored
3
.github/workflows/cargo-build.yml
vendored
@ -15,6 +15,9 @@ 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,6 +15,9 @@ 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:
|
||||||
@ -54,4 +57,4 @@ jobs:
|
|||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
cargo clippy --all --tests -- -D warnings
|
cargo clippy --all --tests --benches -- -D warnings
|
||||||
|
|||||||
40
.github/workflows/cargo-criterion.yml
vendored
Normal file
40
.github/workflows/cargo-criterion.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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,6 +18,9 @@ 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,6 +17,9 @@ 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:
|
||||||
@ -58,4 +61,5 @@ 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
|
||||||
|
|
||||||
|
|||||||
198
.github/workflows/ci.yml
vendored
198
.github/workflows/ci.yml
vendored
@ -7,13 +7,24 @@ 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)
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && contains(github.event.pull_request.title, 'Cut release v') }}
|
||||||
|
|
||||||
|
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-latest'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -21,11 +32,11 @@ jobs:
|
|||||||
- run: yarn fmt-check
|
- run: yarn fmt-check
|
||||||
|
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -37,14 +48,27 @@ jobs:
|
|||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
- run: yarn tsc
|
- run: yarn tsc
|
||||||
|
|
||||||
|
|
||||||
|
check-typos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
- name: Install codespell
|
||||||
|
run: |
|
||||||
|
python -m pip install codespell
|
||||||
|
- name: Run codespell
|
||||||
|
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
|
||||||
|
|
||||||
|
|
||||||
build-test-web:
|
build-test-web:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
version: ${{ steps.export_version.outputs.version }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@ -63,36 +87,81 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn test:cov
|
- run: yarn test:cov
|
||||||
|
|
||||||
- id: export_version
|
|
||||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
build-apps:
|
prepare-json-files:
|
||||||
needs: [check-format, build-test-web, check-types]
|
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||||
runs-on: ${{ matrix.os }}
|
outputs:
|
||||||
strategy:
|
version: ${{ steps.export_version.outputs.version }}
|
||||||
matrix:
|
|
||||||
os: [macos-latest, ubuntu-20.04, windows-latest]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: install ubuntu system dependencies
|
- uses: actions/setup-node@v4
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Set nightly version
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
|
||||||
|
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
package.json
|
||||||
|
src-tauri/tauri.conf.json
|
||||||
|
src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- id: export_version
|
||||||
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
|
build-test-apps:
|
||||||
|
needs: [prepare-json-files]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- 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
|
||||||
|
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- name: Install ubuntu system dependencies
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: >
|
||||||
|
sudo apt-get update &&
|
||||||
|
sudo apt-get install -y
|
||||||
|
libgtk-3-dev
|
||||||
|
libgtksourceview-3.0-dev
|
||||||
|
webkit2gtk-4.0
|
||||||
|
libappindicator3-dev
|
||||||
|
webkit2gtk-driver
|
||||||
|
xvfb
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||||
|
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Setup Rust cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: './src-tauri -> target'
|
||||||
@ -101,24 +170,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
- name: wasm prep
|
- name: Run build:wasm manually
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
|
||||||
run: |
|
run: |
|
||||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||||
npx wasm-pack build --target web --out-dir pkg
|
echo "building with ${{ env.MODE }}"
|
||||||
|
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
|
||||||
cd ../../
|
cd ../../
|
||||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||||
|
|
||||||
|
- name: Run vite build (build:both)
|
||||||
|
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
|
|
||||||
- name: Fix format
|
- name: Fix format
|
||||||
run: yarn fmt
|
run: yarn fmt
|
||||||
|
|
||||||
- name: install apple silicon target mac
|
- name: Install Universal target (MacOS only)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: |
|
run: |
|
||||||
rustup target add aarch64-apple-darwin
|
rustup target add aarch64-apple-darwin
|
||||||
|
|
||||||
- name: Prepare Windows certificate and variables
|
- name: Prepare certificate and variables (Windows only)
|
||||||
if: matrix.os == 'windows-latest'
|
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||||
cat /d/Certificate_pkcs12.p12
|
cat /d/Certificate_pkcs12.p12
|
||||||
@ -132,8 +207,8 @@ jobs:
|
|||||||
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
|
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Setup Windows certicate with SSM KSP
|
- name: Setup certicate with SSM KSP (Windows only)
|
||||||
if: matrix.os == 'windows-latest'
|
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||||
@ -143,8 +218,17 @@ jobs:
|
|||||||
smksp_cert_sync.exe
|
smksp_cert_sync.exe
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- name: Build and sign the app for the current platform
|
- name: Build the app (debug)
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
if: ${{ env.BUILD_RELEASE == 'false' }}
|
||||||
|
with:
|
||||||
|
includeRelease: false
|
||||||
|
includeDebug: true
|
||||||
|
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: Build the app (release) and sign
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
env:
|
env:
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
@ -153,21 +237,40 @@ 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 }}
|
||||||
|
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
|
env:
|
||||||
|
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||||
|
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||||
with:
|
with:
|
||||||
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
|
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||||
|
|
||||||
|
- name: Run e2e tests (linux only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
cargo install tauri-driver
|
||||||
|
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
|
export VITE_KC_API_BASE_URL
|
||||||
|
xvfb-run yarn test:e2e:tauri
|
||||||
|
env:
|
||||||
|
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
|
||||||
|
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
|
||||||
|
|
||||||
publish-apps-release:
|
publish-apps-release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'release'
|
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||||
needs: [build-test-web, build-apps]
|
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
|
||||||
env:
|
env:
|
||||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
||||||
PUB_DATE: ${{ github.event.release.created_at }}
|
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||||
NOTES: ${{ github.event.release.body }}
|
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||||
|
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
|
||||||
|
|
||||||
@ -177,9 +280,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://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "v${VERSION_NO_V}" \
|
--arg version "${VERSION}" \
|
||||||
--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" \
|
||||||
@ -215,9 +318,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate the download static endpoint
|
- name: Generate the download static endpoint
|
||||||
run: |
|
run: |
|
||||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "v${VERSION_NO_V}" \
|
--arg version "${VERSION}" \
|
||||||
--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" \
|
||||||
@ -242,7 +345,7 @@ jobs:
|
|||||||
cat last_download.json
|
cat last_download.json
|
||||||
|
|
||||||
- name: Authenticate to Google Cloud
|
- name: Authenticate to Google Cloud
|
||||||
uses: 'google-github-actions/auth@v1.1.1'
|
uses: 'google-github-actions/auth@v2.0.0'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||||
|
|
||||||
@ -257,21 +360,22 @@ jobs:
|
|||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/*itty*'
|
glob: '*/*itty*'
|
||||||
parent: false
|
parent: false
|
||||||
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||||
|
|
||||||
- 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: dl.kittycad.io/releases/modeling-app
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- 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: dl.kittycad.io/releases/modeling-app
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- 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*
|
||||||
|
|||||||
114
.github/workflows/playwright.yml
vendored
Normal file
114
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
jobs:
|
||||||
|
playwright-ubuntu:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache wasm
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
- name: build wasm
|
||||||
|
run: yarn build:wasm
|
||||||
|
- name: build web
|
||||||
|
run: yarn build:local
|
||||||
|
- name: Run ubuntu/chrome snapshots
|
||||||
|
run: yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
- name: check for changes
|
||||||
|
id: git-check
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
if git status | grep -q "Changes to be committed"
|
||||||
|
then
|
||||||
|
echo "::set-output name=modified::true"
|
||||||
|
else
|
||||||
|
echo "::set-output name=modified::false"
|
||||||
|
fi
|
||||||
|
- name: Commit changes, if any
|
||||||
|
if: steps.git-check.outputs.modified == 'true'
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||||
|
git fetch origin
|
||||||
|
echo ${{ github.head_ref }}
|
||||||
|
git checkout ${{ github.head_ref }}
|
||||||
|
# TODO when safari works on ubuntu remove the os part of the commit message
|
||||||
|
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" || true
|
||||||
|
git push
|
||||||
|
git push origin ${{ github.head_ref }}
|
||||||
|
- name: Run ubuntu/chrome flow
|
||||||
|
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
playwright-macos:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: playwright-ubuntu
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache wasm
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
- name: build wasm
|
||||||
|
run: yarn build:wasm
|
||||||
|
- name: build web
|
||||||
|
run: yarn build:local
|
||||||
|
- name: Run macos/safari flow
|
||||||
|
# safari doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues)
|
||||||
|
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed
|
||||||
|
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -22,9 +22,22 @@ 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
|
||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
public/wasm_lib_bg.wasm
|
public/wasm_lib_bg.wasm
|
||||||
src/wasm-lib/lcov.info
|
src/wasm-lib/lcov.info
|
||||||
|
|
||||||
|
e2e/playwright/playwright-secrets.env
|
||||||
|
e2e/playwright/temp1.png
|
||||||
|
e2e/playwright/temp2.png
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|||||||
@ -7,3 +7,7 @@ coverage
|
|||||||
target
|
target
|
||||||
src/wasm-lib/pkg
|
src/wasm-lib/pkg
|
||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
|
e2e/playwright/export-snapshots
|
||||||
|
|
||||||
|
# XState generated files
|
||||||
|
src/machines/modelingMachine.typegen.ts
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
140
README.md
140
README.md
@ -29,6 +29,7 @@ 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
|
||||||
@ -47,7 +48,7 @@ We recommend downloading the latest application binary from [our Releases page](
|
|||||||
|
|
||||||
## Running a development build
|
## Running a development build
|
||||||
|
|
||||||
First, [install Rust via `rustup`](https://www.rust-lang.org/tools/install). This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. Then, run:
|
First, [install Rust via `rustup`](https://www.rust-lang.org/tools/install). This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We always use the latest stable version of Rust, so you may need to run `rustup update stable`. Then, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn install
|
yarn install
|
||||||
@ -56,7 +57,7 @@ yarn install
|
|||||||
followed by:
|
followed by:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn build:wasm
|
yarn build:wasm-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
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)
|
||||||
@ -88,15 +89,22 @@ 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` need to have been done before hand then
|
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn tauri dev
|
yarn tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writting they can conflict.
|
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writing they can conflict.
|
||||||
|
|
||||||
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
|
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
|
||||||
|
|
||||||
@ -123,13 +131,24 @@ 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 `Bump to v{x}.{y}.{z}` PR, committing the changes from
|
1. Bump the versions in the .json files by creating a `Cut release 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.
|
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
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
@ -157,3 +176,112 @@ $ cargo +nightly fuzz run parser
|
|||||||
|
|
||||||
For more information on fuzzing you can check out
|
For more information on fuzzing you can check out
|
||||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
||||||
|
|
||||||
|
|
||||||
|
### Playwright
|
||||||
|
|
||||||
|
First time running plawright locally, you'll need to add the secrets file
|
||||||
|
```bash
|
||||||
|
touch ./e2e/playwright/playwright-secrets.env
|
||||||
|
echo 'token="your-token"' > ./e2e/playwright/playwright-secrets.env
|
||||||
|
```
|
||||||
|
then replace "your-token" with a dev token from dev.kittycad.io/account/api-tokens
|
||||||
|
|
||||||
|
then:
|
||||||
|
run playwright
|
||||||
|
```
|
||||||
|
yarn playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
run a specific test suite
|
||||||
|
```
|
||||||
|
yarn playwright test src/e2e-tests/example.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
run a specific test change the test from `test('...` to `test.only('...`
|
||||||
|
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||||
|
|
||||||
|
run headed
|
||||||
|
```
|
||||||
|
yarn playwright test --headed
|
||||||
|
```
|
||||||
|
|
||||||
|
run with step through debugger
|
||||||
|
```
|
||||||
|
PWDEBUG=1 yarn playwright test
|
||||||
|
```
|
||||||
|
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
|
||||||
|
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
|
||||||
|
|
||||||
|
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
|
||||||
|
Or comment out browsers in `playwright.config.ts`.
|
||||||
|
|
||||||
|
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
|
||||||
|
|
||||||
|
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `yarn playwright codegen -b wk --load-storage ./store localhost:3000`
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
|
||||||
|
Where `./store` should look like this
|
||||||
|
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"cookies": [],
|
||||||
|
"origins": [
|
||||||
|
{
|
||||||
|
"origin": "http://localhost:3000",
|
||||||
|
"localStorage": [
|
||||||
|
{
|
||||||
|
"name": "store",
|
||||||
|
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "persistCode",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TOKEN_PERSIST_KEY",
|
||||||
|
"value": "your-token"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
|
||||||
|
|
||||||
|
#### Some notes on CI
|
||||||
|
|
||||||
|
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
|
||||||
|
|
||||||
|
How to interpret failing playwright tests?
|
||||||
|
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
|
||||||
|
|
||||||
|
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
|
||||||
|
|
||||||
|
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
|
||||||
|
|
||||||
|
#### Getting started writing a playwright test in our app
|
||||||
|
|
||||||
|
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
|
||||||
|
|
||||||
|
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>
|
||||||
|
Ps for the debug panel, the following JSON is useful for snapping the camera
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|||||||
4516
docs/kcl/std.json
4516
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
1894
docs/kcl/std.md
1894
docs/kcl/std.md
File diff suppressed because it is too large
Load Diff
BIN
e2e/playwright/export-snapshots/gltf-binary.gltf
Normal file
BIN
e2e/playwright/export-snapshots/gltf-binary.gltf
Normal file
Binary file not shown.
3018
e2e/playwright/export-snapshots/gltf-embedded.gltf
Normal file
3018
e2e/playwright/export-snapshots/gltf-embedded.gltf
Normal file
File diff suppressed because one or more lines are too long
3018
e2e/playwright/export-snapshots/gltf-standard-2.gltf
Normal file
3018
e2e/playwright/export-snapshots/gltf-standard-2.gltf
Normal file
File diff suppressed because it is too large
Load Diff
BIN
e2e/playwright/export-snapshots/gltf-standard.gltf
Normal file
BIN
e2e/playwright/export-snapshots/gltf-standard.gltf
Normal file
Binary file not shown.
189
e2e/playwright/export-snapshots/obj-.obj
Normal file
189
e2e/playwright/export-snapshots/obj-.obj
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
v 0 -4 0
|
||||||
|
v 0 0 0
|
||||||
|
v 0 -4 -1
|
||||||
|
v 0 0 -1
|
||||||
|
v 3.0950184 -4 -1
|
||||||
|
v 3.0950184 0 -1
|
||||||
|
v 5.9513144 -4 -3
|
||||||
|
v 5.9513144 0 -3
|
||||||
|
v 9.5 -4 -3
|
||||||
|
v 9.5 0 -3
|
||||||
|
v 9.5 -4 -2.5
|
||||||
|
v 9.5 0 -2.5
|
||||||
|
v 6.108964 -4 -2.5
|
||||||
|
v 6.108964 0 -2.5
|
||||||
|
v 3.4311862 -4 -0.625
|
||||||
|
v 4.323779 -4 -1.25
|
||||||
|
v 4.323779 -0 -1.25
|
||||||
|
v 3.4311862 -0 -0.625
|
||||||
|
v 2.5385938 0 0
|
||||||
|
v 2.5385938 -4 0
|
||||||
|
v 3.342784 -4 0.375
|
||||||
|
v 4.146974 -4 0.75
|
||||||
|
v 3.342784 -0 0.375
|
||||||
|
v 4.146974 -0 0.75
|
||||||
|
v 5.755354 0 1.5
|
||||||
|
v 5.755354 -4 1.5
|
||||||
|
v 9.5 -4 1.5
|
||||||
|
v 9.5 0 1.5
|
||||||
|
v 9.5 -4 2
|
||||||
|
v 9.5 0 2
|
||||||
|
v 5.644507 -4 2
|
||||||
|
v 5.644507 0 2
|
||||||
|
v 3.5 -4 1
|
||||||
|
v 3.5 0 1
|
||||||
|
v 0 -4 1
|
||||||
|
v 0 0 1
|
||||||
|
vt 0.0127 -0.0508
|
||||||
|
vt 0.0127 0.0508
|
||||||
|
vt -0.0127 -0.0508
|
||||||
|
vt -0.0127 0.0508
|
||||||
|
vt -0.039306734 0.0508
|
||||||
|
vt -0.039306734 -0.0508
|
||||||
|
vt 0.039306734 0.0508
|
||||||
|
vt 0.039306734 -0.0508
|
||||||
|
vt -0.04428355 0.0508
|
||||||
|
vt -0.04428355 -0.0508
|
||||||
|
vt 0.04428355 0.0508
|
||||||
|
vt 0.04428355 -0.0508
|
||||||
|
vt -0.045068305 0.0508
|
||||||
|
vt -0.045068305 -0.0508
|
||||||
|
vt 0.045068305 0.0508
|
||||||
|
vt 0.045068305 -0.0508
|
||||||
|
vt -0.00635 0.0508
|
||||||
|
vt -0.00635 -0.0508
|
||||||
|
vt 0.00635 0.0508
|
||||||
|
vt 0.00635 -0.0508
|
||||||
|
vt 0.04306616 -0.0508
|
||||||
|
vt 0.04306616 0.0508
|
||||||
|
vt -0.04306616 -0.0508
|
||||||
|
vt -0.04306616 0.0508
|
||||||
|
vt -0.027677217 -0.0508
|
||||||
|
vt 0.000000000000000048572257 -0.0508
|
||||||
|
vt 0.000000000000000048572257 0.0508
|
||||||
|
vt 0.055354435 -0.0508
|
||||||
|
vt 0.055354435 0.0508
|
||||||
|
vt -0.027677217 0.0508
|
||||||
|
vt -0.055354435 0.0508
|
||||||
|
vt -0.055354435 -0.0508
|
||||||
|
vt -0.02253807 0.0508
|
||||||
|
vt -0.04507614 0.0508
|
||||||
|
vt -0.04507614 -0.0508
|
||||||
|
vt 0.00000000000000005551115 0.0508
|
||||||
|
vt -0.02253807 -0.0508
|
||||||
|
vt 0.00000000000000005551115 -0.0508
|
||||||
|
vt 0.04507614 -0.0508
|
||||||
|
vt 0.04507614 0.0508
|
||||||
|
vt -0.047557 0.0508
|
||||||
|
vt -0.047557 -0.0508
|
||||||
|
vt 0.047557 0.0508
|
||||||
|
vt 0.047557 -0.0508
|
||||||
|
vt 0.04896476 -0.0508
|
||||||
|
vt 0.04896476 0.0508
|
||||||
|
vt -0.04896476 -0.0508
|
||||||
|
vt -0.04896476 0.0508
|
||||||
|
vt 0.03005076 -0.0508
|
||||||
|
vt 0.03005076 0.0508
|
||||||
|
vt -0.03005076 -0.0508
|
||||||
|
vt -0.03005076 0.0508
|
||||||
|
vt 0.04445 -0.0508
|
||||||
|
vt 0.04445 0.0508
|
||||||
|
vt -0.04445 -0.0508
|
||||||
|
vt -0.04445 0.0508
|
||||||
|
vt 0.08490671 0.009525
|
||||||
|
vt 0.06448028 0
|
||||||
|
vt 0.0889 0.0254
|
||||||
|
vt 0.08715213 -0.015875
|
||||||
|
vt 0.10982399 -0.03175
|
||||||
|
vt 0.07861347 -0.0254
|
||||||
|
vt 0.10533314 0.01905
|
||||||
|
vt 0.15116338 -0.0762
|
||||||
|
vt 0 -0.0254
|
||||||
|
vt 0 0
|
||||||
|
vt 0.2413 -0.0762
|
||||||
|
vt 0.15516768 -0.0635
|
||||||
|
vt 0.2413 -0.0635
|
||||||
|
vt 0.14337048 0.0508
|
||||||
|
vt 0.146186 0.0381
|
||||||
|
vt 0.2413 0.0381
|
||||||
|
vt 0.2413 0.0508
|
||||||
|
vt 0 0.0254
|
||||||
|
vn -1 -0 0
|
||||||
|
vn 0 -0 -1
|
||||||
|
vn -0.57357645 -0 -0.81915206
|
||||||
|
vn 1 -0 0
|
||||||
|
vn 0 -0 1
|
||||||
|
vn 0.57357645 -0 0.81915206
|
||||||
|
vn 0.42261827 -0 -0.9063078
|
||||||
|
vn -0.42261827 -0 0.9063078
|
||||||
|
vn -0 1 -0
|
||||||
|
vn 0 -1 0
|
||||||
|
o Unnamed-0
|
||||||
|
f 1/1/1 2/2/1 3/3/1
|
||||||
|
f 3/3/1 2/2/1 4/4/1
|
||||||
|
f 3/5/2 4/6/2 5/7/2
|
||||||
|
f 5/7/2 4/6/2 6/8/2
|
||||||
|
f 5/9/3 6/10/3 7/11/3
|
||||||
|
f 7/11/3 6/10/3 8/12/3
|
||||||
|
f 7/13/2 8/14/2 9/15/2
|
||||||
|
f 9/15/2 8/14/2 10/16/2
|
||||||
|
f 9/17/4 10/18/4 11/19/4
|
||||||
|
f 11/19/4 10/18/4 12/20/4
|
||||||
|
f 11/21/5 12/22/5 13/23/5
|
||||||
|
f 13/23/5 12/22/5 14/24/5
|
||||||
|
f 15/25/6 16/26/6 17/27/6
|
||||||
|
f 16/26/6 13/28/6 14/29/6
|
||||||
|
f 18/30/6 19/31/6 20/32/6
|
||||||
|
f 15/25/6 18/30/6 20/32/6
|
||||||
|
f 16/26/6 14/29/6 17/27/6
|
||||||
|
f 18/30/6 15/25/6 17/27/6
|
||||||
|
f 21/33/7 20/34/7 19/35/7
|
||||||
|
f 22/36/7 21/33/7 23/37/7
|
||||||
|
f 23/37/7 24/38/7 22/36/7
|
||||||
|
f 24/38/7 25/39/7 26/40/7
|
||||||
|
f 21/33/7 19/35/7 23/37/7
|
||||||
|
f 26/40/7 22/36/7 24/38/7
|
||||||
|
f 26/41/2 25/42/2 27/43/2
|
||||||
|
f 27/43/2 25/42/2 28/44/2
|
||||||
|
f 27/17/4 28/18/4 29/19/4
|
||||||
|
f 29/19/4 28/18/4 30/20/4
|
||||||
|
f 29/45/5 30/46/5 31/47/5
|
||||||
|
f 31/47/5 30/46/5 32/48/5
|
||||||
|
f 31/49/8 32/50/8 33/51/8
|
||||||
|
f 33/51/8 32/50/8 34/52/8
|
||||||
|
f 33/53/5 34/54/5 35/55/5
|
||||||
|
f 35/55/5 34/54/5 36/56/5
|
||||||
|
f 35/1/1 36/2/1 1/3/1
|
||||||
|
f 1/3/1 36/2/1 2/4/1
|
||||||
|
f 23/57/9 19/58/9 34/59/9
|
||||||
|
f 18/60/9 17/61/9 6/62/9
|
||||||
|
f 23/57/9 34/59/9 24/63/9
|
||||||
|
f 17/61/9 8/64/9 6/62/9
|
||||||
|
f 4/65/9 19/58/9 6/62/9
|
||||||
|
f 4/65/9 2/66/9 19/58/9
|
||||||
|
f 10/67/9 14/68/9 12/69/9
|
||||||
|
f 10/67/9 8/64/9 14/68/9
|
||||||
|
f 8/64/9 17/61/9 14/68/9
|
||||||
|
f 32/70/9 25/71/9 24/63/9
|
||||||
|
f 6/62/9 19/58/9 18/60/9
|
||||||
|
f 24/63/9 34/59/9 32/70/9
|
||||||
|
f 28/72/9 25/71/9 30/73/9
|
||||||
|
f 25/71/9 32/70/9 30/73/9
|
||||||
|
f 19/58/9 2/66/9 36/74/9
|
||||||
|
f 34/59/9 19/58/9 36/74/9
|
||||||
|
f 21/57/10 33/59/10 20/58/10
|
||||||
|
f 22/63/10 33/59/10 21/57/10
|
||||||
|
f 15/60/10 5/62/10 16/61/10
|
||||||
|
f 22/63/10 26/71/10 31/70/10
|
||||||
|
f 35/74/10 20/58/10 33/59/10
|
||||||
|
f 35/74/10 1/66/10 20/58/10
|
||||||
|
f 31/70/10 26/71/10 29/73/10
|
||||||
|
f 29/73/10 26/71/10 27/72/10
|
||||||
|
f 22/63/10 31/70/10 33/59/10
|
||||||
|
f 20/58/10 5/62/10 15/60/10
|
||||||
|
f 16/61/10 5/62/10 7/64/10
|
||||||
|
f 13/68/10 16/61/10 7/64/10
|
||||||
|
f 11/69/10 13/68/10 9/67/10
|
||||||
|
f 13/68/10 7/64/10 9/67/10
|
||||||
|
f 20/58/10 3/65/10 5/62/10
|
||||||
|
f 3/65/10 20/58/10 1/66/10
|
||||||
282
e2e/playwright/export-snapshots/ply-ascii.ply
Normal file
282
e2e/playwright/export-snapshots/ply-ascii.ply
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
ply
|
||||||
|
format ascii 1.0
|
||||||
|
comment Generated by kittycad.io
|
||||||
|
element vertex 204
|
||||||
|
property float x
|
||||||
|
property float y
|
||||||
|
property float z
|
||||||
|
element face 68
|
||||||
|
property list uchar uint vertex_indices
|
||||||
|
end_header
|
||||||
|
0 0 4
|
||||||
|
0 0 0
|
||||||
|
0 -1 4
|
||||||
|
0 -1 4
|
||||||
|
0 0 0
|
||||||
|
0 -1 0
|
||||||
|
0 -1 4
|
||||||
|
0 -1 0
|
||||||
|
3.0950184 -1 4
|
||||||
|
3.0950184 -1 4
|
||||||
|
0 -1 0
|
||||||
|
3.0950184 -1 0
|
||||||
|
3.0950184 -1 4
|
||||||
|
3.0950184 -1 0
|
||||||
|
5.9513144 -3 4
|
||||||
|
5.9513144 -3 4
|
||||||
|
3.0950184 -1 0
|
||||||
|
5.9513144 -3 0
|
||||||
|
5.9513144 -3 4
|
||||||
|
5.9513144 -3 0
|
||||||
|
9.5 -3 4
|
||||||
|
9.5 -3 4
|
||||||
|
5.9513144 -3 0
|
||||||
|
9.5 -3 0
|
||||||
|
9.5 -3 4
|
||||||
|
9.5 -3 0
|
||||||
|
9.5 -2.5 4
|
||||||
|
9.5 -2.5 4
|
||||||
|
9.5 -3 0
|
||||||
|
9.5 -2.5 0
|
||||||
|
9.5 -2.5 4
|
||||||
|
9.5 -2.5 0
|
||||||
|
6.108964 -2.5 4
|
||||||
|
6.108964 -2.5 4
|
||||||
|
9.5 -2.5 0
|
||||||
|
6.108964 -2.5 0
|
||||||
|
3.4311862 -0.625 4
|
||||||
|
4.323779 -1.25 4
|
||||||
|
4.323779 -1.25 0
|
||||||
|
4.323779 -1.25 4
|
||||||
|
6.108964 -2.5 4
|
||||||
|
6.108964 -2.5 0
|
||||||
|
3.4311862 -0.625 0
|
||||||
|
2.5385938 0 0
|
||||||
|
2.5385938 0 4
|
||||||
|
3.4311862 -0.625 4
|
||||||
|
3.4311862 -0.625 0
|
||||||
|
2.5385938 0 4
|
||||||
|
4.323779 -1.25 4
|
||||||
|
6.108964 -2.5 0
|
||||||
|
4.323779 -1.25 0
|
||||||
|
3.4311862 -0.625 0
|
||||||
|
3.4311862 -0.625 4
|
||||||
|
4.323779 -1.25 0
|
||||||
|
3.342784 0.375 4
|
||||||
|
2.5385938 0 4
|
||||||
|
2.5385938 0 0
|
||||||
|
4.146974 0.75 4
|
||||||
|
3.342784 0.375 4
|
||||||
|
3.342784 0.375 0
|
||||||
|
3.342784 0.375 0
|
||||||
|
4.146974 0.75 0
|
||||||
|
4.146974 0.75 4
|
||||||
|
4.146974 0.75 0
|
||||||
|
5.755354 1.5 0
|
||||||
|
5.755354 1.5 4
|
||||||
|
3.342784 0.375 4
|
||||||
|
2.5385938 0 0
|
||||||
|
3.342784 0.375 0
|
||||||
|
5.755354 1.5 4
|
||||||
|
4.146974 0.75 4
|
||||||
|
4.146974 0.75 0
|
||||||
|
5.755354 1.5 4
|
||||||
|
5.755354 1.5 0
|
||||||
|
9.5 1.5 4
|
||||||
|
9.5 1.5 4
|
||||||
|
5.755354 1.5 0
|
||||||
|
9.5 1.5 0
|
||||||
|
9.5 1.5 4
|
||||||
|
9.5 1.5 0
|
||||||
|
9.5 2 4
|
||||||
|
9.5 2 4
|
||||||
|
9.5 1.5 0
|
||||||
|
9.5 2 0
|
||||||
|
9.5 2 4
|
||||||
|
9.5 2 0
|
||||||
|
5.644507 2 4
|
||||||
|
5.644507 2 4
|
||||||
|
9.5 2 0
|
||||||
|
5.644507 2 0
|
||||||
|
5.644507 2 4
|
||||||
|
5.644507 2 0
|
||||||
|
3.5 1 4
|
||||||
|
3.5 1 4
|
||||||
|
5.644507 2 0
|
||||||
|
3.5 1 0
|
||||||
|
3.5 1 4
|
||||||
|
3.5 1 0
|
||||||
|
0 1 4
|
||||||
|
0 1 4
|
||||||
|
3.5 1 0
|
||||||
|
0 1 0
|
||||||
|
0 1 4
|
||||||
|
0 1 0
|
||||||
|
0 0 4
|
||||||
|
0 0 4
|
||||||
|
0 1 0
|
||||||
|
0 0 0
|
||||||
|
3.342784 0.375 0
|
||||||
|
2.5385938 0 0
|
||||||
|
3.5 1 0
|
||||||
|
3.4311862 -0.625 0
|
||||||
|
4.323779 -1.25 0
|
||||||
|
3.0950184 -1 0
|
||||||
|
3.342784 0.375 0
|
||||||
|
3.5 1 0
|
||||||
|
4.146974 0.75 0
|
||||||
|
4.323779 -1.25 0
|
||||||
|
5.9513144 -3 0
|
||||||
|
3.0950184 -1 0
|
||||||
|
0 -1 0
|
||||||
|
2.5385938 0 0
|
||||||
|
3.0950184 -1 0
|
||||||
|
0 -1 0
|
||||||
|
0 0 0
|
||||||
|
2.5385938 0 0
|
||||||
|
9.5 -3 0
|
||||||
|
6.108964 -2.5 0
|
||||||
|
9.5 -2.5 0
|
||||||
|
9.5 -3 0
|
||||||
|
5.9513144 -3 0
|
||||||
|
6.108964 -2.5 0
|
||||||
|
5.9513144 -3 0
|
||||||
|
4.323779 -1.25 0
|
||||||
|
6.108964 -2.5 0
|
||||||
|
5.644507 2 0
|
||||||
|
5.755354 1.5 0
|
||||||
|
4.146974 0.75 0
|
||||||
|
3.0950184 -1 0
|
||||||
|
2.5385938 0 0
|
||||||
|
3.4311862 -0.625 0
|
||||||
|
4.146974 0.75 0
|
||||||
|
3.5 1 0
|
||||||
|
5.644507 2 0
|
||||||
|
9.5 1.5 0
|
||||||
|
5.755354 1.5 0
|
||||||
|
9.5 2 0
|
||||||
|
5.755354 1.5 0
|
||||||
|
5.644507 2 0
|
||||||
|
9.5 2 0
|
||||||
|
2.5385938 0 0
|
||||||
|
0 0 0
|
||||||
|
0 1 0
|
||||||
|
3.5 1 0
|
||||||
|
2.5385938 0 0
|
||||||
|
0 1 0
|
||||||
|
3.342784 0.375 4
|
||||||
|
3.5 1 4
|
||||||
|
2.5385938 0 4
|
||||||
|
4.146974 0.75 4
|
||||||
|
3.5 1 4
|
||||||
|
3.342784 0.375 4
|
||||||
|
3.4311862 -0.625 4
|
||||||
|
3.0950184 -1 4
|
||||||
|
4.323779 -1.25 4
|
||||||
|
4.146974 0.75 4
|
||||||
|
5.755354 1.5 4
|
||||||
|
5.644507 2 4
|
||||||
|
0 1 4
|
||||||
|
2.5385938 0 4
|
||||||
|
3.5 1 4
|
||||||
|
0 1 4
|
||||||
|
0 0 4
|
||||||
|
2.5385938 0 4
|
||||||
|
5.644507 2 4
|
||||||
|
5.755354 1.5 4
|
||||||
|
9.5 2 4
|
||||||
|
9.5 2 4
|
||||||
|
5.755354 1.5 4
|
||||||
|
9.5 1.5 4
|
||||||
|
4.146974 0.75 4
|
||||||
|
5.644507 2 4
|
||||||
|
3.5 1 4
|
||||||
|
2.5385938 0 4
|
||||||
|
3.0950184 -1 4
|
||||||
|
3.4311862 -0.625 4
|
||||||
|
4.323779 -1.25 4
|
||||||
|
3.0950184 -1 4
|
||||||
|
5.9513144 -3 4
|
||||||
|
6.108964 -2.5 4
|
||||||
|
4.323779 -1.25 4
|
||||||
|
5.9513144 -3 4
|
||||||
|
9.5 -2.5 4
|
||||||
|
6.108964 -2.5 4
|
||||||
|
9.5 -3 4
|
||||||
|
6.108964 -2.5 4
|
||||||
|
5.9513144 -3 4
|
||||||
|
9.5 -3 4
|
||||||
|
2.5385938 0 4
|
||||||
|
0 -1 4
|
||||||
|
3.0950184 -1 4
|
||||||
|
0 -1 4
|
||||||
|
2.5385938 0 4
|
||||||
|
0 0 4
|
||||||
|
3 0 1 2
|
||||||
|
3 3 4 5
|
||||||
|
3 6 7 8
|
||||||
|
3 9 10 11
|
||||||
|
3 12 13 14
|
||||||
|
3 15 16 17
|
||||||
|
3 18 19 20
|
||||||
|
3 21 22 23
|
||||||
|
3 24 25 26
|
||||||
|
3 27 28 29
|
||||||
|
3 30 31 32
|
||||||
|
3 33 34 35
|
||||||
|
3 36 37 38
|
||||||
|
3 39 40 41
|
||||||
|
3 42 43 44
|
||||||
|
3 45 46 47
|
||||||
|
3 48 49 50
|
||||||
|
3 51 52 53
|
||||||
|
3 54 55 56
|
||||||
|
3 57 58 59
|
||||||
|
3 60 61 62
|
||||||
|
3 63 64 65
|
||||||
|
3 66 67 68
|
||||||
|
3 69 70 71
|
||||||
|
3 72 73 74
|
||||||
|
3 75 76 77
|
||||||
|
3 78 79 80
|
||||||
|
3 81 82 83
|
||||||
|
3 84 85 86
|
||||||
|
3 87 88 89
|
||||||
|
3 90 91 92
|
||||||
|
3 93 94 95
|
||||||
|
3 96 97 98
|
||||||
|
3 99 100 101
|
||||||
|
3 102 103 104
|
||||||
|
3 105 106 107
|
||||||
|
3 108 109 110
|
||||||
|
3 111 112 113
|
||||||
|
3 114 115 116
|
||||||
|
3 117 118 119
|
||||||
|
3 120 121 122
|
||||||
|
3 123 124 125
|
||||||
|
3 126 127 128
|
||||||
|
3 129 130 131
|
||||||
|
3 132 133 134
|
||||||
|
3 135 136 137
|
||||||
|
3 138 139 140
|
||||||
|
3 141 142 143
|
||||||
|
3 144 145 146
|
||||||
|
3 147 148 149
|
||||||
|
3 150 151 152
|
||||||
|
3 153 154 155
|
||||||
|
3 156 157 158
|
||||||
|
3 159 160 161
|
||||||
|
3 162 163 164
|
||||||
|
3 165 166 167
|
||||||
|
3 168 169 170
|
||||||
|
3 171 172 173
|
||||||
|
3 174 175 176
|
||||||
|
3 177 178 179
|
||||||
|
3 180 181 182
|
||||||
|
3 183 184 185
|
||||||
|
3 186 187 188
|
||||||
|
3 189 190 191
|
||||||
|
3 192 193 194
|
||||||
|
3 195 196 197
|
||||||
|
3 198 199 200
|
||||||
|
3 201 202 203
|
||||||
BIN
e2e/playwright/export-snapshots/ply-binary_big_endian.ply
Normal file
BIN
e2e/playwright/export-snapshots/ply-binary_big_endian.ply
Normal file
Binary file not shown.
BIN
e2e/playwright/export-snapshots/ply-binary_little_endian.ply
Normal file
BIN
e2e/playwright/export-snapshots/ply-binary_little_endian.ply
Normal file
Binary file not shown.
494
e2e/playwright/export-snapshots/step-.step
Normal file
494
e2e/playwright/export-snapshots/step-.step
Normal file
@ -0,0 +1,494 @@
|
|||||||
|
ISO-10303-21;
|
||||||
|
HEADER;
|
||||||
|
FILE_DESCRIPTION((('kittycad.io export')), '2;1');
|
||||||
|
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'kittycad.io beta', 'kittycad.io', 'Authorization unknown');
|
||||||
|
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
|
||||||
|
ENDSEC;
|
||||||
|
DATA;
|
||||||
|
#1 = (
|
||||||
|
LENGTH_UNIT()
|
||||||
|
NAMED_UNIT(*)
|
||||||
|
SI_UNIT($, .METRE.)
|
||||||
|
);
|
||||||
|
#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
|
||||||
|
#3 = (
|
||||||
|
GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
||||||
|
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2))
|
||||||
|
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1))
|
||||||
|
REPRESENTATION_CONTEXT('', '3D')
|
||||||
|
);
|
||||||
|
#4 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#5 = VERTEX_POINT('NONE', #4);
|
||||||
|
#6 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
||||||
|
#7 = VERTEX_POINT('NONE', #6);
|
||||||
|
#8 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
|
||||||
|
#9 = VERTEX_POINT('NONE', #8);
|
||||||
|
#10 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
||||||
|
#11 = VERTEX_POINT('NONE', #10);
|
||||||
|
#12 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
||||||
|
#13 = VERTEX_POINT('NONE', #12);
|
||||||
|
#14 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
|
||||||
|
#15 = VERTEX_POINT('NONE', #14);
|
||||||
|
#16 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
||||||
|
#17 = VERTEX_POINT('NONE', #16);
|
||||||
|
#18 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
|
||||||
|
#19 = VERTEX_POINT('NONE', #18);
|
||||||
|
#20 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
||||||
|
#21 = VERTEX_POINT('NONE', #20);
|
||||||
|
#22 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
|
||||||
|
#23 = VERTEX_POINT('NONE', #22);
|
||||||
|
#24 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
||||||
|
#25 = VERTEX_POINT('NONE', #24);
|
||||||
|
#26 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
|
||||||
|
#27 = VERTEX_POINT('NONE', #26);
|
||||||
|
#28 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
||||||
|
#29 = VERTEX_POINT('NONE', #28);
|
||||||
|
#30 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
|
||||||
|
#31 = VERTEX_POINT('NONE', #30);
|
||||||
|
#32 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
||||||
|
#33 = VERTEX_POINT('NONE', #32);
|
||||||
|
#34 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
|
||||||
|
#35 = VERTEX_POINT('NONE', #34);
|
||||||
|
#36 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
||||||
|
#37 = VERTEX_POINT('NONE', #36);
|
||||||
|
#38 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
|
||||||
|
#39 = VERTEX_POINT('NONE', #38);
|
||||||
|
#40 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
||||||
|
#41 = VERTEX_POINT('NONE', #40);
|
||||||
|
#42 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
|
||||||
|
#43 = VERTEX_POINT('NONE', #42);
|
||||||
|
#44 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
||||||
|
#45 = VERTEX_POINT('NONE', #44);
|
||||||
|
#46 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
|
||||||
|
#47 = VERTEX_POINT('NONE', #46);
|
||||||
|
#48 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
||||||
|
#49 = VERTEX_POINT('NONE', #48);
|
||||||
|
#50 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
|
||||||
|
#51 = VERTEX_POINT('NONE', #50);
|
||||||
|
#52 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
||||||
|
#53 = VERTEX_POINT('NONE', #52);
|
||||||
|
#54 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
|
||||||
|
#55 = VERTEX_POINT('NONE', #54);
|
||||||
|
#56 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
||||||
|
#57 = VERTEX_POINT('NONE', #56);
|
||||||
|
#58 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
|
||||||
|
#59 = VERTEX_POINT('NONE', #58);
|
||||||
|
#60 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#61 = VECTOR('NONE', #60, 0.0254);
|
||||||
|
#62 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#63 = LINE('NONE', #62, #61);
|
||||||
|
#64 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#65 = VECTOR('NONE', #64, 0.1016);
|
||||||
|
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
||||||
|
#67 = LINE('NONE', #66, #65);
|
||||||
|
#68 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#69 = VECTOR('NONE', #68, 0.0254);
|
||||||
|
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
||||||
|
#71 = LINE('NONE', #70, #69);
|
||||||
|
#72 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#73 = VECTOR('NONE', #72, 0.1016);
|
||||||
|
#74 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#75 = LINE('NONE', #74, #73);
|
||||||
|
#76 = DIRECTION('NONE', (1, 0, 0));
|
||||||
|
#77 = VECTOR('NONE', #76, 0.07861346939195568);
|
||||||
|
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
||||||
|
#79 = LINE('NONE', #78, #77);
|
||||||
|
#80 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#81 = VECTOR('NONE', #80, 0.1016);
|
||||||
|
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
||||||
|
#83 = LINE('NONE', #82, #81);
|
||||||
|
#84 = DIRECTION('NONE', (1, 0, 0));
|
||||||
|
#85 = VECTOR('NONE', #84, 0.07861346939195568);
|
||||||
|
#86 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
|
||||||
|
#87 = LINE('NONE', #86, #85);
|
||||||
|
#88 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
|
||||||
|
#89 = VECTOR('NONE', #88, 0.08856709721755177);
|
||||||
|
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
||||||
|
#91 = LINE('NONE', #90, #89);
|
||||||
|
#92 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#93 = VECTOR('NONE', #92, 0.1016);
|
||||||
|
#94 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
||||||
|
#95 = LINE('NONE', #94, #93);
|
||||||
|
#96 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
|
||||||
|
#97 = VECTOR('NONE', #96, 0.08856709721755177);
|
||||||
|
#98 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
|
||||||
|
#99 = LINE('NONE', #98, #97);
|
||||||
|
#100 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
|
||||||
|
#101 = VECTOR('NONE', #100, 0.09013661186554489);
|
||||||
|
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
||||||
|
#103 = LINE('NONE', #102, #101);
|
||||||
|
#104 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#105 = VECTOR('NONE', #104, 0.1016);
|
||||||
|
#106 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
||||||
|
#107 = LINE('NONE', #106, #105);
|
||||||
|
#108 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
|
||||||
|
#109 = VECTOR('NONE', #108, 0.09013661186554489);
|
||||||
|
#110 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
|
||||||
|
#111 = LINE('NONE', #110, #109);
|
||||||
|
#112 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#113 = VECTOR('NONE', #112, 0.012700000000000003);
|
||||||
|
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
||||||
|
#115 = LINE('NONE', #114, #113);
|
||||||
|
#116 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#117 = VECTOR('NONE', #116, 0.1016);
|
||||||
|
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
||||||
|
#119 = LINE('NONE', #118, #117);
|
||||||
|
#120 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#121 = VECTOR('NONE', #120, 0.012700000000000003);
|
||||||
|
#122 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
|
||||||
|
#123 = LINE('NONE', #122, #121);
|
||||||
|
#124 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#125 = VECTOR('NONE', #124, 0.08613231724678178);
|
||||||
|
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
||||||
|
#127 = LINE('NONE', #126, #125);
|
||||||
|
#128 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#129 = VECTOR('NONE', #128, 0.1016);
|
||||||
|
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
||||||
|
#131 = LINE('NONE', #130, #129);
|
||||||
|
#132 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#133 = VECTOR('NONE', #132, 0.08613231724678178);
|
||||||
|
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
|
||||||
|
#135 = LINE('NONE', #134, #133);
|
||||||
|
#136 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
|
||||||
|
#137 = VECTOR('NONE', #136, 0.11070887152193974);
|
||||||
|
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
||||||
|
#139 = LINE('NONE', #138, #137);
|
||||||
|
#140 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#141 = VECTOR('NONE', #140, 0.1016);
|
||||||
|
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
||||||
|
#143 = LINE('NONE', #142, #141);
|
||||||
|
#144 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
|
||||||
|
#145 = VECTOR('NONE', #144, 0.11070887152193974);
|
||||||
|
#146 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
|
||||||
|
#147 = LINE('NONE', #146, #145);
|
||||||
|
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
|
||||||
|
#149 = VECTOR('NONE', #148, 0.09015228031811025);
|
||||||
|
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
||||||
|
#151 = LINE('NONE', #150, #149);
|
||||||
|
#152 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#153 = VECTOR('NONE', #152, 0.1016);
|
||||||
|
#154 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
||||||
|
#155 = LINE('NONE', #154, #153);
|
||||||
|
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
|
||||||
|
#157 = VECTOR('NONE', #156, 0.09015228031811025);
|
||||||
|
#158 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
|
||||||
|
#159 = LINE('NONE', #158, #157);
|
||||||
|
#160 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
|
||||||
|
#161 = VECTOR('NONE', #160, 0.09511400200349182);
|
||||||
|
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
||||||
|
#163 = LINE('NONE', #162, #161);
|
||||||
|
#164 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#165 = VECTOR('NONE', #164, 0.1016);
|
||||||
|
#166 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
||||||
|
#167 = LINE('NONE', #166, #165);
|
||||||
|
#168 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
|
||||||
|
#169 = VECTOR('NONE', #168, 0.09511400200349182);
|
||||||
|
#170 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
|
||||||
|
#171 = LINE('NONE', #170, #169);
|
||||||
|
#172 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#173 = VECTOR('NONE', #172, 0.012699999999999996);
|
||||||
|
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
||||||
|
#175 = LINE('NONE', #174, #173);
|
||||||
|
#176 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#177 = VECTOR('NONE', #176, 0.1016);
|
||||||
|
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
||||||
|
#179 = LINE('NONE', #178, #177);
|
||||||
|
#180 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#181 = VECTOR('NONE', #180, 0.012699999999999996);
|
||||||
|
#182 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
|
||||||
|
#183 = LINE('NONE', #182, #181);
|
||||||
|
#184 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#185 = VECTOR('NONE', #184, 0.0979295242190572);
|
||||||
|
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
||||||
|
#187 = LINE('NONE', #186, #185);
|
||||||
|
#188 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#189 = VECTOR('NONE', #188, 0.1016);
|
||||||
|
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
||||||
|
#191 = LINE('NONE', #190, #189);
|
||||||
|
#192 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#193 = VECTOR('NONE', #192, 0.0979295242190572);
|
||||||
|
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
|
||||||
|
#195 = LINE('NONE', #194, #193);
|
||||||
|
#196 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
|
||||||
|
#197 = VECTOR('NONE', #196, 0.06010152021207346);
|
||||||
|
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
||||||
|
#199 = LINE('NONE', #198, #197);
|
||||||
|
#200 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#201 = VECTOR('NONE', #200, 0.1016);
|
||||||
|
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
||||||
|
#203 = LINE('NONE', #202, #201);
|
||||||
|
#204 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
|
||||||
|
#205 = VECTOR('NONE', #204, 0.06010152021207346);
|
||||||
|
#206 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
|
||||||
|
#207 = LINE('NONE', #206, #205);
|
||||||
|
#208 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#209 = VECTOR('NONE', #208, 0.08889999999999999);
|
||||||
|
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
||||||
|
#211 = LINE('NONE', #210, #209);
|
||||||
|
#212 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#213 = VECTOR('NONE', #212, 0.1016);
|
||||||
|
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
||||||
|
#215 = LINE('NONE', #214, #213);
|
||||||
|
#216 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#217 = VECTOR('NONE', #216, 0.08889999999999999);
|
||||||
|
#218 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
|
||||||
|
#219 = LINE('NONE', #218, #217);
|
||||||
|
#220 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#221 = VECTOR('NONE', #220, 0.0254);
|
||||||
|
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
||||||
|
#223 = LINE('NONE', #222, #221);
|
||||||
|
#224 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#225 = VECTOR('NONE', #224, 0.0254);
|
||||||
|
#226 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
|
||||||
|
#227 = LINE('NONE', #226, #225);
|
||||||
|
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);
|
||||||
|
#229 = EDGE_CURVE('NONE', #7, #9, #67, .T.);
|
||||||
|
#230 = EDGE_CURVE('NONE', #11, #9, #71, .T.);
|
||||||
|
#231 = EDGE_CURVE('NONE', #5, #11, #75, .T.);
|
||||||
|
#232 = EDGE_CURVE('NONE', #7, #13, #79, .T.);
|
||||||
|
#233 = EDGE_CURVE('NONE', #13, #15, #83, .T.);
|
||||||
|
#234 = EDGE_CURVE('NONE', #9, #15, #87, .T.);
|
||||||
|
#235 = EDGE_CURVE('NONE', #13, #17, #91, .T.);
|
||||||
|
#236 = EDGE_CURVE('NONE', #17, #19, #95, .T.);
|
||||||
|
#237 = EDGE_CURVE('NONE', #15, #19, #99, .T.);
|
||||||
|
#238 = EDGE_CURVE('NONE', #17, #21, #103, .T.);
|
||||||
|
#239 = EDGE_CURVE('NONE', #21, #23, #107, .T.);
|
||||||
|
#240 = EDGE_CURVE('NONE', #19, #23, #111, .T.);
|
||||||
|
#241 = EDGE_CURVE('NONE', #21, #25, #115, .T.);
|
||||||
|
#242 = EDGE_CURVE('NONE', #25, #27, #119, .T.);
|
||||||
|
#243 = EDGE_CURVE('NONE', #23, #27, #123, .T.);
|
||||||
|
#244 = EDGE_CURVE('NONE', #25, #29, #127, .T.);
|
||||||
|
#245 = EDGE_CURVE('NONE', #29, #31, #131, .T.);
|
||||||
|
#246 = EDGE_CURVE('NONE', #27, #31, #135, .T.);
|
||||||
|
#247 = EDGE_CURVE('NONE', #29, #33, #139, .T.);
|
||||||
|
#248 = EDGE_CURVE('NONE', #33, #35, #143, .T.);
|
||||||
|
#249 = EDGE_CURVE('NONE', #31, #35, #147, .T.);
|
||||||
|
#250 = EDGE_CURVE('NONE', #33, #37, #151, .T.);
|
||||||
|
#251 = EDGE_CURVE('NONE', #37, #39, #155, .T.);
|
||||||
|
#252 = EDGE_CURVE('NONE', #35, #39, #159, .T.);
|
||||||
|
#253 = EDGE_CURVE('NONE', #37, #41, #163, .T.);
|
||||||
|
#254 = EDGE_CURVE('NONE', #41, #43, #167, .T.);
|
||||||
|
#255 = EDGE_CURVE('NONE', #39, #43, #171, .T.);
|
||||||
|
#256 = EDGE_CURVE('NONE', #41, #45, #175, .T.);
|
||||||
|
#257 = EDGE_CURVE('NONE', #45, #47, #179, .T.);
|
||||||
|
#258 = EDGE_CURVE('NONE', #43, #47, #183, .T.);
|
||||||
|
#259 = EDGE_CURVE('NONE', #45, #49, #187, .T.);
|
||||||
|
#260 = EDGE_CURVE('NONE', #49, #51, #191, .T.);
|
||||||
|
#261 = EDGE_CURVE('NONE', #47, #51, #195, .T.);
|
||||||
|
#262 = EDGE_CURVE('NONE', #49, #53, #199, .T.);
|
||||||
|
#263 = EDGE_CURVE('NONE', #53, #55, #203, .T.);
|
||||||
|
#264 = EDGE_CURVE('NONE', #51, #55, #207, .T.);
|
||||||
|
#265 = EDGE_CURVE('NONE', #53, #57, #211, .T.);
|
||||||
|
#266 = EDGE_CURVE('NONE', #57, #59, #215, .T.);
|
||||||
|
#267 = EDGE_CURVE('NONE', #55, #59, #219, .T.);
|
||||||
|
#268 = EDGE_CURVE('NONE', #57, #5, #223, .T.);
|
||||||
|
#269 = EDGE_CURVE('NONE', #59, #11, #227, .T.);
|
||||||
|
#270 = ORIENTED_EDGE('NONE', *, *, #228, .T.);
|
||||||
|
#271 = ORIENTED_EDGE('NONE', *, *, #229, .T.);
|
||||||
|
#272 = ORIENTED_EDGE('NONE', *, *, #230, .F.);
|
||||||
|
#273 = ORIENTED_EDGE('NONE', *, *, #231, .F.);
|
||||||
|
#274 = EDGE_LOOP('NONE', (#270, #271, #272, #273));
|
||||||
|
#275 = ORIENTED_EDGE('NONE', *, *, #232, .T.);
|
||||||
|
#276 = ORIENTED_EDGE('NONE', *, *, #233, .T.);
|
||||||
|
#277 = ORIENTED_EDGE('NONE', *, *, #234, .F.);
|
||||||
|
#278 = ORIENTED_EDGE('NONE', *, *, #229, .F.);
|
||||||
|
#279 = EDGE_LOOP('NONE', (#275, #276, #277, #278));
|
||||||
|
#280 = ORIENTED_EDGE('NONE', *, *, #235, .T.);
|
||||||
|
#281 = ORIENTED_EDGE('NONE', *, *, #236, .T.);
|
||||||
|
#282 = ORIENTED_EDGE('NONE', *, *, #237, .F.);
|
||||||
|
#283 = ORIENTED_EDGE('NONE', *, *, #233, .F.);
|
||||||
|
#284 = EDGE_LOOP('NONE', (#280, #281, #282, #283));
|
||||||
|
#285 = ORIENTED_EDGE('NONE', *, *, #238, .T.);
|
||||||
|
#286 = ORIENTED_EDGE('NONE', *, *, #239, .T.);
|
||||||
|
#287 = ORIENTED_EDGE('NONE', *, *, #240, .F.);
|
||||||
|
#288 = ORIENTED_EDGE('NONE', *, *, #236, .F.);
|
||||||
|
#289 = EDGE_LOOP('NONE', (#285, #286, #287, #288));
|
||||||
|
#290 = ORIENTED_EDGE('NONE', *, *, #241, .T.);
|
||||||
|
#291 = ORIENTED_EDGE('NONE', *, *, #242, .T.);
|
||||||
|
#292 = ORIENTED_EDGE('NONE', *, *, #243, .F.);
|
||||||
|
#293 = ORIENTED_EDGE('NONE', *, *, #239, .F.);
|
||||||
|
#294 = EDGE_LOOP('NONE', (#290, #291, #292, #293));
|
||||||
|
#295 = ORIENTED_EDGE('NONE', *, *, #244, .T.);
|
||||||
|
#296 = ORIENTED_EDGE('NONE', *, *, #245, .T.);
|
||||||
|
#297 = ORIENTED_EDGE('NONE', *, *, #246, .F.);
|
||||||
|
#298 = ORIENTED_EDGE('NONE', *, *, #242, .F.);
|
||||||
|
#299 = EDGE_LOOP('NONE', (#295, #296, #297, #298));
|
||||||
|
#300 = ORIENTED_EDGE('NONE', *, *, #247, .T.);
|
||||||
|
#301 = ORIENTED_EDGE('NONE', *, *, #248, .T.);
|
||||||
|
#302 = ORIENTED_EDGE('NONE', *, *, #249, .F.);
|
||||||
|
#303 = ORIENTED_EDGE('NONE', *, *, #245, .F.);
|
||||||
|
#304 = EDGE_LOOP('NONE', (#300, #301, #302, #303));
|
||||||
|
#305 = ORIENTED_EDGE('NONE', *, *, #250, .T.);
|
||||||
|
#306 = ORIENTED_EDGE('NONE', *, *, #251, .T.);
|
||||||
|
#307 = ORIENTED_EDGE('NONE', *, *, #252, .F.);
|
||||||
|
#308 = ORIENTED_EDGE('NONE', *, *, #248, .F.);
|
||||||
|
#309 = EDGE_LOOP('NONE', (#305, #306, #307, #308));
|
||||||
|
#310 = ORIENTED_EDGE('NONE', *, *, #253, .T.);
|
||||||
|
#311 = ORIENTED_EDGE('NONE', *, *, #254, .T.);
|
||||||
|
#312 = ORIENTED_EDGE('NONE', *, *, #255, .F.);
|
||||||
|
#313 = ORIENTED_EDGE('NONE', *, *, #251, .F.);
|
||||||
|
#314 = EDGE_LOOP('NONE', (#310, #311, #312, #313));
|
||||||
|
#315 = ORIENTED_EDGE('NONE', *, *, #256, .T.);
|
||||||
|
#316 = ORIENTED_EDGE('NONE', *, *, #257, .T.);
|
||||||
|
#317 = ORIENTED_EDGE('NONE', *, *, #258, .F.);
|
||||||
|
#318 = ORIENTED_EDGE('NONE', *, *, #254, .F.);
|
||||||
|
#319 = EDGE_LOOP('NONE', (#315, #316, #317, #318));
|
||||||
|
#320 = ORIENTED_EDGE('NONE', *, *, #259, .T.);
|
||||||
|
#321 = ORIENTED_EDGE('NONE', *, *, #260, .T.);
|
||||||
|
#322 = ORIENTED_EDGE('NONE', *, *, #261, .F.);
|
||||||
|
#323 = ORIENTED_EDGE('NONE', *, *, #257, .F.);
|
||||||
|
#324 = EDGE_LOOP('NONE', (#320, #321, #322, #323));
|
||||||
|
#325 = ORIENTED_EDGE('NONE', *, *, #262, .T.);
|
||||||
|
#326 = ORIENTED_EDGE('NONE', *, *, #263, .T.);
|
||||||
|
#327 = ORIENTED_EDGE('NONE', *, *, #264, .F.);
|
||||||
|
#328 = ORIENTED_EDGE('NONE', *, *, #260, .F.);
|
||||||
|
#329 = EDGE_LOOP('NONE', (#325, #326, #327, #328));
|
||||||
|
#330 = ORIENTED_EDGE('NONE', *, *, #265, .T.);
|
||||||
|
#331 = ORIENTED_EDGE('NONE', *, *, #266, .T.);
|
||||||
|
#332 = ORIENTED_EDGE('NONE', *, *, #267, .F.);
|
||||||
|
#333 = ORIENTED_EDGE('NONE', *, *, #263, .F.);
|
||||||
|
#334 = EDGE_LOOP('NONE', (#330, #331, #332, #333));
|
||||||
|
#335 = ORIENTED_EDGE('NONE', *, *, #268, .T.);
|
||||||
|
#336 = ORIENTED_EDGE('NONE', *, *, #231, .T.);
|
||||||
|
#337 = ORIENTED_EDGE('NONE', *, *, #269, .F.);
|
||||||
|
#338 = ORIENTED_EDGE('NONE', *, *, #266, .F.);
|
||||||
|
#339 = EDGE_LOOP('NONE', (#335, #336, #337, #338));
|
||||||
|
#340 = ORIENTED_EDGE('NONE', *, *, #228, .T.);
|
||||||
|
#341 = ORIENTED_EDGE('NONE', *, *, #232, .T.);
|
||||||
|
#342 = ORIENTED_EDGE('NONE', *, *, #235, .T.);
|
||||||
|
#343 = ORIENTED_EDGE('NONE', *, *, #238, .T.);
|
||||||
|
#344 = ORIENTED_EDGE('NONE', *, *, #241, .T.);
|
||||||
|
#345 = ORIENTED_EDGE('NONE', *, *, #244, .T.);
|
||||||
|
#346 = ORIENTED_EDGE('NONE', *, *, #247, .T.);
|
||||||
|
#347 = ORIENTED_EDGE('NONE', *, *, #250, .T.);
|
||||||
|
#348 = ORIENTED_EDGE('NONE', *, *, #253, .T.);
|
||||||
|
#349 = ORIENTED_EDGE('NONE', *, *, #256, .T.);
|
||||||
|
#350 = ORIENTED_EDGE('NONE', *, *, #259, .T.);
|
||||||
|
#351 = ORIENTED_EDGE('NONE', *, *, #262, .T.);
|
||||||
|
#352 = ORIENTED_EDGE('NONE', *, *, #265, .T.);
|
||||||
|
#353 = ORIENTED_EDGE('NONE', *, *, #268, .T.);
|
||||||
|
#354 = EDGE_LOOP('NONE', (#340, #341, #342, #343, #344, #345, #346, #347, #348, #349, #350, #351, #352, #353));
|
||||||
|
#355 = ORIENTED_EDGE('NONE', *, *, #230, .T.);
|
||||||
|
#356 = ORIENTED_EDGE('NONE', *, *, #234, .T.);
|
||||||
|
#357 = ORIENTED_EDGE('NONE', *, *, #237, .T.);
|
||||||
|
#358 = ORIENTED_EDGE('NONE', *, *, #240, .T.);
|
||||||
|
#359 = ORIENTED_EDGE('NONE', *, *, #243, .T.);
|
||||||
|
#360 = ORIENTED_EDGE('NONE', *, *, #246, .T.);
|
||||||
|
#361 = ORIENTED_EDGE('NONE', *, *, #249, .T.);
|
||||||
|
#362 = ORIENTED_EDGE('NONE', *, *, #252, .T.);
|
||||||
|
#363 = ORIENTED_EDGE('NONE', *, *, #255, .T.);
|
||||||
|
#364 = ORIENTED_EDGE('NONE', *, *, #258, .T.);
|
||||||
|
#365 = ORIENTED_EDGE('NONE', *, *, #261, .T.);
|
||||||
|
#366 = ORIENTED_EDGE('NONE', *, *, #264, .T.);
|
||||||
|
#367 = ORIENTED_EDGE('NONE', *, *, #267, .T.);
|
||||||
|
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
|
||||||
|
#369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368));
|
||||||
|
#370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508));
|
||||||
|
#371 = DIRECTION('NONE', (-1, 0, -0));
|
||||||
|
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
|
||||||
|
#373 = PLANE('NONE', #372);
|
||||||
|
#374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508));
|
||||||
|
#375 = DIRECTION('NONE', (0, -1, -0));
|
||||||
|
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
|
||||||
|
#377 = PLANE('NONE', #376);
|
||||||
|
#378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999));
|
||||||
|
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.819152044288992, 0));
|
||||||
|
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
|
||||||
|
#381 = PLANE('NONE', #380);
|
||||||
|
#382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508));
|
||||||
|
#383 = DIRECTION('NONE', (0, -1, -0));
|
||||||
|
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
|
||||||
|
#385 = PLANE('NONE', #384);
|
||||||
|
#386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508));
|
||||||
|
#387 = DIRECTION('NONE', (1, 0, -0));
|
||||||
|
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
|
||||||
|
#389 = PLANE('NONE', #388);
|
||||||
|
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
|
||||||
|
#391 = DIRECTION('NONE', (0, 1, -0));
|
||||||
|
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
|
||||||
|
#393 = PLANE('NONE', #392);
|
||||||
|
#394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508));
|
||||||
|
#395 = DIRECTION('NONE', (0.573576436351046, 0.8191520442889918, -0));
|
||||||
|
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
|
||||||
|
#397 = PLANE('NONE', #396);
|
||||||
|
#398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508));
|
||||||
|
#399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, -0));
|
||||||
|
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
|
||||||
|
#401 = PLANE('NONE', #400);
|
||||||
|
#402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508));
|
||||||
|
#403 = DIRECTION('NONE', (0, -1, -0));
|
||||||
|
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
|
||||||
|
#405 = PLANE('NONE', #404);
|
||||||
|
#406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508));
|
||||||
|
#407 = DIRECTION('NONE', (1, 0, -0));
|
||||||
|
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
|
||||||
|
#409 = PLANE('NONE', #408);
|
||||||
|
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
|
||||||
|
#411 = DIRECTION('NONE', (0, 1, -0));
|
||||||
|
#412 = AXIS2_PLACEMENT_3D('NONE', #410, #411, $);
|
||||||
|
#413 = PLANE('NONE', #412);
|
||||||
|
#414 = CARTESIAN_POINT('NONE', (0.11613523789047137, 0.0381, 0.05079999999999999));
|
||||||
|
#415 = DIRECTION('NONE', (-0.42261826174069966, 0.90630778703665, -0));
|
||||||
|
#416 = AXIS2_PLACEMENT_3D('NONE', #414, #415, $);
|
||||||
|
#417 = PLANE('NONE', #416);
|
||||||
|
#418 = CARTESIAN_POINT('NONE', (0.044449999999999996, 0.0254, 0.0508));
|
||||||
|
#419 = DIRECTION('NONE', (0, 1, -0));
|
||||||
|
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
|
||||||
|
#421 = PLANE('NONE', #420);
|
||||||
|
#422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508));
|
||||||
|
#423 = DIRECTION('NONE', (-1, 0, -0));
|
||||||
|
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
|
||||||
|
#425 = PLANE('NONE', #424);
|
||||||
|
#426 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#427 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#428 = AXIS2_PLACEMENT_3D('NONE', #426, #427, $);
|
||||||
|
#429 = PLANE('NONE', #428);
|
||||||
|
#430 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
||||||
|
#431 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#432 = AXIS2_PLACEMENT_3D('NONE', #430, #431, $);
|
||||||
|
#433 = PLANE('NONE', #432);
|
||||||
|
#434 = FACE_OUTER_BOUND('NONE', #274, .T.);
|
||||||
|
#435 = ADVANCED_FACE('NONE', (#434), #373, .T.);
|
||||||
|
#436 = FACE_OUTER_BOUND('NONE', #279, .T.);
|
||||||
|
#437 = ADVANCED_FACE('NONE', (#436), #377, .T.);
|
||||||
|
#438 = FACE_OUTER_BOUND('NONE', #284, .T.);
|
||||||
|
#439 = ADVANCED_FACE('NONE', (#438), #381, .T.);
|
||||||
|
#440 = FACE_OUTER_BOUND('NONE', #289, .T.);
|
||||||
|
#441 = ADVANCED_FACE('NONE', (#440), #385, .T.);
|
||||||
|
#442 = FACE_OUTER_BOUND('NONE', #294, .T.);
|
||||||
|
#443 = ADVANCED_FACE('NONE', (#442), #389, .T.);
|
||||||
|
#444 = FACE_OUTER_BOUND('NONE', #299, .T.);
|
||||||
|
#445 = ADVANCED_FACE('NONE', (#444), #393, .T.);
|
||||||
|
#446 = FACE_OUTER_BOUND('NONE', #304, .T.);
|
||||||
|
#447 = ADVANCED_FACE('NONE', (#446), #397, .T.);
|
||||||
|
#448 = FACE_OUTER_BOUND('NONE', #309, .T.);
|
||||||
|
#449 = ADVANCED_FACE('NONE', (#448), #401, .T.);
|
||||||
|
#450 = FACE_OUTER_BOUND('NONE', #314, .T.);
|
||||||
|
#451 = ADVANCED_FACE('NONE', (#450), #405, .T.);
|
||||||
|
#452 = FACE_OUTER_BOUND('NONE', #319, .T.);
|
||||||
|
#453 = ADVANCED_FACE('NONE', (#452), #409, .T.);
|
||||||
|
#454 = FACE_OUTER_BOUND('NONE', #324, .T.);
|
||||||
|
#455 = ADVANCED_FACE('NONE', (#454), #413, .T.);
|
||||||
|
#456 = FACE_OUTER_BOUND('NONE', #329, .T.);
|
||||||
|
#457 = ADVANCED_FACE('NONE', (#456), #417, .T.);
|
||||||
|
#458 = FACE_OUTER_BOUND('NONE', #334, .T.);
|
||||||
|
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
|
||||||
|
#460 = FACE_OUTER_BOUND('NONE', #339, .T.);
|
||||||
|
#461 = ADVANCED_FACE('NONE', (#460), #425, .T.);
|
||||||
|
#462 = FACE_OUTER_BOUND('NONE', #354, .T.);
|
||||||
|
#463 = ADVANCED_FACE('NONE', (#462), #429, .F.);
|
||||||
|
#464 = FACE_OUTER_BOUND('NONE', #369, .T.);
|
||||||
|
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.);
|
||||||
|
#466 = CLOSED_SHELL('NONE', (#435, #437, #439, #441, #443, #445, #447, #449, #451, #453, #455, #457, #459, #461, #463, #465));
|
||||||
|
#467 = ORIENTED_CLOSED_SHELL('NONE', *, #466, .T.);
|
||||||
|
#468 = MANIFOLD_SOLID_BREP('NONE', #467);
|
||||||
|
#469 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
|
||||||
|
#470 = PRODUCT_DEFINITION_CONTEXT('part definition', #469, 'design');
|
||||||
|
#471 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
|
||||||
|
#472 = PRODUCT_DEFINITION_FORMATION('', $, #471);
|
||||||
|
#473 = PRODUCT_DEFINITION('design', $, #472, #470);
|
||||||
|
#474 = PRODUCT_DEFINITION_SHAPE('NONE', $, #473);
|
||||||
|
#475 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#468), #3);
|
||||||
|
#476 = SHAPE_DEFINITION_REPRESENTATION(#474, #475);
|
||||||
|
ENDSEC;
|
||||||
|
END-ISO-10303-21;
|
||||||
478
e2e/playwright/export-snapshots/stl-ascii.stl
Normal file
478
e2e/playwright/export-snapshots/stl-ascii.stl
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
solid unnamed
|
||||||
|
facet normal -1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 0
|
||||||
|
vertex 0 -0 0
|
||||||
|
vertex 0 -4 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 -1
|
||||||
|
vertex 0 -0 0
|
||||||
|
vertex 0 -0 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 -1
|
||||||
|
vertex 0 -0 -1
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
vertex 0 -0 -1
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0.57357645 0 -0.81915206
|
||||||
|
outer loop
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0.57357645 0 -0.81915206
|
||||||
|
outer loop
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
vertex 9.5 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 -3
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
vertex 9.5 -0 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 -3
|
||||||
|
vertex 9.5 -0 -3
|
||||||
|
vertex 9.5 -4 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 -0 0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 -2.5
|
||||||
|
vertex 9.5 -0 -3
|
||||||
|
vertex 9.5 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0 0.99999994
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 -2.5
|
||||||
|
vertex 9.5 -0 -2.5
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 0.99999994
|
||||||
|
outer loop
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
vertex 9.5 -0 -2.5
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.5735763 0 0.8191522
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -4 -0.625
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -0 -0.625
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 -0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -4 -0.625
|
||||||
|
vertex 3.4311862 -0 -0.625
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 -0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.5735763 0 0.8191522
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -0 -0.625
|
||||||
|
vertex 3.4311862 -4 -0.625
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261824 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -4 0.375
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261824 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
vertex 3.342784 -4 0.375
|
||||||
|
vertex 3.342784 -0 0.375
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261824 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -0 0.375
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261833 0 -0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261824 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -4 0.375
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 3.342784 -0 0.375
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261833 0 -0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 9.5 -4 1.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 1.5
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 9.5 -0 1.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 1.5
|
||||||
|
vertex 9.5 -0 1.5
|
||||||
|
vertex 9.5 -4 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 -0 0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 2
|
||||||
|
vertex 9.5 -0 1.5
|
||||||
|
vertex 9.5 -0 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0 1
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 2
|
||||||
|
vertex 9.5 -0 2
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 1
|
||||||
|
outer loop
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
vertex 9.5 -0 2
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0.42261824 0 0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0.42261824 0 0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0 1
|
||||||
|
outer loop
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
vertex 0 -4 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 1
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 1
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
vertex 0 -0 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 1
|
||||||
|
vertex 0 -0 1
|
||||||
|
vertex 0 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 0
|
||||||
|
vertex 0 -0 1
|
||||||
|
vertex 0 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -0 0.375
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -0 -0.625
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -0 0.375
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0.99999994 0
|
||||||
|
outer loop
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -0 -1
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -0 -1
|
||||||
|
vertex 0 -0 0
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0.99999994 -0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -0 -3
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
vertex 9.5 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -0 -3
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 5.9513144 -0 -3
|
||||||
|
vertex 4.323779 -0 -1.25
|
||||||
|
vertex 6.108964 -0 -2.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0.99999994 -0
|
||||||
|
outer loop
|
||||||
|
vertex 3.0950184 -0 -1
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 3.4311862 -0 -0.625
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -0 0.75
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -0 1.5
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 9.5 -0 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 5.755354 -0 1.5
|
||||||
|
vertex 5.644507 -0 2
|
||||||
|
vertex 9.5 -0 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 0 -0 0
|
||||||
|
vertex 0 -0 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 3.5 -0 1
|
||||||
|
vertex 2.5385938 -0 0
|
||||||
|
vertex 0 -0 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 3.342784 -4 0.375
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
vertex 3.342784 -4 0.375
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 3.4311862 -4 -0.625
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0.99999994 0
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 1
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 1
|
||||||
|
vertex 0 -4 0
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
vertex 9.5 -4 2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 2
|
||||||
|
vertex 5.755354 -4 1.5
|
||||||
|
vertex 9.5 -4 1.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 4.146974 -4 0.75
|
||||||
|
vertex 5.644507 -4 2
|
||||||
|
vertex 3.5 -4 1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0.99999994 0
|
||||||
|
outer loop
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
vertex 3.4311862 -4 -0.625
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 -0.99999994 -0
|
||||||
|
outer loop
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
vertex 4.323779 -4 -1.25
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 -0.99999994 -0
|
||||||
|
outer loop
|
||||||
|
vertex 9.5 -4 -2.5
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
vertex 9.5 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 6.108964 -4 -2.5
|
||||||
|
vertex 5.9513144 -4 -3
|
||||||
|
vertex 9.5 -4 -3
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
vertex 0 -4 -1
|
||||||
|
vertex 3.0950184 -4 -1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -4 -1
|
||||||
|
vertex 2.5385938 -4 0
|
||||||
|
vertex 0 -4 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
endsolid unnamed
|
||||||
BIN
e2e/playwright/export-snapshots/stl-binary.stl
Normal file
BIN
e2e/playwright/export-snapshots/stl-binary.stl
Normal file
Binary file not shown.
677
e2e/playwright/flow-tests.spec.ts
Normal file
677
e2e/playwright/flow-tests.spec.ts
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { secrets } from './secrets'
|
||||||
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getUtils } from './test-utils'
|
||||||
|
import waitOn from 'wait-on'
|
||||||
|
import { Themes } from '../../src/lib/theme'
|
||||||
|
|
||||||
|
/*
|
||||||
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
|
just from the nature of the stream, running the test with debugger and pasting the below
|
||||||
|
into the console can be useful to get coords
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) =>
|
||||||
|
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.beforeEach(async ({ context, page }) => {
|
||||||
|
// wait for Vite preview server to be up
|
||||||
|
await waitOn({
|
||||||
|
resources: ['tcp:3000'],
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
await context.addInitScript(async (token) => {
|
||||||
|
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||||
|
localStorage.setItem('persistCode', ``)
|
||||||
|
localStorage.setItem(
|
||||||
|
'SETTINGS_PERSIST_KEY',
|
||||||
|
JSON.stringify({
|
||||||
|
baseUnit: 'in',
|
||||||
|
cameraControls: 'KittyCAD',
|
||||||
|
defaultDirectory: '',
|
||||||
|
defaultProjectName: 'project-$nnn',
|
||||||
|
onboardingStatus: 'dismissed',
|
||||||
|
showDebugPanel: true,
|
||||||
|
textWrapping: 'On',
|
||||||
|
theme: 'system',
|
||||||
|
unitSystem: 'imperial',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, secrets.token)
|
||||||
|
// kill animations, speeds up tests and reduced flakiness
|
||||||
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test.setTimeout(60000)
|
||||||
|
|
||||||
|
test('Basic sketch', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await Promise.all([
|
||||||
|
u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
),
|
||||||
|
u.waitForDefaultPlanesVisibilityChange(),
|
||||||
|
])
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||||
|
await u.waitForCmdReceive('set_tool')
|
||||||
|
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||||
|
'set_tool'
|
||||||
|
)
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
||||||
|
'mouse_click',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
|
const startAt = '[18.26, -24.63]'
|
||||||
|
const num = '18.43'
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)`)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)
|
||||||
|
|> line([0, ${num}], %)`)
|
||||||
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)
|
||||||
|
|> line([0, ${num}], %)
|
||||||
|
|> line([-36.69, 0], %)`)
|
||||||
|
|
||||||
|
// deselect line tool
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||||
|
'set_tool'
|
||||||
|
)
|
||||||
|
|
||||||
|
// click between first two clicks to get center of the line
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
|
||||||
|
'select_with_point'
|
||||||
|
)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// hold down shift
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
// click between the latest two clicks to get center of the line
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
|
||||||
|
|
||||||
|
// selected two lines therefore there should be two cursors
|
||||||
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
||||||
|
|> line([0, ${num}], %)
|
||||||
|
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// check no error to begin with
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
|
/* add the following code to the editor (# error is not a valid line)
|
||||||
|
# error
|
||||||
|
const topAng = 30
|
||||||
|
const bottomAng = 25
|
||||||
|
*/
|
||||||
|
await page.click('.cm-content')
|
||||||
|
await page.keyboard.type('# error')
|
||||||
|
|
||||||
|
// press arrows to clear autocomplete
|
||||||
|
await page.keyboard.press('ArrowLeft')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type('const topAng = 30')
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type('const bottomAng = 25')
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// error in guter
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||||
|
|
||||||
|
// error text on hover
|
||||||
|
await page.hover('.cm-lint-marker-error')
|
||||||
|
await expect(page.getByText("found unknown token '#'")).toBeVisible()
|
||||||
|
|
||||||
|
// select the line that's causing the error and delete it
|
||||||
|
await page.getByText('# error').click()
|
||||||
|
await page.keyboard.press('End')
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.keyboard.press('Home')
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
|
||||||
|
// wait for .cm-lint-marker-error not to be visible
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
|
// let's check we get an error when defining the same variable twice
|
||||||
|
await page.getByText('const bottomAng = 25').click()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type("// Let's define the same thing twice")
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type('const topAng = 42')
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||||
|
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible()
|
||||||
|
|
||||||
|
await page.locator('.cm-lintRange.cm-lintRange-error').hover()
|
||||||
|
await expect(page.locator('.cm-diagnosticText')).toBeVisible()
|
||||||
|
await expect(page.getByText('Cannot redefine topAng')).toBeVisible()
|
||||||
|
|
||||||
|
const secondTopAng = await page.getByText('topAng').first()
|
||||||
|
await secondTopAng?.dblclick()
|
||||||
|
await page.keyboard.type('otherAng')
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('executes on load', async ({ page, context }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await context.addInitScript(async (token) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|
|> line([25.1, 0.41], %)
|
||||||
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// expand variables section
|
||||||
|
await page.getByText('Variables').click()
|
||||||
|
|
||||||
|
// can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
|
||||||
|
// part001 only shows up in the variables summary if it's been executed
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const variablesElement = document.querySelector(
|
||||||
|
'.pretty-json-container'
|
||||||
|
) as HTMLDivElement
|
||||||
|
return variablesElement.innerHTML.includes('part001')
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
page.locator('.pretty-json-container >> text=part001')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('re-executes', async ({ page, context }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await context.addInitScript(async (token) => {
|
||||||
|
localStorage.setItem('persistCode', `const myVar = 5`)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('Variables').click()
|
||||||
|
// expect to see "myVar:5"
|
||||||
|
await expect(
|
||||||
|
page.locator('.pretty-json-container >> text=myVar:5')
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// change 5 to 67
|
||||||
|
await page.getByText('const myVar').click()
|
||||||
|
await page.keyboard.press('End')
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await page.keyboard.type('67')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('.pretty-json-container >> text=myVar:67')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can create sketches on all planes and their back sides', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
const camCmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: { x: 15, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
vantage: { x: 30, y: 30, z: 30 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestSinglePlane = async ({
|
||||||
|
viewCmd,
|
||||||
|
expectedCode,
|
||||||
|
clickCoords,
|
||||||
|
}: {
|
||||||
|
viewCmd: EngineCommand
|
||||||
|
expectedCode: string
|
||||||
|
clickCoords: { x: number; y: number }
|
||||||
|
}) => {
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.sendCustomCmd(viewCmd)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
// await page.waitForTimeout(200)
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
|
||||||
|
|
||||||
|
// draw a line
|
||||||
|
const startXPx = 600
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
|
await u.waitForCmdReceive('set_tool')
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForCmdReceive('mouse_click')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.removeCurrentCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeTemplate = (
|
||||||
|
plane = 'XY',
|
||||||
|
sign = ''
|
||||||
|
) => `const part001 = startSketchOn('${plane}')
|
||||||
|
|> startProfileAt([${sign}6.88, -9.29], %)
|
||||||
|
|> line([${sign}6.95, 0], %)`
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmd,
|
||||||
|
expectedCode: codeTemplate('XY'),
|
||||||
|
clickCoords: { x: 700, y: 350 }, // red plane
|
||||||
|
})
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmd,
|
||||||
|
expectedCode: codeTemplate('YZ'),
|
||||||
|
clickCoords: { x: 1000, y: 200 }, // green plane
|
||||||
|
})
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmd,
|
||||||
|
expectedCode: codeTemplate('XZ', '-'),
|
||||||
|
clickCoords: { x: 630, y: 130 }, // blue plane
|
||||||
|
})
|
||||||
|
|
||||||
|
// new camera angle to click the back side of all three planes
|
||||||
|
const camCmdBackSide: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: { x: -15, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
vantage: { x: -30, y: -30, z: -30 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmdBackSide,
|
||||||
|
expectedCode: codeTemplate('-XY', '-'),
|
||||||
|
clickCoords: { x: 705, y: 136 }, // back of red plane
|
||||||
|
})
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmdBackSide,
|
||||||
|
expectedCode: codeTemplate('-YZ', '-'),
|
||||||
|
clickCoords: { x: 1000, y: 350 }, // back of green plane
|
||||||
|
})
|
||||||
|
await TestSinglePlane({
|
||||||
|
viewCmd: camCmdBackSide,
|
||||||
|
expectedCode: codeTemplate('-XZ'),
|
||||||
|
clickCoords: { x: 600, y: 400 }, // back of blue plane
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Auto complete works', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
// this test might be brittle as we add and remove functions
|
||||||
|
// but should also be easy to update.
|
||||||
|
// tests clicking on an option, selection the first option
|
||||||
|
// and arrowing down to an option
|
||||||
|
|
||||||
|
await page.click('.cm-content')
|
||||||
|
await page.keyboard.type('const part001 = start')
|
||||||
|
|
||||||
|
// expect there to be three auto complete options
|
||||||
|
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
||||||
|
await page.getByText('startSketchOn').click()
|
||||||
|
await page.keyboard.type("('XY')")
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type(' |> startProfi')
|
||||||
|
// expect there be a single auto complete option that we can just hit enter on
|
||||||
|
await expect(page.locator('.cm-completionLabel')).toBeVisible()
|
||||||
|
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
||||||
|
|
||||||
|
await page.keyboard.type('([0,0], %)')
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type(' |> lin')
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
|
||||||
|
// press arrow down twice then enter to accept xLine
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type('(5, %)')
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([0,0], %)
|
||||||
|
|> xLine(5, %)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Onboarding tests
|
||||||
|
test('Onboarding redirects and code updating', async ({ page, context }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
|
||||||
|
// Override beforeEach test setup
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
// Give some initial code, so we can test that it's cleared
|
||||||
|
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
|
||||||
|
|
||||||
|
const storedSettings = JSON.parse(
|
||||||
|
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
|
||||||
|
)
|
||||||
|
storedSettings.onboardingStatus = '/export'
|
||||||
|
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// Test that the redirect happened
|
||||||
|
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||||
|
`/file/new/onboarding/export`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test that you come back to this page when you refresh
|
||||||
|
await page.reload()
|
||||||
|
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
|
||||||
|
`/file/new/onboarding/export`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test that the onboarding pane loaded
|
||||||
|
const title = page.locator('[data-testid="onboarding-content"]')
|
||||||
|
await expect(title).toBeAttached()
|
||||||
|
|
||||||
|
// Test that the code changes when you advance to the next step
|
||||||
|
await page.locator('[data-testid="onboarding-next"]').click()
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText('')
|
||||||
|
|
||||||
|
// Test that the code is not empty when you click on the next step
|
||||||
|
await page.locator('[data-testid="onboarding-next"]').click()
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(/.+/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||||
|
// tests mapping works on fresh sketch and edited sketch
|
||||||
|
// tests using hovers which is the same as selections, because if
|
||||||
|
// source ranges are wrong, hovers won't work
|
||||||
|
const u = getUtils(page)
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
const xAxisClick = () => page.mouse.click(700, 250)
|
||||||
|
const emptySpaceClick = () => page.mouse.click(700, 300)
|
||||||
|
const topHorzSegmentClick = () => page.mouse.click(700, 285)
|
||||||
|
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||||
|
await u.waitForCmdReceive('set_tool')
|
||||||
|
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||||
|
'set_tool'
|
||||||
|
)
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
||||||
|
'mouse_click',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
|
const startAt = '[18.26, -24.63]'
|
||||||
|
const num = '18.43'
|
||||||
|
const num2 = '36.69'
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)`)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)
|
||||||
|
|> line([0, ${num}], %)`)
|
||||||
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${startAt}, %)
|
||||||
|
|> line([${num}, 0], %)
|
||||||
|
|> line([0, ${num}], %)
|
||||||
|
|> line([-${num2}, 0], %)`)
|
||||||
|
|
||||||
|
// deselect line tool
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||||
|
'set_tool'
|
||||||
|
)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
const selectionSequence = async () => {
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||||
|
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||||
|
// and will be an easy fix if it breaks because we change the colour
|
||||||
|
await expect(page.locator('.bg-yellow-200')).toBeVisible()
|
||||||
|
|
||||||
|
// check mousing off, than mousing onto another line
|
||||||
|
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||||
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
||||||
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
|
||||||
|
// now check clicking works including axis
|
||||||
|
|
||||||
|
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||||
|
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||||
|
await expect(absYButton).toBeDisabled()
|
||||||
|
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
|
// clear selection by clicking on nothing
|
||||||
|
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||||
|
|
||||||
|
// same selection but click the axis first
|
||||||
|
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||||
|
await expect(absYButton).toBeDisabled()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
|
// clear selection by clicking on nothing
|
||||||
|
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||||
|
|
||||||
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||||
|
'select_clear',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await expect(absYButton).toBeDisabled()
|
||||||
|
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
|
// clear selection by clicking on nothing
|
||||||
|
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||||
|
|
||||||
|
// select segment in editor than another segment in scene and check there are two cursors
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||||
|
'select_clear',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||||
|
await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
|
|
||||||
|
// clear selection by clicking on nothing
|
||||||
|
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
await selectionSequence()
|
||||||
|
|
||||||
|
// hovering in fresh sketch worked, lets try exiting and re-entering
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
||||||
|
'edit_mode_exit'
|
||||||
|
)
|
||||||
|
// wait for execution done
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
|
// select a line
|
||||||
|
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
|
||||||
|
|
||||||
|
// enter sketch again
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
'edit_mode_enter',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
// hover again and check it works
|
||||||
|
await selectionSequence()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Command bar works and can change a setting', async ({ page }) => {
|
||||||
|
// Brief boilerplate
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
|
||||||
|
// First try opening the command bar and closing it
|
||||||
|
await page.getByRole('button', { name: '⌘K' }).click()
|
||||||
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
await expect(cmdSearchBar).not.toBeVisible()
|
||||||
|
|
||||||
|
// Now try the same, but with the keyboard shortcut, check focus
|
||||||
|
await page.keyboard.press('Meta+K')
|
||||||
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
|
await expect(cmdSearchBar).toBeFocused()
|
||||||
|
|
||||||
|
// Try typing in the command bar
|
||||||
|
await page.keyboard.type('theme')
|
||||||
|
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||||
|
await expect(themeOption).toBeVisible()
|
||||||
|
await themeOption.click()
|
||||||
|
const themeInput = page.getByPlaceholder(Themes.System)
|
||||||
|
await expect(themeInput).toBeVisible()
|
||||||
|
await expect(themeInput).toBeFocused()
|
||||||
|
// Select dark theme
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check the toast appeared
|
||||||
|
await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
|
||||||
|
// Check that the theme changed
|
||||||
|
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
|
||||||
|
})
|
||||||
20
e2e/playwright/secrets.ts
Normal file
20
e2e/playwright/secrets.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const secrets: Record<string, string> = {}
|
||||||
|
try {
|
||||||
|
const file = readFileSync('./e2e/playwright/playwright-secrets.env', 'utf8')
|
||||||
|
file
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line && line.length > 1)
|
||||||
|
.forEach((line) => {
|
||||||
|
const [key, value] = line.split('=')
|
||||||
|
// prefer env vars over secrets file
|
||||||
|
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// probably running in CI
|
||||||
|
secrets.token = process.env.token || ''
|
||||||
|
// add more env vars here to make them available in CI
|
||||||
|
}
|
||||||
|
|
||||||
|
export { secrets }
|
||||||
324
e2e/playwright/snapshot-tests.spec.ts
Normal file
324
e2e/playwright/snapshot-tests.spec.ts
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { secrets } from './secrets'
|
||||||
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getUtils } from './test-utils'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import fsp from 'fs/promises'
|
||||||
|
|
||||||
|
test.beforeEach(async ({ context, page }) => {
|
||||||
|
await context.addInitScript(async (token) => {
|
||||||
|
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||||
|
localStorage.setItem('persistCode', ``)
|
||||||
|
localStorage.setItem(
|
||||||
|
'SETTINGS_PERSIST_KEY',
|
||||||
|
JSON.stringify({
|
||||||
|
baseUnit: 'in',
|
||||||
|
cameraControls: 'KittyCAD',
|
||||||
|
defaultDirectory: '',
|
||||||
|
defaultProjectName: 'project-$nnn',
|
||||||
|
onboardingStatus: 'dismissed',
|
||||||
|
showDebugPanel: true,
|
||||||
|
textWrapping: 'On',
|
||||||
|
theme: 'system',
|
||||||
|
unitSystem: 'imperial',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, secrets.token)
|
||||||
|
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
||||||
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test.setTimeout(60000)
|
||||||
|
|
||||||
|
test('change camera, show planes', async ({ page, context }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
|
const camCmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
vantage: { x: 0, y: 85, z: 85 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await u.sendCustomCmd(camCmd)
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
|
||||||
|
// rotate
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.move(700, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(600, 300)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForCmdReceive('camera_drag_end')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
await u.sendCustomCmd(camCmd)
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
// pan
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.mouse.move(600, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(700, 200)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForCmdReceive('camera_drag_end')
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
|
||||||
|
await u.sendCustomCmd(camCmd)
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// zoom
|
||||||
|
await page.keyboard.down('Control')
|
||||||
|
await page.mouse.move(700, 400)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(700, 350)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
await page.keyboard.up('Control')
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForCmdReceive('camera_drag_end')
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('exports of each format should work', async ({ page, context }) => {
|
||||||
|
// FYI this test doesn't work with only engine running locally
|
||||||
|
const u = getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const topAng = 25
|
||||||
|
const bottomAng = 35
|
||||||
|
const baseLen = 3.5
|
||||||
|
const baseHeight = 1
|
||||||
|
const totalHeightHalf = 2
|
||||||
|
const armThick = 0.5
|
||||||
|
const totalLen = 9.5
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> yLine(baseHeight, %)
|
||||||
|
|> xLine(baseLen, %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: topAng,
|
||||||
|
to: totalHeightHalf,
|
||||||
|
tag: 'seg04'
|
||||||
|
}, %)
|
||||||
|
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
||||||
|
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: _180,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg04'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg04', %) + 180, _0], %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: -bottomAng,
|
||||||
|
to: -totalHeightHalf - armThick,
|
||||||
|
tag: 'seg02'
|
||||||
|
}, %)
|
||||||
|
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||||
|
|> yLine(-segLen('seg01', %), %)
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: _180,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg02'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||||
|
|> xLineTo(_0, %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(4, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.waitForDefaultPlanesVisibilityChange()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.waitForCmdReceive('extrude')
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'KittyCAD Modeling App' }).click()
|
||||||
|
|
||||||
|
const doExport = async (output: Models['OutputFormat_type']) => {
|
||||||
|
await page.getByRole('button', { name: 'Export Model' }).click()
|
||||||
|
|
||||||
|
const exportSelect = page.getByTestId('export-type')
|
||||||
|
await exportSelect.selectOption({ label: output.type })
|
||||||
|
|
||||||
|
if ('storage' in output) {
|
||||||
|
const storageSelect = page.getByTestId('export-storage')
|
||||||
|
await storageSelect.selectOption({ label: output.storage })
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadPromise = page.waitForEvent('download')
|
||||||
|
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
||||||
|
const download = await downloadPromise
|
||||||
|
const downloadLocationer = (extra = '') =>
|
||||||
|
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||||
|
'storage' in output ? output.storage : ''
|
||||||
|
}${extra}.${output.type}`
|
||||||
|
const downloadLocation = downloadLocationer()
|
||||||
|
const downloadLocation2 = downloadLocationer('-2')
|
||||||
|
|
||||||
|
if (output.type === 'gltf' && output.storage === 'standard') {
|
||||||
|
// wait for second download
|
||||||
|
const download2 = await page.waitForEvent('download')
|
||||||
|
await download.saveAs(downloadLocation)
|
||||||
|
await download2.saveAs(downloadLocation2)
|
||||||
|
|
||||||
|
// rewrite uri to reference our file name
|
||||||
|
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||||
|
const isJson = fileContents.includes('buffers')
|
||||||
|
let contents = fileContents
|
||||||
|
let reWriteLocation = downloadLocation
|
||||||
|
let uri = downloadLocation2.split('/').pop()
|
||||||
|
if (!isJson) {
|
||||||
|
contents = await fsp.readFile(downloadLocation2, 'utf-8')
|
||||||
|
reWriteLocation = downloadLocation2
|
||||||
|
uri = downloadLocation.split('/').pop()
|
||||||
|
}
|
||||||
|
contents = contents.replace(/"uri": ".*"/g, `"uri": "${uri}"`)
|
||||||
|
await fsp.writeFile(reWriteLocation, contents)
|
||||||
|
} else {
|
||||||
|
await download.saveAs(downloadLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.type === 'step') {
|
||||||
|
// stable timestamps for step files
|
||||||
|
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||||
|
const newFileContents = fileContents.replace(
|
||||||
|
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||||
|
'1970-01-01T00:00:00.0+00:00'
|
||||||
|
)
|
||||||
|
await fsp.writeFile(downloadLocation, newFileContents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||||
|
axis: 'z',
|
||||||
|
direction: 'positive',
|
||||||
|
}
|
||||||
|
const sysType: Models['System_type'] = {
|
||||||
|
forward: axisDirectionPair,
|
||||||
|
up: axisDirectionPair,
|
||||||
|
}
|
||||||
|
// NOTE it was easiest to leverage existing types and have doExport take Models['OutputFormat_type'] as in input
|
||||||
|
// just note that only `type` and `storage` are used for selecting the drop downs is the app
|
||||||
|
// the rest are only there to make typescript happy
|
||||||
|
await doExport({
|
||||||
|
type: 'step',
|
||||||
|
coords: sysType,
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'embedded',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'binary',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'standard',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
storage: 'ascii',
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
storage: 'binary_little_endian',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
storage: 'binary_big_endian',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'stl',
|
||||||
|
storage: 'ascii',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
type: 'stl',
|
||||||
|
storage: 'binary',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
})
|
||||||
|
await doExport({
|
||||||
|
// obj seems to be a little flaky, times out tests sometimes
|
||||||
|
type: 'obj',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
156
e2e/playwright/test-utils.ts
Normal file
156
e2e/playwright/test-utils.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { expect, Page } from '@playwright/test'
|
||||||
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
|
import fsp from 'fs/promises'
|
||||||
|
import pixelMatch from 'pixelmatch'
|
||||||
|
import { PNG } from 'pngjs'
|
||||||
|
|
||||||
|
async function waitForPageLoad(page: Page) {
|
||||||
|
// wait for 'Loading stream...' spinner
|
||||||
|
await page.getByTestId('loading-stream').waitFor()
|
||||||
|
// wait for all spinners to be gone
|
||||||
|
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
||||||
|
|
||||||
|
await page.getByTestId('start-sketch').waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCurrentCode(page: Page) {
|
||||||
|
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
|
||||||
|
await page.click('.cm-content')
|
||||||
|
await page.keyboard.down(hotkey)
|
||||||
|
await page.keyboard.press('a')
|
||||||
|
await page.keyboard.up(hotkey)
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||||
|
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
|
||||||
|
await page.click('[data-testid="custom-cmd-send-button"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCommandLogs(page: Page) {
|
||||||
|
await page.click('[data-testid="clear-commands"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectCmdLog(page: Page, locatorStr: string) {
|
||||||
|
await expect(page.locator(locatorStr)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
document.querySelectorAll('[data-receive-command-type="object_visible"]')
|
||||||
|
.length >= 3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDebugPanel(page: Page) {
|
||||||
|
const isOpen =
|
||||||
|
(await page
|
||||||
|
.locator('[data-testid="debug-panel"]')
|
||||||
|
?.getAttribute('open')) === ''
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
await page.getByText('Debug').click()
|
||||||
|
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeDebugPanel(page: Page) {
|
||||||
|
const isOpen =
|
||||||
|
(await page.getByTestId('debug-panel')?.getAttribute('open')) === ''
|
||||||
|
if (isOpen) {
|
||||||
|
await page.getByText('Debug').click()
|
||||||
|
await page
|
||||||
|
.getByTestId('debug-panel')
|
||||||
|
.and(page.locator(':not([open])'))
|
||||||
|
.waitFor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForCmdReceive(page: Page, commandType: string) {
|
||||||
|
return page
|
||||||
|
.locator(`[data-receive-command-type="${commandType}"]`)
|
||||||
|
.first()
|
||||||
|
.waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUtils(page: Page) {
|
||||||
|
return {
|
||||||
|
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||||
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
|
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||||
|
clearCommandLogs: () => clearCommandLogs(page),
|
||||||
|
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||||
|
waitForDefaultPlanesVisibilityChange: () =>
|
||||||
|
waitForDefaultPlanesToBeVisible(page),
|
||||||
|
openDebugPanel: () => openDebugPanel(page),
|
||||||
|
closeDebugPanel: () => closeDebugPanel(page),
|
||||||
|
openAndClearDebugPanel: async () => {
|
||||||
|
await openDebugPanel(page)
|
||||||
|
return clearCommandLogs(page)
|
||||||
|
},
|
||||||
|
clearAndCloseDebugPanel: async () => {
|
||||||
|
await clearCommandLogs(page)
|
||||||
|
return closeDebugPanel(page)
|
||||||
|
},
|
||||||
|
waitForCmdReceive: (commandType: string) =>
|
||||||
|
waitForCmdReceive(page, commandType),
|
||||||
|
doAndWaitForCmd: async (
|
||||||
|
fn: () => Promise<void>,
|
||||||
|
commandType: string,
|
||||||
|
endWithDebugPanelOpen = true
|
||||||
|
) => {
|
||||||
|
await openDebugPanel(page)
|
||||||
|
await clearCommandLogs(page)
|
||||||
|
await closeDebugPanel(page)
|
||||||
|
await fn()
|
||||||
|
await openDebugPanel(page)
|
||||||
|
await waitForCmdReceive(page, commandType)
|
||||||
|
if (!endWithDebugPanelOpen) {
|
||||||
|
await closeDebugPanel(page)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
await page.screenshot({
|
||||||
|
path: './e2e/playwright/temp1.png',
|
||||||
|
fullPage: true,
|
||||||
|
})
|
||||||
|
await fn()
|
||||||
|
const isImageDiff = async () => {
|
||||||
|
await page.screenshot({
|
||||||
|
path: './e2e/playwright/temp2.png',
|
||||||
|
fullPage: true,
|
||||||
|
})
|
||||||
|
const screenshot1 = PNG.sync.read(
|
||||||
|
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||||
|
)
|
||||||
|
const screenshot2 = PNG.sync.read(
|
||||||
|
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||||
|
)
|
||||||
|
const actualDiffCount = pixelMatch(
|
||||||
|
screenshot1.data,
|
||||||
|
screenshot2.data,
|
||||||
|
null,
|
||||||
|
screenshot1.width,
|
||||||
|
screenshot2.height
|
||||||
|
)
|
||||||
|
return actualDiffCount > diffCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||||
|
let count = 0
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
count++
|
||||||
|
if (await isImageDiff()) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(true)
|
||||||
|
} else if (count > 100) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
63
e2e/tauri/specs/auth.e2e.ts
Normal file
63
e2e/tauri/specs/auth.e2e.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { browser, $, expect } from '@wdio/globals'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
|
describe('KCMA (Tauri, Linux)', () => {
|
||||||
|
it('opens the auth page, signs in, and signs out', async () => {
|
||||||
|
// Clean up previous tests
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
await fs.rm('/tmp/kittycad_user_code', { force: true })
|
||||||
|
await browser.execute('window.localStorage.clear()')
|
||||||
|
|
||||||
|
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||||
|
expect(await signInButton.getText()).toEqual('Sign in')
|
||||||
|
|
||||||
|
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||||
|
await signInButton.waitForClickable()
|
||||||
|
await browser.execute('arguments[0].click();', signInButton)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
// Get from main.rs
|
||||||
|
const userCode = await (
|
||||||
|
await fs.readFile('/tmp/kittycad_user_code')
|
||||||
|
).toString()
|
||||||
|
console.log(`Found user code ${userCode}`)
|
||||||
|
|
||||||
|
// Device flow: verify
|
||||||
|
const token = process.env.KITTYCAD_API_TOKEN
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
const apiBaseUrl = process.env.VITE_KC_API_BASE_URL
|
||||||
|
const verifyUrl = `${apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
|
||||||
|
console.log(`GET ${verifyUrl}`)
|
||||||
|
const vr = await fetch(verifyUrl, { headers })
|
||||||
|
console.log(vr.status)
|
||||||
|
|
||||||
|
// Device flow: confirm
|
||||||
|
const confirmUrl = `${apiBaseUrl}/oauth2/device/confirm`
|
||||||
|
const data = JSON.stringify({ user_code: userCode })
|
||||||
|
console.log(`POST ${confirmUrl} ${data}`)
|
||||||
|
const cr = await fetch(confirmUrl, {
|
||||||
|
headers,
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
console.log(cr.status)
|
||||||
|
|
||||||
|
// Now should be signed in
|
||||||
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
|
expect(await newFileButton.getText()).toEqual('New file')
|
||||||
|
|
||||||
|
// So let's sign out!
|
||||||
|
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||||
|
await menuButton.waitForClickable()
|
||||||
|
await browser.execute('arguments[0].click();', menuButton)
|
||||||
|
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
||||||
|
await signoutButton.waitForClickable()
|
||||||
|
await browser.execute('arguments[0].click();', signoutButton)
|
||||||
|
const newSignInButton = await $('[data-testid="sign-in-button"]')
|
||||||
|
expect(await newSignInButton.getText()).toEqual('Sign in')
|
||||||
|
})
|
||||||
|
})
|
||||||
93
package.json
93
package.json
@ -1,37 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.8.2",
|
"version": "0.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.10.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.37",
|
"@kittycad/lib": "^0.0.46",
|
||||||
"@lezer/javascript": "^1.4.7",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@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.77.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.5.1",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.2.41",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/react-codemirror": "^4.21.13",
|
"@uiw/react-codemirror": "^4.21.20",
|
||||||
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.2.0",
|
||||||
|
"debounce-promise": "^3.1.2",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^7.0.0",
|
||||||
"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.9",
|
"re-resizable": "^6.9.11",
|
||||||
"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",
|
||||||
@ -41,23 +43,24 @@
|
|||||||
"react-modal-promise": "^1.0.2",
|
"react-modal-promise": "^1.0.2",
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.0.4",
|
"swr": "^2.2.2",
|
||||||
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^5.2.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.1",
|
||||||
"vitest": "^0.34.1",
|
"vitest": "^0.34.6",
|
||||||
"vscode-jsonrpc": "^8.1.0",
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
"vscode-languageserver-protocol": "^3.17.3",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^3.5.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
"xstate": "^4.38.2",
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.1.4"
|
"zustand": "^4.4.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "BROWSER=none vite",
|
"start": "vite",
|
||||||
|
"serve": "vite serve --port=3000",
|
||||||
"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",
|
||||||
@ -65,12 +68,14 @@
|
|||||||
"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)",
|
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
||||||
"test:cov": "vitest run --coverage --mode development",
|
"test:cov": "vitest run --coverage --mode development",
|
||||||
|
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||||
"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 && prettier --write ./e2e",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
|
||||||
|
"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\"",
|
||||||
@ -98,30 +103,42 @@
|
|||||||
},
|
},
|
||||||
"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.23.3",
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@playwright/test": "^1.39.0",
|
||||||
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/debounce": "^1.2.1",
|
"@types/debounce-promise": "^3.1.8",
|
||||||
"@types/isomorphic-fetch": "^0.0.36",
|
"@types/isomorphic-fetch": "^0.0.36",
|
||||||
"@types/react-modal": "^3.16.0",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/pngjs": "^6.0.4",
|
||||||
|
"@types/react-modal": "^3.16.3",
|
||||||
|
"@types/uuid": "^9.0.4",
|
||||||
|
"@types/wait-on": "^5.3.4",
|
||||||
"@types/wicg-file-system-access": "^2020.9.6",
|
"@types/wicg-file-system-access": "^2020.9.6",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.1.1",
|
||||||
"@vitest/coverage-istanbul": "^0.34.1",
|
"@vitest/coverage-istanbul": "^0.34.6",
|
||||||
|
"@wdio/cli": "^8.24.3",
|
||||||
|
"@wdio/globals": "^8.24.3",
|
||||||
|
"@wdio/local-runner": "^8.24.3",
|
||||||
|
"@wdio/mocha-framework": "^8.24.3",
|
||||||
|
"@wdio/spec-reporter": "^8.24.2",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.11.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"happy-dom": "^10.8.0",
|
"happy-dom": "^10.8.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"postcss": "^8.4.19",
|
"pixelmatch": "^5.3.0",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.3.6",
|
||||||
"vite": "^4.4.3",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.1",
|
||||||
|
"wait-on": "^7.2.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
playwright.config.ts
Normal file
82
playwright.config.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e/playwright',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 3 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : 1,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'Google Chrome',
|
||||||
|
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'firefox',
|
||||||
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'chromium', // compat issue with encoding atm, so we're using the branded 'Google Chrome' instead
|
||||||
|
// use: { ...devices['Desktop Chrome'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'yarn serve',
|
||||||
|
// url: 'http://127.0.0.1:3000',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
3
public/kcl-icon.svg
Normal file
3
public/kcl-icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
296
src-tauri/Cargo.lock
generated
296
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.0",
|
"toml 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -122,6 +122,12 @@ dependencies = [
|
|||||||
"system-deps 6.1.0",
|
"system-deps 6.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -155,6 +161,20 @@ 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"
|
||||||
@ -1295,7 +1315,21 @@ checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"mac",
|
"mac",
|
||||||
"markup5ever",
|
"markup5ever 0.10.1",
|
||||||
|
"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",
|
||||||
@ -1539,7 +1573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.3.1",
|
"hermit-abi 0.3.1",
|
||||||
"rustix 0.38.13",
|
"rustix 0.38.21",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1630,13 +1664,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.25"
|
version = "0.2.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
|
checksum = "6aa554d86b6dbbd976a659c912ae25ce817b4378eb12a5684907e263410f0a7b"
|
||||||
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",
|
||||||
@ -1671,7 +1706,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
|
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cssparser",
|
"cssparser",
|
||||||
"html5ever",
|
"html5ever 0.25.2",
|
||||||
|
"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",
|
||||||
]
|
]
|
||||||
@ -1684,9 +1732,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.148"
|
version = "0.2.150"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||||
|
|
||||||
|
[[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"
|
||||||
@ -1711,9 +1765,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.7"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
|
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@ -1727,9 +1781,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.18"
|
version = "0.4.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -1781,7 +1835,21 @@ checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"phf 0.8.0",
|
"phf 0.8.0",
|
||||||
"phf_codegen",
|
"phf_codegen 0.8.0",
|
||||||
|
"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",
|
||||||
@ -1845,12 +1913,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minisign-verify"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -1872,9 +1934,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
@ -1959,6 +2021,17 @@ 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"
|
||||||
@ -1982,9 +2055,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.15"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
@ -2342,6 +2415,16 @@ 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"
|
||||||
@ -2410,9 +2493,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phonenumber"
|
name = "phonenumber"
|
||||||
version = "0.3.2+8.13.9"
|
version = "0.3.3+8.13.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f"
|
checksum = "635f3e6288e4f01c049d89332a031bd74f25d64b6fb94703ca966e819488cd06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"either",
|
"either",
|
||||||
@ -2425,6 +2508,7 @@ dependencies = [
|
|||||||
"regex-cache",
|
"regex-cache",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
"strum",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2749,9 +2833,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.20"
|
version = "0.11.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
|
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -2778,6 +2862,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
@ -2927,9 +3012,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.37.19"
|
version = "0.37.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
|
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"errno",
|
"errno",
|
||||||
@ -2941,14 +3026,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.13"
|
version = "0.38.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
|
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.7",
|
"linux-raw-sys 0.4.10",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3021,10 +3106,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.13"
|
version = "0.8.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
|
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bigdecimal",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
@ -3037,9 +3123,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "0.8.13"
|
version = "0.8.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
|
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3105,7 +3191,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"matches",
|
"matches",
|
||||||
"phf 0.8.0",
|
"phf 0.8.0",
|
||||||
"phf_codegen",
|
"phf_codegen 0.8.0",
|
||||||
"precomputed-hash",
|
"precomputed-hash",
|
||||||
"servo_arc",
|
"servo_arc",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -3123,9 +3209,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.188"
|
version = "1.0.193"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -3141,9 +3227,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.188"
|
version = "1.0.193"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3163,9 +3249,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.107"
|
version = "1.0.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.6",
|
"itoa 1.0.6",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -3346,9 +3432,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.4"
|
version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
@ -3458,6 +3544,28 @@ 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"
|
||||||
@ -3493,6 +3601,27 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
@ -3605,12 +3734,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "1.4.1"
|
version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
|
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.2",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
@ -3624,7 +3752,6 @@ dependencies = [
|
|||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"http",
|
"http",
|
||||||
"ignore",
|
"ignore",
|
||||||
"minisign-verify",
|
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
@ -3649,24 +3776,23 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows 0.39.0",
|
||||||
"zip",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
|
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
|
"dirs-next",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"semver",
|
"semver",
|
||||||
@ -3674,13 +3800,14 @@ 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.0"
|
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 = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
|
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"brotli",
|
"brotli",
|
||||||
@ -3704,9 +3831,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "1.4.0"
|
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 = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
|
checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3719,7 +3846,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#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#537053d3171a7374a1a86fed422523e7b45a4fb8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3730,9 +3857,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "0.14.0"
|
version = "0.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
|
checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@ -3751,9 +3878,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "0.14.0"
|
version = "0.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
|
checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"gtk",
|
"gtk",
|
||||||
@ -3771,19 +3898,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
|
checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"html5ever",
|
"html5ever 0.26.0",
|
||||||
"infer",
|
"infer",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"kuchiki",
|
"kuchikiki",
|
||||||
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"phf 0.10.1",
|
"phf 0.10.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3817,7 +3945,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"redox_syscall 0.3.5",
|
"redox_syscall 0.3.5",
|
||||||
"rustix 0.37.19",
|
"rustix 0.37.27",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3897,9 +4025,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.32.0"
|
version = "1.34.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -3907,7 +4035,7 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.5.4",
|
"socket2 0.5.5",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3968,14 +4096,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.0"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e"
|
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_edit 0.20.0",
|
"toml_edit 0.20.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4002,9 +4130,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.20.0"
|
version = "0.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95"
|
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
"serde",
|
"serde",
|
||||||
@ -4182,6 +4310,7 @@ version = "1.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic",
|
||||||
"getrandom 0.2.9",
|
"getrandom 0.2.9",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -4799,9 +4928,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.24.3"
|
version = "0.24.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33748f35413c8a98d45f7a08832d848c0c5915501803d1faade5a4ebcd258cea"
|
checksum = "88ef04bdad49eba2e01f06e53688c8413bd6a87b0bc14b72284465cf96e3578e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"block",
|
"block",
|
||||||
@ -4813,7 +4942,7 @@ dependencies = [
|
|||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"gtk",
|
"gtk",
|
||||||
"html5ever",
|
"html5ever 0.25.2",
|
||||||
"http",
|
"http",
|
||||||
"kuchiki",
|
"kuchiki",
|
||||||
"libc",
|
"libc",
|
||||||
@ -4873,14 +5002,3 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zip"
|
|
||||||
version = "0.6.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"crc32fast",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
default-run = "app"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.60"
|
rust-version = "1.60"
|
||||||
@ -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.4.0", features = [] }
|
tauri-build = { version = "1.5.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kittycad = "0.2.25"
|
kittycad = "0.2.42"
|
||||||
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.4.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "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.32.0", features = ["time"] }
|
tokio = { version = "1.34.0", features = ["time"] }
|
||||||
toml = "0.8.0"
|
toml = "0.8.2"
|
||||||
|
|
||||||
[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.
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -68,10 +70,26 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Open the system browser with the auth_uri.
|
// Open the system browser with the auth_uri.
|
||||||
// We do this in the browser and not a seperate window because we want 1password and
|
// We do this in the browser and not a separate window because we want 1password and
|
||||||
// other crap to work well.
|
// other crap to work well.
|
||||||
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
// TODO: find a better way to share this value with tauri e2e tests
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
// Here we're using an env var to enable the /tmp file (windows not supported for now)
|
||||||
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||||
|
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||||
|
if (e2e_tauri_enabled) {
|
||||||
|
println!(
|
||||||
|
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
||||||
|
auth_uri.secret()
|
||||||
|
);
|
||||||
|
fs::write(
|
||||||
|
"/tmp/kittycad_user_code",
|
||||||
|
details.user_code().secret().to_string(),
|
||||||
|
)
|
||||||
|
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||||
|
} else {
|
||||||
|
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for the user to login.
|
// Wait for the user to login.
|
||||||
let token = auth_client
|
let token = auth_client
|
||||||
@ -129,10 +147,10 @@ async fn get_user(
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.setup(|_app| {
|
||||||
#[cfg(debug_assertions)] // only include this code on debug builds
|
#[cfg(debug_assertions)] // only include this code on debug builds
|
||||||
{
|
{
|
||||||
let window = app.get_window("main").unwrap();
|
let window = _app.get_window("main").unwrap();
|
||||||
// comment out the below if you don't devtools to open everytime.
|
// comment out the below if you don't devtools to open everytime.
|
||||||
// it's useful because otherwise devtools shuts everytime rust code changes.
|
// it's useful because otherwise devtools shuts everytime rust code changes.
|
||||||
window.open_devtools();
|
window.open_devtools();
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "yarn build:both",
|
|
||||||
"beforeDevCommand": "yarn start",
|
"beforeDevCommand": "yarn start",
|
||||||
"devPath": "http://localhost:3000",
|
"devPath": "http://localhost:3000",
|
||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "kittycad-modeling",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.8.2"
|
"version": "0.13.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -72,23 +71,13 @@
|
|||||||
},
|
},
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all",
|
"targets": "all"
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": "http://timestamp.digicert.com"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": false
|
||||||
"endpoints": [
|
|
||||||
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
|
||||||
],
|
|
||||||
"dialog": true,
|
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
|
|||||||
21
src-tauri/tauri.release.conf.json
Normal file
21
src-tauri/tauri.release.conf.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"tauri": {
|
||||||
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"identifier": "io.kittycad.modeling-app",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "http://timestamp.digicert.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
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 { BrowserRouter } from 'react-router-dom'
|
import {
|
||||||
|
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 {
|
||||||
@ -24,7 +31,7 @@ describe('App tests', () => {
|
|||||||
>
|
>
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useParams: () => ({ id: 'new' }),
|
useParams: () => ({ id: BROWSER_FILE_NAME }),
|
||||||
useLoaderData: () => ({ code: null }),
|
useLoaderData: () => ({ code: null }),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -41,12 +48,26 @@ describe('App tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
// 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>
|
||||||
|
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialEntries: ['/file/new'],
|
||||||
|
initialIndex: 0,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
return <RouterProvider router={router} />
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/App.tsx
106
src/App.tsx
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
import { 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'
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { isTauri } from './lib/isTauri'
|
|
||||||
import { useLoaderData } from 'react-router-dom'
|
import { useLoaderData } from 'react-router-dom'
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
@ -29,46 +28,32 @@ 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 { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { project, file } = 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 {
|
const { settings } = useGlobalStateContext()
|
||||||
auth: {
|
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
||||||
context: { token },
|
settings?.context || {}
|
||||||
},
|
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
|
||||||
|
|
||||||
@ -85,50 +70,7 @@ 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', () => {
|
useHotkeys('esc', () => send('Cancel'))
|
||||||
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
|
||||||
@ -138,25 +80,10 @@ export function App() {
|
|||||||
? 'opacity-40'
|
? 'opacity-40'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// Use file code loaded from disk
|
|
||||||
// on mount, and overwrite any locally-stored code
|
|
||||||
useEffect(() => {
|
|
||||||
if (isTauri() && loadedCode !== null) {
|
|
||||||
setCode(loadedCode)
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
// Clear code on unmount if in desktop app
|
|
||||||
if (isTauri()) {
|
|
||||||
setCode('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [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()
|
||||||
@ -170,10 +97,7 @@ export function App() {
|
|||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (buttonDownInStream === undefined) {
|
if (buttonDownInStream === undefined) {
|
||||||
if (
|
if (state.matches('Sketch.Line Tool')) {
|
||||||
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,
|
||||||
@ -193,7 +117,7 @@ export function App() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
if (state.matches('Sketch.Move Tool')) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
@ -216,7 +140,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,9 +157,8 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
className="relative h-full flex flex-col"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
ref={streamRef}
|
|
||||||
>
|
>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className={
|
className={
|
||||||
@ -244,7 +166,7 @@ export function App() {
|
|||||||
paneOpacity +
|
paneOpacity +
|
||||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={project}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
|
|||||||
10
src/Auth.tsx
10
src/Auth.tsx
@ -3,13 +3,13 @@ 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 {
|
const { auth } = useGlobalStateContext()
|
||||||
auth: { state },
|
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
||||||
} = useGlobalStateContext()
|
|
||||||
const isLoggingIn = state.matches('checkIfLoggedIn')
|
|
||||||
|
|
||||||
return isLoggingIn ? (
|
return isLoggingIn ? (
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>
|
||||||
|
<span data-testid="initial-load">Loading KittyCAD Modeling App...</span>
|
||||||
|
</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -31,6 +31,7 @@ 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,
|
||||||
@ -40,6 +41,10 @@ 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, kclManager } 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({
|
||||||
@ -94,13 +99,16 @@ 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 & {
|
||||||
entrypoint_metadata: Metadata
|
entrypointMetadata: Metadata
|
||||||
}
|
}
|
||||||
export type HomeLoaderData = {
|
export type HomeLoaderData = {
|
||||||
projects: ProjectWithEntryPointMetadata[]
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
@ -129,15 +137,24 @@ const router = createBrowserRouter(
|
|||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: () =>
|
||||||
isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'),
|
isTauri()
|
||||||
|
? 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>
|
||||||
<Outlet />
|
<FileMachineProvider>
|
||||||
<App />
|
<KclContextProvider>
|
||||||
|
<ModelingMachineProvider>
|
||||||
|
<Outlet />
|
||||||
|
<App />
|
||||||
|
</ModelingMachineProvider>
|
||||||
|
<WasmErrBanner />
|
||||||
|
</KclContextProvider>
|
||||||
|
</FileMachineProvider>
|
||||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
@ -167,21 +184,42 @@ const router = createBrowserRouter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.id && params.id !== 'new') {
|
const defaultDir = persistedSettings.defaultDirectory || ''
|
||||||
|
|
||||||
|
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(params.id + '/' + PROJECT_ENTRYPOINT)
|
const code = await readTextFile(decodedId)
|
||||||
const entrypoint_metadata = await metadata(
|
const entrypointMetadata = await metadata(
|
||||||
params.id + '/' + PROJECT_ENTRYPOINT
|
projectPath + sep + PROJECT_ENTRYPOINT
|
||||||
)
|
)
|
||||||
const children = await readDir(params.id)
|
const children = await readDir(projectPath, { recursive: true })
|
||||||
|
kclManager.setCodeAndExecute(code, false)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
project: {
|
project: {
|
||||||
name: params.id.slice(params.id.lastIndexOf('/') + 1),
|
name: projectName,
|
||||||
path: params.id,
|
path: projectPath,
|
||||||
children,
|
children,
|
||||||
entrypoint_metadata,
|
entrypointMetadata,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
name: currentFileName,
|
||||||
|
path: params.id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +250,7 @@ const router = createBrowserRouter(
|
|||||||
),
|
),
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/new')
|
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
||||||
}
|
}
|
||||||
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<
|
||||||
@ -234,9 +272,9 @@ const router = createBrowserRouter(
|
|||||||
isProjectDirectory
|
isProjectDirectory
|
||||||
)
|
)
|
||||||
const projects = await Promise.all(
|
const projects = await Promise.all(
|
||||||
projectsNoMeta.map(async (p) => ({
|
projectsNoMeta.map(async (p: FileEntry) => ({
|
||||||
entrypoint_metadata: await metadata(
|
entrypointMetadata: await metadata(
|
||||||
p.path + '/' + PROJECT_ENTRYPOINT
|
p.path + sep + PROJECT_ENTRYPOINT
|
||||||
),
|
),
|
||||||
...p,
|
...p,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
.toolbarWrapper {
|
|
||||||
@apply relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
@apply flex gap-4 items-center rounded-full;
|
|
||||||
@apply border border-cool-20/30 bg-cool-10/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .toolbar {
|
|
||||||
@apply border-cool-100/50 bg-cool-120/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.sketch) .toolbar {
|
|
||||||
@apply border-fern-20/20 bg-fern-10/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark .sketch) .toolbar {
|
|
||||||
@apply border-fern-120/50 bg-fern-100/30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarCap {
|
|
||||||
@apply text-sm font-bold;
|
|
||||||
@apply bg-cool-20/50 text-cool-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .toolbarCap {
|
|
||||||
@apply bg-cool-90/50 text-cool-30;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.sketch) .toolbarCap {
|
|
||||||
@apply bg-fern-20/50 text-fern-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark .sketch) .toolbarCap {
|
|
||||||
@apply bg-fern-90/50 text-fern-30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
@apply self-stretch flex items-center px-4 py-1;
|
|
||||||
@apply rounded-l-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popoverToggle {
|
|
||||||
@apply self-stretch m-0 flex items-center px-4 py-1;
|
|
||||||
@apply rounded-r-full border-none;
|
|
||||||
@apply hover:bg-cool-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarButtons::-webkit-scrollbar {
|
|
||||||
@apply h-0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarButtons {
|
|
||||||
@apply flex items-center overflow-x-auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarButtons button {
|
|
||||||
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
@apply gap-1.5 p-0.5 pr-1;
|
|
||||||
@apply rounded-sm;
|
|
||||||
}
|
|
||||||
:global(.dark) .toolbarButtons button {
|
|
||||||
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
|
|
||||||
}
|
|
||||||
.toolbarButtons button:hover {
|
|
||||||
@apply text-cool-90 bg-cool-10;
|
|
||||||
}
|
|
||||||
:global(.sketch) .toolbarButtons button:hover {
|
|
||||||
@apply text-fern-90 bg-fern-10;
|
|
||||||
}
|
|
||||||
.toolbarButtons button:disabled {
|
|
||||||
@apply text-chalkboard-70 bg-chalkboard-30;
|
|
||||||
}
|
|
||||||
.toolbarButtons button:disabled:hover {
|
|
||||||
@apply !bg-inherit !text-inherit cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .toolbarButtons button {
|
|
||||||
@apply text-chalkboard-20 border-chalkboard-50;
|
|
||||||
}
|
|
||||||
:global(.dark) .toolbarButtons button:hover {
|
|
||||||
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
|
|
||||||
}
|
|
||||||
:global(.dark .sketch) .toolbarButtons button:hover {
|
|
||||||
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
|
|
||||||
}
|
|
||||||
:global(.dark) .toolbarButtons button:disabled {
|
|
||||||
@apply text-chalkboard-40 bg-chalkboard-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .popoverToggle {
|
|
||||||
@apply hover:bg-cool-90;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.sketch) .popoverToggle {
|
|
||||||
@apply hover:bg-fern-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark .sketch) .popoverToggle {
|
|
||||||
@apply hover:bg-fern-90;
|
|
||||||
}
|
|
||||||
517
src/Toolbar.tsx
517
src/Toolbar.tsx
@ -1,335 +1,214 @@
|
|||||||
import { useStore, toolTips, ToolTip } from './useStore'
|
import { WheelEvent, useRef, useMemo } from 'react'
|
||||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
import { engineCommandManager } from './lang/std/engineConnection'
|
||||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { EqualLength } from './components/Toolbar/EqualLength'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
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 { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
|
||||||
import styles from './Toolbar.module.css'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { useAppMode } from 'hooks/useAppMode'
|
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
|
||||||
|
|
||||||
export const sketchButtonClassnames = {
|
|
||||||
background:
|
|
||||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
|
||||||
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sketchFnLabels: Record<ToolTip | 'sketch_line' | 'move', string> = {
|
|
||||||
sketch_line: 'Line',
|
|
||||||
line: 'Line',
|
|
||||||
move: 'Move',
|
|
||||||
angledLine: 'Angled Line',
|
|
||||||
angledLineThatIntersects: 'Angled Line That Intersects',
|
|
||||||
angledLineOfXLength: 'Angled Line Of X Length',
|
|
||||||
angledLineOfYLength: 'Angled Line Of Y Length',
|
|
||||||
angledLineToX: 'Angled Line To X',
|
|
||||||
angledLineToY: 'Angled Line To Y',
|
|
||||||
lineTo: 'Line to Point',
|
|
||||||
xLine: 'Horizontal Line',
|
|
||||||
yLine: 'Vertical Line',
|
|
||||||
xLineTo: 'Horizontal Line to Point',
|
|
||||||
yLineTo: 'Vertical Line to Point',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
setGuiMode,
|
const { state, send, context } = useModelingContext()
|
||||||
guiMode,
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
selectionRanges,
|
const bgClassName =
|
||||||
ast,
|
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
|
||||||
updateAst,
|
const pathId = useMemo(
|
||||||
programMemory,
|
() =>
|
||||||
engineCommandManager,
|
isCursorInSketchCommandRange(
|
||||||
executeAst,
|
engineCommandManager.artifactMap,
|
||||||
} = useStore((s) => ({
|
context.selectionRanges
|
||||||
guiMode: s.guiMode,
|
),
|
||||||
setGuiMode: s.setGuiMode,
|
[engineCommandManager.artifactMap, context.selectionRanges]
|
||||||
selectionRanges: s.selectionRanges,
|
)
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
engineCommandManager: s.engineCommandManager,
|
|
||||||
executeAst: s.executeAst,
|
|
||||||
}))
|
|
||||||
useAppMode()
|
|
||||||
|
|
||||||
useEffect(() => {
|
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||||
console.log('guiMode', guiMode)
|
const span = toolbarButtonsRef.current
|
||||||
}, [guiMode])
|
if (!span) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButtons({
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLElement>) {
|
||||||
return (
|
return (
|
||||||
<span className={styles.toolbarButtons + ' ' + className}>
|
<ul
|
||||||
{guiMode.mode === 'default' && (
|
{...props}
|
||||||
<button
|
ref={toolbarButtonsRef}
|
||||||
onClick={() => {
|
onWheel={handleToolbarButtonsWheelEvent}
|
||||||
setGuiMode({
|
className={
|
||||||
mode: 'sketch',
|
'm-0 py-1 rounded-l-sm flex gap-2 items-center overflow-x-auto ' +
|
||||||
sketchMode: 'selectFace',
|
className
|
||||||
})
|
}
|
||||||
}}
|
style={{ scrollbarWidth: 'thin' }}
|
||||||
className="group"
|
>
|
||||||
>
|
{state.nextEvents.includes('Enter sketch') && (
|
||||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
<li className="contents">
|
||||||
Start Sketch
|
<ActionButton
|
||||||
</button>
|
Element="button"
|
||||||
)}
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
{guiMode.mode === 'canEditExtrude' && (
|
icon={{
|
||||||
<button
|
icon: 'sketch',
|
||||||
onClick={() => {
|
bgClassName,
|
||||||
if (!ast) return
|
}}
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
>
|
||||||
ast,
|
<span data-testid="start-sketch">Start Sketch</span>
|
||||||
selectionRanges.codeBasedSelections[0].range
|
</ActionButton>
|
||||||
)
|
</li>
|
||||||
const { modifiedAst } = sketchOnExtrudedFace(
|
)}
|
||||||
ast,
|
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||||
pathToNode,
|
<li className="contents">
|
||||||
programMemory
|
<ActionButton
|
||||||
)
|
Element="button"
|
||||||
updateAst(modifiedAst, true)
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
}}
|
icon={{
|
||||||
className="group"
|
icon: 'sketch',
|
||||||
>
|
bgClassName,
|
||||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
}}
|
||||||
Sketch on Face
|
>
|
||||||
</button>
|
Edit Sketch
|
||||||
)}
|
</ActionButton>
|
||||||
{guiMode.mode === 'canEditSketch' && (
|
</li>
|
||||||
<button
|
)}
|
||||||
onClick={() => {
|
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
<li className="contents">
|
||||||
ast,
|
<ActionButton
|
||||||
selectionRanges.codeBasedSelections[0].range
|
Element="button"
|
||||||
)
|
onClick={() => send({ type: 'Cancel' })}
|
||||||
setGuiMode({
|
icon={{
|
||||||
mode: 'sketch',
|
icon: 'arrowLeft',
|
||||||
sketchMode: 'enterSketchEdit',
|
bgClassName,
|
||||||
pathToNode: pathToNode,
|
}}
|
||||||
rotation: [0, 0, 0, 1],
|
>
|
||||||
position: [0, 0, 0],
|
Exit Sketch
|
||||||
pathId: guiMode.pathId,
|
</ActionButton>
|
||||||
})
|
</li>
|
||||||
}}
|
)}
|
||||||
className="group"
|
{state.matches('Sketch') && !state.matches('idle') && (
|
||||||
>
|
<li className="contents">
|
||||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
<ActionButton
|
||||||
Edit Sketch
|
Element="button"
|
||||||
</button>
|
onClick={() =>
|
||||||
)}
|
state.matches('Sketch.Line Tool')
|
||||||
{guiMode.mode === 'canEditSketch' && (
|
? send('CancelSketch')
|
||||||
<>
|
: send('Equip tool')
|
||||||
<button
|
}
|
||||||
onClick={() => {
|
aria-pressed={state.matches('Sketch.Line Tool')}
|
||||||
if (!ast) return
|
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
icon={{
|
||||||
ast,
|
icon: 'line',
|
||||||
selectionRanges.codeBasedSelections[0].range
|
bgClassName,
|
||||||
)
|
}}
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
>
|
||||||
ast,
|
Line
|
||||||
pathToNode
|
</ActionButton>
|
||||||
)
|
</li>
|
||||||
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
)}
|
||||||
|
{state.matches('Sketch') && (
|
||||||
|
<li className="contents">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() =>
|
||||||
|
state.matches('Sketch.Move Tool')
|
||||||
|
? send('CancelSketch')
|
||||||
|
: send('Equip move tool')
|
||||||
|
}
|
||||||
|
aria-pressed={state.matches('Sketch.Move Tool')}
|
||||||
|
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||||
|
icon={{
|
||||||
|
icon: 'move',
|
||||||
|
bgClassName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Move
|
||||||
|
</ActionButton>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{state.matches('Sketch.SketchIdle') &&
|
||||||
|
state.nextEvents
|
||||||
|
.filter(
|
||||||
|
(eventName) =>
|
||||||
|
eventName.includes('Make segment') ||
|
||||||
|
eventName.includes('Constrain')
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aisEnabled = state.nextEvents
|
||||||
|
.filter((event) => state.can(event as any))
|
||||||
|
.includes(a)
|
||||||
|
const bIsEnabled = state.nextEvents
|
||||||
|
.filter((event) => state.can(event as any))
|
||||||
|
.includes(b)
|
||||||
|
if (aisEnabled && !bIsEnabled) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (!aisEnabled && bIsEnabled) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map((eventName) => (
|
||||||
|
<li className="contents">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
className="text-sm"
|
||||||
|
key={eventName}
|
||||||
|
onClick={() => send(eventName)}
|
||||||
|
disabled={
|
||||||
|
!state.nextEvents
|
||||||
|
.filter((event) => state.can(event as any))
|
||||||
|
.includes(eventName)
|
||||||
|
}
|
||||||
|
title={eventName}
|
||||||
|
icon={{
|
||||||
|
icon: 'line',
|
||||||
|
bgClassName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{eventName
|
||||||
|
.replace('Make segment ', '')
|
||||||
|
.replace('Constrain ', '')}
|
||||||
|
</ActionButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{state.matches('idle') && (
|
||||||
|
<li className="contents">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
className="text-sm"
|
||||||
|
onClick={() => send('extrude intent')}
|
||||||
|
disabled={!state.can('extrude intent')}
|
||||||
|
title={
|
||||||
|
state.can('extrude intent')
|
||||||
|
? 'extrude'
|
||||||
|
: 'sketches need to be closed, or not already extruded'
|
||||||
|
}
|
||||||
|
icon={{
|
||||||
|
icon: 'extrude',
|
||||||
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
className="group"
|
|
||||||
>
|
>
|
||||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
|
||||||
Extrude
|
Extrude
|
||||||
</button>
|
</ActionButton>
|
||||||
<button
|
</li>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</ul>
|
||||||
{guiMode.mode === 'sketch' && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
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' })
|
|
||||||
executeAst()
|
|
||||||
}}
|
|
||||||
className="group"
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
icon="exit"
|
|
||||||
className="!p-0.5"
|
|
||||||
bgClassName={sketchButtonClassnames.background}
|
|
||||||
iconClassName={sketchButtonClassnames.icon}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
Exit sketch
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{toolTips
|
|
||||||
.filter(
|
|
||||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
|
||||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
|
||||||
)
|
|
||||||
.map((sketchFnName) => {
|
|
||||||
if (
|
|
||||||
guiMode.mode !== 'sketch' ||
|
|
||||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={sketchFnName}
|
|
||||||
onClick={() => {
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
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'
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
icon={sketchFnName.includes('line') ? 'line' : 'move'}
|
|
||||||
className="!p-0.5"
|
|
||||||
bgClassName={sketchButtonClassnames.background}
|
|
||||||
iconClassName={sketchButtonClassnames.icon}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
{sketchFnLabels[sketchFnName]}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<HorzVert horOrVert="horizontal" />
|
|
||||||
<HorzVert horOrVert="vertical" />
|
|
||||||
<EqualLength />
|
|
||||||
<EqualAngle />
|
|
||||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
|
||||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
|
||||||
<SetAbsDistance buttonType="snapToYAxis" />
|
|
||||||
<SetAbsDistance buttonType="xAbs" />
|
|
||||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
|
||||||
<SetAbsDistance buttonType="snapToXAxis" />
|
|
||||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
|
||||||
<SetAbsDistance buttonType="yAbs" />
|
|
||||||
<SetAngleLength angleOrLength="setAngle" />
|
|
||||||
<SetAngleLength angleOrLength="setLength" />
|
|
||||||
<Intersect />
|
|
||||||
<RemoveConstrainingValues />
|
|
||||||
<SetAngleBetween />
|
|
||||||
</span>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
|
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative">
|
||||||
<div className={styles.toolbar}>
|
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0">
|
||||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
<ToolbarButtons />
|
||||||
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
</menu>
|
||||||
</span>
|
<ActionButton
|
||||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
Element="button"
|
||||||
<ToolbarButtons />
|
onClick={() => setCommandBarOpen(true)}
|
||||||
</menu>
|
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
|
||||||
<Popover.Button
|
|
||||||
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faSearch} />
|
|
||||||
</Popover.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="transition ease-out duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
>
|
||||||
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
⌘K
|
||||||
</Transition>
|
</ActionButton>
|
||||||
<Transition
|
</div>
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="opacity-0 translate-y-1 scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 scale-100"
|
|
||||||
leave="transition ease-out duration-75"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-2"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
|
||||||
<section className="flex justify-between items-center">
|
|
||||||
<p
|
|
||||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
|
||||||
>
|
|
||||||
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
|
||||||
</p>
|
|
||||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
|
||||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
|
||||||
</Popover.Button>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<ToolbarButtons className="flex-wrap" />
|
|
||||||
</section>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</Popover>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,7 @@ type ActionButtonAsLink = BaseActionButtonProps &
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActionButtonAsExternal = BaseActionButtonProps &
|
type ActionButtonAsExternal = BaseActionButtonProps &
|
||||||
Omit<
|
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
||||||
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
||||||
keyof BaseActionButtonProps
|
|
||||||
> & {
|
|
||||||
Element: 'externalLink'
|
Element: 'externalLink'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,16 +39,16 @@ type ActionButtonProps =
|
|||||||
| ActionButtonAsElement
|
| ActionButtonAsElement
|
||||||
|
|
||||||
export const ActionButton = (props: ActionButtonProps) => {
|
export const ActionButton = (props: ActionButtonProps) => {
|
||||||
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
|
const classNames = `action-button m-0 group mono text-sm flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 p-[3px] text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||||
props.icon ? 'pr-2' : 'px-2'
|
props.icon ? 'pr-2' : 'px-2'
|
||||||
} ${props.className || ''}`
|
} ${props.className ? props.className : ''}`
|
||||||
|
|
||||||
switch (props.Element) {
|
switch (props.Element) {
|
||||||
case 'button': {
|
case 'button': {
|
||||||
// Note we have to destructure 'className' and 'Element' out of props
|
// Note we have to destructure 'className' and 'Element' out of props
|
||||||
// because we don't want to pass them to the button element;
|
// because we don't want to pass them to the button element;
|
||||||
// the same is true for the other cases below.
|
// the same is true for the other cases below.
|
||||||
const { Element, icon, children, className, ...rest } = props
|
const { Element, icon, children, className: _className, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<button className={classNames} {...rest}>
|
<button className={classNames} {...rest}>
|
||||||
{props.icon && <ActionIcon {...icon} />}
|
{props.icon && <ActionIcon {...icon} />}
|
||||||
@ -60,7 +57,14 @@ export const ActionButton = (props: ActionButtonProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'link': {
|
case 'link': {
|
||||||
const { Element, to, icon, children, className, ...rest } = props
|
const {
|
||||||
|
Element,
|
||||||
|
to,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
className: _className,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
return (
|
return (
|
||||||
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
@ -69,16 +73,28 @@ export const ActionButton = (props: ActionButtonProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'externalLink': {
|
case 'externalLink': {
|
||||||
const { Element, icon, children, className, ...rest } = props
|
const {
|
||||||
|
Element,
|
||||||
|
to,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
className: _className,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
return (
|
return (
|
||||||
<a className={classNames} {...rest}>
|
<Link
|
||||||
|
to={to || paths.INDEX}
|
||||||
|
className={classNames}
|
||||||
|
{...rest}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const { Element, icon, children, className, ...rest } = props
|
const { Element, icon, children, className: _className, ...rest } = props
|
||||||
if (!Element) throw new Error('Element is required')
|
if (!Element) throw new Error('Element is required')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { CustomIcon, CustomIconName } from './CustomIcon'
|
import { CustomIcon, CustomIconName } from './CustomIcon'
|
||||||
|
|
||||||
const iconSizes = {
|
const iconSizes = {
|
||||||
sm: 12,
|
xs: 12,
|
||||||
md: 14.4,
|
sm: 14,
|
||||||
lg: 20,
|
md: 20,
|
||||||
xl: 28,
|
lg: 24,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionIconProps extends React.PropsWithChildren {
|
export interface ActionIconProps extends React.PropsWithChildren {
|
||||||
@ -30,20 +30,14 @@ export const ActionIcon = ({
|
|||||||
children,
|
children,
|
||||||
}: ActionIconProps) => {
|
}: ActionIconProps) => {
|
||||||
// By default, we reverse the icon color and background color in dark mode
|
// By default, we reverse the icon color and background color in dark mode
|
||||||
const computedIconClassName =
|
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
||||||
iconClassName ||
|
|
||||||
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
|
|
||||||
|
|
||||||
const computedBgClassName =
|
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
||||||
bgClassName ||
|
|
||||||
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`p-${
|
`w-fit inline-grid place-content-center ${className} ` +
|
||||||
size === 'xl' ? '2' : '1'
|
|
||||||
} w-fit inline-grid place-content-center ${className} ` +
|
|
||||||
computedBgClassName
|
computedBgClassName
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { IndexLoaderData } 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'
|
||||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
project?: ProjectWithEntryPointMetadata
|
project?: Omit<IndexLoaderData, 'code'>
|
||||||
className?: string
|
className?: string
|
||||||
enableMenu?: boolean
|
enableMenu?: boolean
|
||||||
}
|
}
|
||||||
@ -20,35 +22,50 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const {
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
auth: {
|
const { auth } = useGlobalStateContext()
|
||||||
context: { user },
|
const user = auth?.context?.user
|
||||||
},
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={
|
className={
|
||||||
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
|
'w-full grid ' +
|
||||||
styles.header +
|
styles.header +
|
||||||
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||||
className
|
className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
<ProjectSidebarMenu
|
||||||
|
renderAsLink={!enableMenu}
|
||||||
|
project={project?.project}
|
||||||
|
file={project?.file}
|
||||||
|
/>
|
||||||
{/* Toolbar if the context deems it */}
|
{/* Toolbar if the context deems it */}
|
||||||
{showToolbar && (
|
<div className="flex-grow flex justify-center 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">
|
{showToolbar ? (
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
</div>
|
) : (
|
||||||
)}
|
<ActionButton
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
Element="button"
|
||||||
{children || (
|
onClick={() => setCommandBarOpen(true)}
|
||||||
<div className="ml-auto flex items-center gap-1">
|
className="text-sm self-center flex items-center w-fit gap-3"
|
||||||
<NetworkHealthIndicator />
|
>
|
||||||
<UserSidebarMenu user={user} />
|
Command Palette{' '}
|
||||||
</div>
|
<kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
|
||||||
)}
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 ml-auto">
|
||||||
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<NetworkHealthIndicator />
|
||||||
|
<UserSidebarMenu user={user} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||||
ast: s.ast,
|
const { context } = useModelingContext()
|
||||||
setHighlightRange: s.setHighlightRange,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
}))
|
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
ast,
|
// TODO maybe need to have callback to make sure it stays in sync
|
||||||
selectionRanges.codeBasedSelections?.[0]?.range
|
kclManager.ast,
|
||||||
|
context.selectionRanges.codeBasedSelections?.[0]?.range
|
||||||
)
|
)
|
||||||
const node = getNodeFromPath(ast, pathToNode).node
|
const node = getNodeFromPath(kclManager.ast, pathToNode).node
|
||||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,7 +46,11 @@ export function AstExplorer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||||
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
|
<DisplayObj
|
||||||
|
obj={kclManager.ast}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -84,10 +88,8 @@ function DisplayObj({
|
|||||||
filterKeys: string[]
|
filterKeys: string[]
|
||||||
node: any
|
node: any
|
||||||
}) {
|
}) {
|
||||||
const { setHighlightRange, setCursor2 } = useStore((s) => ({
|
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||||
setHighlightRange: s.setHighlightRange,
|
const { send } = useModelingContext()
|
||||||
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)
|
||||||
@ -118,7 +120,16 @@ function DisplayObj({
|
|||||||
setHighlightRange([obj?.start || 0, obj.end])
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
|
send({
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'singleCodeCursor',
|
||||||
|
selection: {
|
||||||
|
type: 'default',
|
||||||
|
range: [obj?.start || 0, obj.end || 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -173,6 +184,7 @@ function DisplayObj({
|
|||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
import { parse, BinaryPart, Value } from '../lang/wasm'
|
||||||
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
|
||||||
import { executor } from '../lang/executor'
|
|
||||||
import {
|
import {
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
@ -9,7 +7,10 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
} from '../lang/modifyAst'
|
} from '../lang/modifyAst'
|
||||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||||
import { useStore } from '../useStore'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { executeAst } from 'useStore'
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
onVarClick,
|
onVarClick,
|
||||||
@ -92,14 +93,9 @@ export function useCalc({
|
|||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
} {
|
} {
|
||||||
const { ast, programMemory, selectionRange, engineCommandManager } = useStore(
|
const { programMemory } = useKclContext()
|
||||||
(s) => ({
|
const { context } = useModelingContext()
|
||||||
ast: s.ast,
|
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
||||||
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>
|
||||||
@ -119,9 +115,7 @@ export function useCalc({
|
|||||||
inputRef.current &&
|
inputRef.current &&
|
||||||
inputRef.current.setSelectionRange(0, String(value).length)
|
inputRef.current.setSelectionRange(0, String(value).length)
|
||||||
}, 100)
|
}, 100)
|
||||||
if (ast) {
|
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
||||||
setNewVariableName(findUniqueName(ast, valueName))
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -134,21 +128,32 @@ export function useCalc({
|
|||||||
}, [newVariableName])
|
}, [newVariableName])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ast || !programMemory || !selectionRange) return
|
if (!programMemory || !selectionRange) return
|
||||||
const varInfo = findAllPreviousVariables(ast, programMemory, selectionRange)
|
const varInfo = findAllPreviousVariables(
|
||||||
|
kclManager.ast,
|
||||||
|
kclManager.programMemory,
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
setAvailableVarInfo(varInfo)
|
setAvailableVarInfo(varInfo)
|
||||||
}, [ast, programMemory, selectionRange])
|
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!engineCommandManager) return
|
|
||||||
try {
|
try {
|
||||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
const code = `const __result__ = ${value}`
|
||||||
const ast = parser_wasm(code)
|
const ast = parse(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) => {
|
executeAst({
|
||||||
|
ast,
|
||||||
|
engineCommandManager,
|
||||||
|
defaultPlanes: kclManager.defaultPlanes,
|
||||||
|
useFakeExecutor: true,
|
||||||
|
programMemoryOverride: JSON.parse(
|
||||||
|
JSON.stringify(kclManager.programMemory)
|
||||||
|
),
|
||||||
|
}).then(({ programMemory }) => {
|
||||||
const resultDeclaration = ast.body.find(
|
const resultDeclaration = ast.body.find(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.type === 'VariableDeclaration' &&
|
a.type === 'VariableDeclaration' &&
|
||||||
@ -165,7 +170,7 @@ export function useCalc({
|
|||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
}
|
}
|
||||||
}, [value])
|
}, [value, availableVarInfo])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valueNode,
|
valueNode,
|
||||||
@ -210,7 +215,10 @@ export const CreateNewVariable = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
<label
|
||||||
|
htmlFor="create-new-variable"
|
||||||
|
className="block mt-3 font-mono text-gray-900"
|
||||||
|
>
|
||||||
Create new variable
|
Create new variable
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex gap-2 items-center">
|
<div className="mt-1 flex gap-2 items-center">
|
||||||
@ -221,6 +229,7 @@ export const CreateNewVariable = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setShouldCreateVariable(e.target.checked)
|
setShouldCreateVariable(e.target.checked)
|
||||||
}}
|
}}
|
||||||
|
className="bg-white text-gray-900"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
.button {
|
.button {
|
||||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
@apply ui-active:bg-energy-10/50 ui-active:text-inherit;
|
||||||
@apply transition-colors ease-out;
|
@apply transition-colors ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .button {
|
:global(.dark) .button {
|
||||||
@apply text-chalkboard-30;
|
@apply text-chalkboard-30;
|
||||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
@apply ui-active:bg-chalkboard-80 ui-active:text-energy-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button small {
|
.button small {
|
||||||
|
|||||||
@ -5,16 +5,13 @@ 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()
|
||||||
|
|
||||||
@ -33,15 +30,20 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
<Menu.Button className="p-0 border-none relative">
|
<Menu.Button className="p-0 border-none relative">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={faEllipsis}
|
icon={faEllipsis}
|
||||||
|
className="p-1"
|
||||||
|
size="sm"
|
||||||
bgClassName={
|
bgClassName={
|
||||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-energy-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded-sm'
|
||||||
}
|
}
|
||||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||||
/>
|
/>
|
||||||
</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 onClick={() => formatCode()} className={styles.button}>
|
<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>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface CollapsiblePanelProps
|
|||||||
icon?: IconDefinition
|
icon?: IconDefinition
|
||||||
open?: boolean
|
open?: boolean
|
||||||
menu?: React.ReactNode
|
menu?: React.ReactNode
|
||||||
|
detailsTestId?: string
|
||||||
iconClassNames?: {
|
iconClassNames?: {
|
||||||
bg?: string
|
bg?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@ -23,16 +24,17 @@ export const PanelHeader = ({
|
|||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
<div className="flex gap-2 align-center flex-1">
|
<div className="flex gap-2 align-center items-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
|
className="p-1"
|
||||||
|
size="sm"
|
||||||
bgClassName={
|
bgClassName={
|
||||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
|
||||||
(iconClassNames?.bg || '')
|
(iconClassNames?.bg || '')
|
||||||
}
|
}
|
||||||
iconClassName={
|
iconClassName={
|
||||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
'group-open:text-energy-10 ' + (iconClassNames?.icon || '')
|
||||||
(iconClassNames?.icon || '')
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{title}
|
{title}
|
||||||
@ -51,11 +53,13 @@ export const CollapsiblePanel = ({
|
|||||||
className,
|
className,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
menu,
|
menu,
|
||||||
|
detailsTestId,
|
||||||
...props
|
...props
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
{...props}
|
{...props}
|
||||||
|
data-testid={detailsTestId}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
className={styles.panel + ' group ' + (className || '')}
|
||||||
>
|
>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
|
|||||||
@ -4,18 +4,22 @@ import {
|
|||||||
Fragment,
|
Fragment,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
createContext,
|
createContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { ActionIcon } from './ActionIcon'
|
|
||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { Command, SubCommand } from '../lib/commands'
|
import {
|
||||||
|
Command,
|
||||||
|
CommandArgument,
|
||||||
|
CommandArgumentOption,
|
||||||
|
} from '../lib/commands'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
|
||||||
export type SortedCommand = {
|
type ComboboxOption = Command | CommandArgumentOption
|
||||||
item: Partial<Command | SubCommand> & { name: string }
|
type CommandArgumentData = [string, any]
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandsContext = createContext(
|
export const CommandsContext = createContext(
|
||||||
{} as {
|
{} as {
|
||||||
@ -35,12 +39,24 @@ export const CommandBarProvider = ({
|
|||||||
const [commands, internalSetCommands] = useState([] as Command[])
|
const [commands, internalSetCommands] = useState([] as Command[])
|
||||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||||
|
|
||||||
|
function sortCommands(a: Command, b: Command) {
|
||||||
|
if (b.owner === 'auth') return -1
|
||||||
|
if (a.owner === 'auth') return 1
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => console.log('commands updated', commands), [commands])
|
||||||
|
|
||||||
const addCommands = (newCommands: Command[]) => {
|
const addCommands = (newCommands: Command[]) => {
|
||||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
internalSetCommands((prevCommands) =>
|
||||||
|
[...newCommands, ...prevCommands].sort(sortCommands)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const removeCommands = (newCommands: Command[]) => {
|
const removeCommands = (newCommands: Command[]) => {
|
||||||
internalSetCommands((prevCommands) =>
|
internalSetCommands((prevCommands) =>
|
||||||
prevCommands.filter((command) => !newCommands.includes(command))
|
prevCommands
|
||||||
|
.filter((command) => !newCommands.includes(command))
|
||||||
|
.sort(sortCommands)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,152 +79,117 @@ export const CommandBarProvider = ({
|
|||||||
const CommandBar = () => {
|
const CommandBar = () => {
|
||||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||||
useHotkeys(['meta+k', 'meta+/'], () => {
|
useHotkeys(['meta+k', 'meta+/'], () => {
|
||||||
if (commands.length === 0) return
|
if (commands?.length === 0) return
|
||||||
setCommandBarOpen(!commandBarOpen)
|
setCommandBarOpen(!commandBarOpen)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
|
const [selectedCommand, setSelectedCommand] = useState<Command>()
|
||||||
null
|
const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
|
||||||
|
[]
|
||||||
)
|
)
|
||||||
// keep track of the current subcommand index
|
const [commandArgumentData, setCommandArgumentData] = useState<
|
||||||
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
CommandArgumentData[]
|
||||||
const [subCommandData, setSubCommandData] = useState<{
|
>([])
|
||||||
[key: string]: string
|
const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
|
||||||
}>({})
|
|
||||||
|
|
||||||
// if the subcommand index is null, we're not in a subcommand
|
|
||||||
const inSubCommand =
|
|
||||||
selectedCommand &&
|
|
||||||
'meta' in selectedCommand.item &&
|
|
||||||
selectedCommand.item.meta?.args !== undefined &&
|
|
||||||
subCommandIndex !== undefined
|
|
||||||
const currentSubCommand =
|
|
||||||
inSubCommand && 'meta' in selectedCommand.item
|
|
||||||
? selectedCommand.item.meta?.args[subCommandIndex]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
|
|
||||||
const availableCommands =
|
|
||||||
inSubCommand && currentSubCommand
|
|
||||||
? currentSubCommand.type === 'string'
|
|
||||||
? query
|
|
||||||
? [{ name: query }]
|
|
||||||
: currentSubCommand.options
|
|
||||||
: currentSubCommand.options
|
|
||||||
: commands
|
|
||||||
|
|
||||||
const fuse = new Fuse(availableCommands || [], {
|
|
||||||
keys: ['name', 'description'],
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredCommands = query
|
|
||||||
? fuse.search(query)
|
|
||||||
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
|
||||||
|
|
||||||
function clearState() {
|
function clearState() {
|
||||||
setQuery('')
|
|
||||||
setCommandBarOpen(false)
|
setCommandBarOpen(false)
|
||||||
setSelectedCommand(null)
|
setSelectedCommand(undefined)
|
||||||
setSubCommandIndex(undefined)
|
setCommandArguments([])
|
||||||
setSubCommandData({})
|
setCommandArgumentData([])
|
||||||
|
setCommandArgumentIndex(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCommandSelection(entry: SortedCommand) {
|
function selectCommand(command: Command) {
|
||||||
// If we have subcommands and have not yet gathered all the
|
console.log('selecting command', command)
|
||||||
// data required from them, set the selected command to the
|
if (!('args' in command && command.args?.length)) {
|
||||||
// current command and increment the subcommand index
|
submitCommand({ command })
|
||||||
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
} else {
|
||||||
setSelectedCommand(entry)
|
setCommandArguments(command.args)
|
||||||
setSubCommandIndex(0)
|
setSelectedCommand(command)
|
||||||
setQuery('')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { item } = entry
|
function stepBack() {
|
||||||
// If we have just selected a command with no subcommands, run it
|
if (!selectedCommand) {
|
||||||
const isCommandWithoutSubcommands =
|
clearState()
|
||||||
'callback' in item && !('meta' in item && item.meta)
|
} else {
|
||||||
if (isCommandWithoutSubcommands) {
|
if (commandArgumentIndex === 0) {
|
||||||
if (item.callback === undefined) return
|
setSelectedCommand(undefined)
|
||||||
item.callback()
|
} else {
|
||||||
setCommandBarOpen(false)
|
setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
|
||||||
return
|
}
|
||||||
}
|
if (commandArgumentData.length > 0) {
|
||||||
|
setCommandArgumentData((prevData) => prevData.slice(0, -1))
|
||||||
// If we have subcommands and have not yet gathered all the
|
|
||||||
// data required from them, set the selected command to the
|
|
||||||
// current command and increment the subcommand index
|
|
||||||
if (
|
|
||||||
selectedCommand &&
|
|
||||||
subCommandIndex !== undefined &&
|
|
||||||
'meta' in selectedCommand.item
|
|
||||||
) {
|
|
||||||
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
|
||||||
|
|
||||||
if (subCommand) {
|
|
||||||
const newSubCommandData = {
|
|
||||||
...subCommandData,
|
|
||||||
[subCommand.name]: item.name,
|
|
||||||
}
|
|
||||||
const newSubCommandIndex = subCommandIndex + 1
|
|
||||||
|
|
||||||
// If we have subcommands and have gathered all the data required
|
|
||||||
// from them, run the command with the gathered data
|
|
||||||
if (
|
|
||||||
selectedCommand.item.callback &&
|
|
||||||
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
|
||||||
) {
|
|
||||||
selectedCommand.item.callback(newSubCommandData)
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
} else {
|
|
||||||
// Otherwise, set the subcommand data and increment the subcommand index
|
|
||||||
setSubCommandData(newSubCommandData)
|
|
||||||
setSubCommandIndex(newSubCommandIndex)
|
|
||||||
setQuery('')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayValue(command: Command) {
|
function appendCommandArgumentData(data: { name: any }) {
|
||||||
if (command.meta?.displayValue === undefined || !command.meta.args)
|
const transformedData = [
|
||||||
return command.name
|
commandArguments[commandArgumentIndex].name,
|
||||||
return command.meta?.displayValue(
|
data.name,
|
||||||
command.meta.args.map((c) =>
|
]
|
||||||
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
if (commandArgumentIndex + 1 === commandArguments.length) {
|
||||||
|
submitCommand({
|
||||||
|
dataArr: [
|
||||||
|
...commandArgumentData,
|
||||||
|
transformedData,
|
||||||
|
] as CommandArgumentData[],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setCommandArgumentData(
|
||||||
|
(prevData) => [...prevData, transformedData] as CommandArgumentData[]
|
||||||
)
|
)
|
||||||
)
|
setCommandArgumentIndex((prevIndex) => prevIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCommand({
|
||||||
|
command = selectedCommand,
|
||||||
|
dataArr = commandArgumentData,
|
||||||
|
}) {
|
||||||
|
console.log('submitting command', command, dataArr)
|
||||||
|
if (dataArr.length === 0) {
|
||||||
|
command?.callback()
|
||||||
|
} else {
|
||||||
|
const data = Object.fromEntries(dataArr)
|
||||||
|
console.log('submitting data', data)
|
||||||
|
command?.callback(data)
|
||||||
|
}
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayValue(command: Command) {
|
||||||
|
if (
|
||||||
|
'args' in command &&
|
||||||
|
command.args &&
|
||||||
|
command.args?.length > 0 &&
|
||||||
|
'formatFunction' in command &&
|
||||||
|
command.formatFunction
|
||||||
|
) {
|
||||||
|
command.formatFunction(
|
||||||
|
command.args.map((c, i) =>
|
||||||
|
commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.name
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={
|
show={commandBarOpen || false}
|
||||||
commandBarOpen &&
|
|
||||||
availableCommands?.length !== undefined &&
|
|
||||||
availableCommands.length > 0
|
|
||||||
}
|
|
||||||
as={Fragment}
|
|
||||||
afterLeave={() => clearState()}
|
afterLeave={() => clearState()}
|
||||||
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Dialog
|
<Dialog
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setCommandBarOpen(false)
|
setCommandBarOpen(false)
|
||||||
clearState()
|
|
||||||
}}
|
}}
|
||||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
|
||||||
>
|
>
|
||||||
<Transition.Child
|
|
||||||
enter="duration-100 ease-out"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="duration-75 ease-in"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
|
||||||
</Transition.Child>
|
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
enter="duration-100 ease-out"
|
enter="duration-100 ease-out"
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom="opacity-0 scale-95"
|
||||||
@ -216,75 +197,208 @@ const CommandBar = () => {
|
|||||||
leave="duration-75 ease-in"
|
leave="duration-75 ease-in"
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
as={Fragment}
|
|
||||||
>
|
>
|
||||||
<Combobox
|
<Dialog.Panel
|
||||||
value={selectedCommand}
|
className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||||
onChange={handleCommandSelection}
|
|
||||||
className="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
|
||||||
as="div"
|
as="div"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
{!(
|
||||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
commandArguments &&
|
||||||
<div>
|
commandArguments.length &&
|
||||||
{inSubCommand && (
|
selectedCommand
|
||||||
<p className="text-liquid-70 dark:text-liquid-30">
|
) ? (
|
||||||
{selectedCommand.item &&
|
<CommandComboBox
|
||||||
getDisplayValue(selectedCommand.item as Command)}
|
options={commands}
|
||||||
|
handleSelection={selectCommand}
|
||||||
|
stepBack={stepBack}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="px-4 text-sm flex flex-wrap gap-2">
|
||||||
|
<p className="pr-4 flex gap-2 items-center">
|
||||||
|
{selectedCommand &&
|
||||||
|
'icon' in selectedCommand &&
|
||||||
|
selectedCommand.icon && (
|
||||||
|
<CustomIcon
|
||||||
|
name={selectedCommand.icon}
|
||||||
|
className="w-5 h-5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{getDisplayValue(selectedCommand)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
{commandArguments.map((arg, i) => (
|
||||||
<Combobox.Input
|
<p
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
key={arg.name}
|
||||||
className="w-full bg-transparent focus:outline-none"
|
className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
||||||
onKeyDown={(event) => {
|
i === commandArgumentIndex
|
||||||
if (event.metaKey && event.key === 'k')
|
? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
|
||||||
setCommandBarOpen(false)
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
if (
|
}`}
|
||||||
inSubCommand &&
|
>
|
||||||
event.key === 'Backspace' &&
|
{commandArgumentIndex >= i && commandArgumentData[i] ? (
|
||||||
!event.currentTarget.value
|
commandArgumentData[i][1]
|
||||||
) {
|
) : arg.defaultValue ? (
|
||||||
setSubCommandIndex(subCommandIndex - 1)
|
arg.defaultValue
|
||||||
setSelectedCommand(null)
|
) : (
|
||||||
}
|
<em>{arg.name}</em>
|
||||||
}}
|
)}
|
||||||
displayValue={(command: SortedCommand) =>
|
|
||||||
command !== null ? command.item.name : ''
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
inSubCommand
|
|
||||||
? `Enter <${currentSubCommand?.name}>`
|
|
||||||
: 'Search for a command'
|
|
||||||
}
|
|
||||||
value={query}
|
|
||||||
autoCapitalize="off"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Combobox.Options static className="overflow-y-auto max-h-96">
|
|
||||||
{filteredCommands?.map((commandResult) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={commandResult.item.name}
|
|
||||||
value={commandResult}
|
|
||||||
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
|
|
||||||
>
|
|
||||||
<p>{commandResult.item.name}</p>
|
|
||||||
{(commandResult.item as SubCommand).description && (
|
|
||||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
|
||||||
{(commandResult.item as SubCommand).description}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
))}
|
||||||
</Combobox.Option>
|
</div>
|
||||||
))}
|
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
||||||
</Combobox.Options>
|
<Argument
|
||||||
</Combobox>
|
arg={commandArguments[commandArgumentIndex]}
|
||||||
|
appendCommandArgumentData={appendCommandArgumentData}
|
||||||
|
stepBack={stepBack}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Argument({
|
||||||
|
arg,
|
||||||
|
appendCommandArgumentData,
|
||||||
|
stepBack,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument
|
||||||
|
appendCommandArgumentData: Dispatch<SetStateAction<any>>
|
||||||
|
stepBack: () => void
|
||||||
|
}) {
|
||||||
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}
|
||||||
|
}, [arg, inputRef])
|
||||||
|
|
||||||
|
return arg.type === 'select' ? (
|
||||||
|
<CommandComboBox
|
||||||
|
options={arg.options}
|
||||||
|
handleSelection={appendCommandArgumentData}
|
||||||
|
stepBack={stepBack}
|
||||||
|
placeholder="Select an option"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
appendCommandArgumentData({ name: inputRef.current?.value })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="flex items-center mx-4 my-4">
|
||||||
|
<span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
||||||
|
{arg.name}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
|
placeholder="Enter a value"
|
||||||
|
defaultValue={arg.defaultValue}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
||||||
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default CommandBarProvider
|
export default CommandBarProvider
|
||||||
|
|
||||||
|
function CommandComboBox({
|
||||||
|
options,
|
||||||
|
handleSelection,
|
||||||
|
stepBack,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
options: ComboboxOption[]
|
||||||
|
handleSelection: Dispatch<SetStateAction<any>>
|
||||||
|
stepBack: () => void
|
||||||
|
placeholder?: string
|
||||||
|
}) {
|
||||||
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
|
||||||
|
|
||||||
|
const defaultOption =
|
||||||
|
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
||||||
|
|
||||||
|
const fuse = new Fuse(options, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
|
setFilteredOptions(query.length > 0 ? results : options)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
|
||||||
|
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
|
||||||
|
<CustomIcon
|
||||||
|
name="search"
|
||||||
|
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
|
||||||
|
/>
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
||||||
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
(defaultOption && defaultOption.name) ||
|
||||||
|
placeholder ||
|
||||||
|
'Search commands'
|
||||||
|
}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
|
>
|
||||||
|
{filteredOptions?.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.name}
|
||||||
|
value={option}
|
||||||
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||||
|
>
|
||||||
|
{'icon' in option && option.icon && (
|
||||||
|
<CustomIcon
|
||||||
|
name={option.icon}
|
||||||
|
className="w-5 h-5 dark:text-energy-10"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="flex-grow">{option.name} </p>
|
||||||
|
{'isCurrent' in option && option.isCurrent && (
|
||||||
|
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
|
current
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,21 @@
|
|||||||
export type CustomIconName =
|
export type CustomIconName =
|
||||||
|
| 'arrowDown'
|
||||||
|
| 'arrowLeft'
|
||||||
|
| 'arrowRight'
|
||||||
|
| 'arrowUp'
|
||||||
|
| 'close'
|
||||||
| 'equal'
|
| 'equal'
|
||||||
| 'exit'
|
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
|
| 'file'
|
||||||
|
| 'filePlus'
|
||||||
|
| 'folder'
|
||||||
|
| 'folderPlus'
|
||||||
|
| 'gear'
|
||||||
| 'horizontal'
|
| 'horizontal'
|
||||||
| 'line'
|
| 'line'
|
||||||
| 'move'
|
| 'move'
|
||||||
| 'parallel'
|
| 'parallel'
|
||||||
|
| 'search'
|
||||||
| 'sketch'
|
| 'sketch'
|
||||||
| 'vertical'
|
| 'vertical'
|
||||||
|
|
||||||
@ -16,6 +26,86 @@ export const CustomIcon = ({
|
|||||||
name: CustomIconName
|
name: CustomIconName
|
||||||
} & React.SVGProps<SVGSVGElement>) => {
|
} & React.SVGProps<SVGSVGElement>) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case 'arrowDown':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 17.7071L9.64648 17.3535L6.14648 13.8535L6.85359 13.1464L9.50004 15.7929V2.99997H10.5V15.7929L13.1465 13.1464L13.8536 13.8535L10.3536 17.3535L10 17.7071Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'arrowLeft':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2.29291 10L2.64646 9.64645L6.14646 6.14645L6.85357 6.85356L4.20712 9.50001L17 9.50001V10.5L4.20712 10.5L6.85357 13.1465L6.14646 13.8536L2.64646 10.3536L2.29291 10Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'arrowRight':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M17.7071 10L17.3536 10.3536L13.8536 13.8536L13.1464 13.1465L15.7929 10.5H3V9.50001H15.7929L13.1464 6.85356L13.8536 6.14645L17.3536 9.64645L17.7071 10Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'arrowUp':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 2.29288L10.3536 2.64643L13.8536 6.14643L13.1465 6.85354L10.5 4.20709V17H9.50004V4.20709L6.85359 6.85354L6.14648 6.14643L9.64648 2.64643L10 2.29288Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'close':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M9.2929 10L6.46448 7.17158L7.17158 6.46448L10 9.2929L12.8284 6.46448L13.5355 7.17158L10.7071 10L13.5355 12.8284L12.8284 13.5355L10 10.7071L7.17158 13.5355L6.46448 12.8284L9.2929 10Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'equal':
|
case 'equal':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -30,21 +120,6 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
case 'exit':
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'extrude':
|
case 'extrude':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -61,6 +136,86 @@ 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
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V16.5V17H15.5H4.5H4V16.5V3.5V3ZM5 4V16H15V8.50001H11H10.5V8.00001V4H5ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'filePlus':
|
||||||
|
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 'folder':
|
||||||
|
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.5V16V16.5H16H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM15.5 8H4.5V15.5H15.5V8Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'folderPlus':
|
||||||
|
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 'gear':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8.61477 3.0884L5.87402 4.67077L6.50004 5.75505L5.25004 7.92011H4.0047V11.07H5.25004L6.50004 13.2351L5.86973 14.3268L8.62776 15.9191L9.24503 14.85H11.745L12.3647 15.9234L15.1416 14.3202L14.5151 13.2351L15.7651 11.07H16.9951V7.92011H15.7651L14.5151 5.75505L15.1373 4.67741L12.3778 3.08423L11.7451 4.18012H9.24508L8.61477 3.0884ZM10.4999 13C12.4329 13 13.9999 11.433 13.9999 9.50003C13.9999 7.56703 12.4329 6.00003 10.4999 6.00003C8.56687 6.00003 6.99986 7.56703 6.99986 9.50003C6.99986 11.433 8.56687 13 10.4999 13Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'horizontal':
|
case 'horizontal':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@ -125,6 +280,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'search':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M14.016 9.00482C14.016 10.662 12.6731 12.0048 11.0172 12.0048C9.3613 12.0048 8.01841 10.662 8.01841 9.00482C8.01841 7.34768 9.3613 6.00482 11.0172 6.00482C12.6731 6.00482 14.016 7.34768 14.016 9.00482ZM15.016 9.00482C15.016 11.214 13.2257 13.0048 11.0172 13.0048C10.082 13.0048 9.22178 12.6837 8.54074 12.1456L5.6912 14.9952L4.98409 14.2881L7.83921 11.433C7.32431 10.7597 7.01841 9.91799 7.01841 9.00482C7.01841 6.79568 8.80873 5.00482 11.0172 5.00482C13.2257 5.00482 15.016 6.79568 15.016 9.00482Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'sketch':
|
case 'sketch':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -1,104 +1,20 @@
|
|||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { EngineCommand } from '../lang/std/engineConnection'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { ActionButton } from '../components/ActionButton'
|
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { isReducedMotion } from 'lang/util'
|
|
||||||
import { AstExplorer } from './AstExplorer'
|
import { AstExplorer } from './AstExplorer'
|
||||||
|
import { EngineCommands } from './EngineCommands'
|
||||||
type SketchModeCmd = Extract<
|
|
||||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
|
||||||
{ type: 'default_camera_enable_sketch_mode' }
|
|
||||||
>
|
|
||||||
|
|
||||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||||
const { engineCommandManager } = useStore((s) => ({
|
|
||||||
engineCommandManager: s.engineCommandManager,
|
|
||||||
}))
|
|
||||||
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
|
|
||||||
type: 'default_camera_enable_sketch_mode',
|
|
||||||
origin: { x: 0, y: 0, z: 0 },
|
|
||||||
x_axis: { x: 1, y: 0, z: 0 },
|
|
||||||
y_axis: { x: 0, y: 1, z: 0 },
|
|
||||||
distance_to_plane: 100,
|
|
||||||
ortho: true,
|
|
||||||
animated: !isReducedMotion(),
|
|
||||||
})
|
|
||||||
if (!sketchModeCmd) return null
|
|
||||||
return (
|
return (
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
{...props}
|
{...props}
|
||||||
className={
|
className={
|
||||||
'!absolute overflow-hidden !h-auto bottom-5 right-5 ' + className
|
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
|
||||||
}
|
}
|
||||||
// header height, top-5, and bottom-5
|
// header height, top-5, and bottom-5
|
||||||
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
||||||
|
detailsTestId="debug-panel"
|
||||||
>
|
>
|
||||||
<section className="p-4 flex flex-col gap-4">
|
<section className="p-4 flex flex-col gap-4">
|
||||||
<Xyz
|
<EngineCommands />
|
||||||
onChange={setSketchModeCmd}
|
|
||||||
pointKey="origin"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<Xyz
|
|
||||||
onChange={setSketchModeCmd}
|
|
||||||
pointKey="x_axis"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<Xyz
|
|
||||||
onChange={setSketchModeCmd}
|
|
||||||
pointKey="y_axis"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="pr-4">distance_to_plane</div>
|
|
||||||
<input
|
|
||||||
className="w-16 dark:bg-chalkboard-90"
|
|
||||||
type="number"
|
|
||||||
value={sketchModeCmd.distance_to_plane}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
setSketchModeCmd({
|
|
||||||
...sketchModeCmd,
|
|
||||||
distance_to_plane: Number(target.value),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="pr-4">ortho</div>
|
|
||||||
<input
|
|
||||||
className="w-16"
|
|
||||||
type="checkbox"
|
|
||||||
checked={sketchModeCmd.ortho}
|
|
||||||
onChange={(a) => {
|
|
||||||
console.log(a, (a as any).checked)
|
|
||||||
setSketchModeCmd({
|
|
||||||
...sketchModeCmd,
|
|
||||||
ortho: a.target.checked,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={() => {
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: sketchModeCmd,
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="hover:border-succeed-50"
|
|
||||||
icon={{
|
|
||||||
icon: faCheck,
|
|
||||||
bgClassName:
|
|
||||||
'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
|
|
||||||
iconClassName:
|
|
||||||
'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Send sketch mode command
|
|
||||||
</ActionButton>
|
|
||||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||||
<AstExplorer />
|
<AstExplorer />
|
||||||
</div>
|
</div>
|
||||||
@ -106,41 +22,3 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
|||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Xyz = ({
|
|
||||||
pointKey,
|
|
||||||
data,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
pointKey: 'origin' | 'y_axis' | 'x_axis'
|
|
||||||
data: SketchModeCmd
|
|
||||||
onChange: (a: SketchModeCmd) => void
|
|
||||||
}) => {
|
|
||||||
if (!data) return null
|
|
||||||
return (
|
|
||||||
<div className="flex">
|
|
||||||
<div className="pr-4">{pointKey}</div>
|
|
||||||
{Object.entries(data[pointKey]).map(([axis, val]) => {
|
|
||||||
return (
|
|
||||||
<div key={axis} className="flex">
|
|
||||||
<div className="w-4">{axis}</div>
|
|
||||||
<input
|
|
||||||
className="w-16 dark:bg-chalkboard-90"
|
|
||||||
type="number"
|
|
||||||
value={val}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
onChange({
|
|
||||||
...data,
|
|
||||||
[pointKey]: {
|
|
||||||
...data[pointKey],
|
|
||||||
[axis]: Number(target.value),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
const DownloadAppBanner = () => {
|
const DownloadAppBanner = () => {
|
||||||
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
||||||
@ -24,7 +23,8 @@ const DownloadAppBanner = () => {
|
|||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => setBannerDismissed(true)}
|
onClick={() => setBannerDismissed(true)}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faX,
|
icon: 'close',
|
||||||
|
className: 'p-1',
|
||||||
bgClassName:
|
bgClassName:
|
||||||
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||||
iconClassName:
|
iconClassName:
|
||||||
|
|||||||
85
src/components/EngineCommands.tsx
Normal file
85
src/components/EngineCommands.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { CommandLog, engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
function useEngineCommands(): [CommandLog[], () => void] {
|
||||||
|
const [engineCommands, setEngineCommands] = useState<CommandLog[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
engineCommandManager.registerCommandLogCallback((commands) =>
|
||||||
|
setEngineCommands(commands)
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [engineCommands, () => engineCommandManager.clearCommandLogs()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EngineCommands = () => {
|
||||||
|
const [engineCommands, clearEngineCommands] = useEngineCommands()
|
||||||
|
const [containsFilter, setContainsFilter] = useState('')
|
||||||
|
const [customCmd, setCustomCmd] = useState('')
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="text-gray-800 bg-slate-300 px-2"
|
||||||
|
data-testid="filter-input"
|
||||||
|
type="text"
|
||||||
|
value={containsFilter}
|
||||||
|
onChange={(e) => setContainsFilter(e.target.value)}
|
||||||
|
placeholder="Filter"
|
||||||
|
/>
|
||||||
|
<div className="max-w-xl max-h-36 overflow-auto">
|
||||||
|
{engineCommands.map((command, index) => {
|
||||||
|
const stringer = JSON.stringify(command)
|
||||||
|
if (containsFilter && !stringer.includes(containsFilter)) return null
|
||||||
|
return (
|
||||||
|
<pre className="text-xs" key={index}>
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
data-message-type={command.type}
|
||||||
|
data-command-type={
|
||||||
|
(command.type === 'send-modeling' ||
|
||||||
|
command.type === 'send-scene') &&
|
||||||
|
command.data.type === 'modeling_cmd_req'
|
||||||
|
? command?.data?.cmd?.type
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
data-command-id={
|
||||||
|
(command.type === 'send-modeling' ||
|
||||||
|
command.type === 'send-scene') &&
|
||||||
|
command.data.type === 'modeling_cmd_req'
|
||||||
|
? command.data.cmd_id
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
data-receive-command-type={
|
||||||
|
command.type === 'receive-reliable' ? command.cmd_type : ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{JSON.stringify(command, null, 2)}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button data-testid="clear-commands" onClick={clearEngineCommands}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
className="text-gray-800 bg-slate-300 px-2"
|
||||||
|
type="text"
|
||||||
|
value={customCmd}
|
||||||
|
onChange={(e) => setCustomCmd(e.target.value)}
|
||||||
|
placeholder="JSON command"
|
||||||
|
data-testid="custom-cmd-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="custom-cmd-send-button"
|
||||||
|
onClick={() =>
|
||||||
|
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Send custom command
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -47,11 +47,9 @@ export const ErrorPage = () => {
|
|||||||
Clear storage
|
Clear storage
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="link"
|
Element="externalLink"
|
||||||
icon={{ icon: faBug }}
|
icon={{ icon: faBug }}
|
||||||
target="_blank"
|
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
||||||
rel="noopener noreferrer"
|
|
||||||
to="https://discord.com/channels/915388055236509727/1138967922614743060"
|
|
||||||
>
|
>
|
||||||
Report Bug
|
Report Bug
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
@ -1,31 +1,40 @@
|
|||||||
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
|
||||||
// If we wanted more classname configuration of sub-elements,
|
icon?: string
|
||||||
// put them here
|
bg?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(defaultType)
|
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
|
||||||
|
const defaultStorage = 'embedded'
|
||||||
|
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
@ -38,7 +47,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: 'embedded',
|
storage: defaultStorage,
|
||||||
presentation: 'pretty',
|
presentation: 'pretty',
|
||||||
}
|
}
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
@ -66,7 +75,25 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
engineCommandManager?.sendSceneCommand({
|
if (
|
||||||
|
values.type === 'obj' ||
|
||||||
|
values.type === 'stl' ||
|
||||||
|
values.type === 'ply'
|
||||||
|
) {
|
||||||
|
values.units = baseUnit
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
values.type === 'ply' ||
|
||||||
|
values.type === 'stl' ||
|
||||||
|
values.type === 'gltf'
|
||||||
|
) {
|
||||||
|
// Set the storage type.
|
||||||
|
values.storage = storage
|
||||||
|
}
|
||||||
|
if (values.type === 'ply' || values.type === 'stl') {
|
||||||
|
values.selection = { type: 'default_scene' }
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'export',
|
type: 'export',
|
||||||
@ -75,6 +102,7 @@ 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(),
|
||||||
})
|
})
|
||||||
@ -88,7 +116,13 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{ icon: faFileExport }}
|
icon={{
|
||||||
|
icon: faFileExport,
|
||||||
|
className: 'p-1',
|
||||||
|
size: 'sm',
|
||||||
|
iconClassName: className?.icon,
|
||||||
|
bgClassName: className?.bg,
|
||||||
|
}}
|
||||||
className={className?.button}
|
className={className?.button}
|
||||||
>
|
>
|
||||||
{children || 'Export'}
|
{children || 'Export'}
|
||||||
@ -108,8 +142,19 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
name="type"
|
name="type"
|
||||||
|
data-testid="export-type"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setType(e.target.value)
|
setType(e.target.value as OutputTypeKey)
|
||||||
|
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"
|
||||||
@ -127,10 +172,11 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<select
|
<select
|
||||||
id="storage"
|
id="storage"
|
||||||
name="storage"
|
name="storage"
|
||||||
onChange={formik.handleChange}
|
data-testid="export-storage"
|
||||||
value={
|
onChange={(e) => {
|
||||||
'storage' in formik.values ? formik.values.storage : ''
|
setStorage(e.target.value as StorageUnion)
|
||||||
}
|
formik.handleChange(e)
|
||||||
|
}}
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
>
|
>
|
||||||
{type === 'gltf' && (
|
{type === 'gltf' && (
|
||||||
@ -140,13 +186,13 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<option value="standard">standard</option>
|
<option value="standard">standard</option>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{type === 'ply' && (
|
{type === 'stl' && (
|
||||||
<>
|
<>
|
||||||
<option value="ascii">ascii</option>
|
<option value="ascii">ascii</option>
|
||||||
<option value="binary">binary</option>
|
<option value="binary">binary</option>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{type === 'stl' && (
|
{type === 'ply' && (
|
||||||
<>
|
<>
|
||||||
<option value="ascii">ascii</option>
|
<option value="ascii">ascii</option>
|
||||||
<option value="binary_little_endian">
|
<option value="binary_little_endian">
|
||||||
@ -168,6 +214,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faXmark,
|
icon: faXmark,
|
||||||
|
className: 'p-1',
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
iconClassName:
|
iconClassName:
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||||
@ -179,7 +226,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
type="submit"
|
type="submit"
|
||||||
icon={{ icon: faFileExport }}
|
icon={{ icon: faFileExport, className: 'p-1' }}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
158
src/components/FileMachineProvider.tsx
Normal file
158
src/components/FileMachineProvider.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
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
|
||||||
16
src/components/FileTree.module.css
Normal file
16
src/components/FileTree.module.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
400
src/components/FileTree.tsx
Normal file
400
src/components/FileTree.tsx
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import { IndexLoaderData, paths } from 'Router'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
import { FileEntry } from '@tauri-apps/api/fs'
|
||||||
|
import { Dispatch, 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 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
|
||||||
|
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-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'filePlus',
|
||||||
|
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||||
|
bgClassName:
|
||||||
|
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 bg-transparent !outline-none"
|
||||||
|
onClick={createFile}
|
||||||
|
>
|
||||||
|
<Tooltip position="inlineStart" delay={750}>
|
||||||
|
Create File
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'folderPlus',
|
||||||
|
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||||
|
bgClassName:
|
||||||
|
'bg-chalkboard-20/50 hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import { useMachine } from '@xstate/react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import {
|
import {
|
||||||
authCommandBarMeta,
|
authCommandBarConfig,
|
||||||
authMachine,
|
authMachine,
|
||||||
TOKEN_PERSIST_KEY,
|
TOKEN_PERSIST_KEY,
|
||||||
} from '../machines/authMachine'
|
} from '../machines/authMachine'
|
||||||
@ -11,7 +11,7 @@ import React, { createContext, useEffect, useRef } from 'react'
|
|||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import {
|
import {
|
||||||
SETTINGS_PERSIST_KEY,
|
SETTINGS_PERSIST_KEY,
|
||||||
settingsCommandBarMeta,
|
settingsCommandBarConfig,
|
||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from 'machines/settingsMachine'
|
} from 'machines/settingsMachine'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
@ -24,9 +24,7 @@ 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>
|
||||||
@ -87,7 +85,7 @@ export const GlobalStateProvider = ({
|
|||||||
send: settingsSend,
|
send: settingsSend,
|
||||||
commands,
|
commands,
|
||||||
owner: 'settings',
|
owner: 'settings',
|
||||||
commandBarMeta: settingsCommandBarMeta,
|
commandBarConfig: settingsCommandBarConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for changes to the system theme and update the app theme accordingly
|
// Listen for changes to the system theme and update the app theme accordingly
|
||||||
@ -126,7 +124,7 @@ export const GlobalStateProvider = ({
|
|||||||
state: authState,
|
state: authState,
|
||||||
send: authSend,
|
send: authSend,
|
||||||
commands,
|
commands,
|
||||||
commandBarMeta: authCommandBarMeta,
|
commandBarConfig: authCommandBarConfig,
|
||||||
owner: 'auth',
|
owner: 'auth',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
|||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [setHasLongLoadTime])
|
}, [setHasLongLoadTime])
|
||||||
return (
|
return (
|
||||||
<div className="body-bg flex flex-col items-center justify-center h-screen">
|
<div
|
||||||
|
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||||
|
data-testid="loading"
|
||||||
|
>
|
||||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||||
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
||||||
<circle
|
<circle
|
||||||
|
|||||||
@ -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,9 +11,7 @@ interface LogPanelProps extends CollapsiblePanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||||
const { logs } = useStore(({ logs }) => ({
|
const { logs } = useKclContext()
|
||||||
logs,
|
|
||||||
}))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
if (element) {
|
if (element) {
|
||||||
@ -47,21 +45,19 @@ export const KCLErrors = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: LogPanelProps) => {
|
}: LogPanelProps) => {
|
||||||
const { kclErrors } = useStore(({ kclErrors }) => ({
|
const { errors } = useKclContext()
|
||||||
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
|
||||||
}
|
}
|
||||||
}, [kclErrors])
|
}, [errors])
|
||||||
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={kclErrors}
|
src={errors}
|
||||||
collapsed={1}
|
collapsed={1}
|
||||||
collapseStringsAfterLength={60}
|
collapseStringsAfterLength={60}
|
||||||
enableClipboard={false}
|
enableClipboard={false}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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 } from '../lang/rust'
|
import { initPromise, parse } from '../lang/wasm'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -15,18 +14,20 @@ describe('processMemory', () => {
|
|||||||
}
|
}
|
||||||
const otherVar = myFn(5)
|
const otherVar = myFn(5)
|
||||||
|
|
||||||
const theExtrude = startSketchAt([0, 0])
|
const theExtrude = startSketchOn('XY')
|
||||||
|
|> 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 = startSketchAt([0, 0])
|
const theSketch = startSketchOn('XY')
|
||||||
|
|> 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 = parser_wasm(code)
|
const ast = parse(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/executor'
|
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
||||||
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,9 +13,7 @@ export const MemoryPanel = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: MemoryPanelProps) => {
|
}: MemoryPanelProps) => {
|
||||||
const { programMemory } = useStore((s) => ({
|
const { programMemory } = useKclContext()
|
||||||
programMemory: s.programMemory,
|
|
||||||
}))
|
|
||||||
const ProcessedMemory = useMemo(
|
const ProcessedMemory = useMemo(
|
||||||
() => processMemory(programMemory),
|
() => processMemory(programMemory),
|
||||||
[programMemory]
|
[programMemory]
|
||||||
@ -50,7 +48,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') {
|
||||||
|
|||||||
566
src/components/ModelingMachineProvider.tsx
Normal file
566
src/components/ModelingMachineProvider.tsx
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
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,
|
||||||
|
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 {
|
||||||
|
angleBetweenInfo,
|
||||||
|
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'
|
||||||
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
|
|
||||||
|
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: 'start_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 (!editorView) return {}
|
||||||
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||||
|
return { selectionRanges: setSelections.selection }
|
||||||
|
else if (setSelections.selectionType === 'otherSelection') {
|
||||||
|
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
||||||
|
// const axisAddCmd: EngineCommand = {
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'highlight_set_entities',
|
||||||
|
// entities: [
|
||||||
|
// setSelections.selection === 'x-axis'
|
||||||
|
// ? X_AXIS_UUID
|
||||||
|
// : Y_AXIS_UUID,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!isShiftDown) {
|
||||||
|
// engineCommandManager
|
||||||
|
// .sendSceneCommand({
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'select_clear',
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const {
|
||||||
|
codeMirrorSelection,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
otherSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
codeSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!setSelections.selection) {
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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 (angleBetweenInfo({
|
||||||
|
selectionRanges,
|
||||||
|
}).enabled
|
||||||
|
? applyConstraintAngleBetween({
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
: applyConstraintAngleLength({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength: 'setAngle',
|
||||||
|
}))
|
||||||
|
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
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
||||||
|
{
|
||||||
|
constraint: 'xAbs',
|
||||||
|
selectionRanges,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
||||||
|
{
|
||||||
|
constraint: 'yAbs',
|
||||||
|
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,5 +1,4 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar'
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export const NetworkHealthIndicator = () => {
|
|||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={
|
className={
|
||||||
'p-0 border-none relative ' +
|
'p-0 border-none bg-transparent dark:bg-transparent relative ' +
|
||||||
(hasIssues
|
(hasIssues
|
||||||
? 'focus-visible:outline-destroy-80'
|
? 'focus-visible:outline-destroy-80'
|
||||||
: 'focus-visible:outline-succeed-80')
|
: 'focus-visible:outline-succeed-80')
|
||||||
@ -56,15 +56,17 @@ export const NetworkHealthIndicator = () => {
|
|||||||
<span className="sr-only">Network Health</span>
|
<span className="sr-only">Network Health</span>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={faWifi}
|
icon={faWifi}
|
||||||
|
className="p-1"
|
||||||
iconClassName={
|
iconClassName={
|
||||||
hasIssues
|
hasIssues
|
||||||
? 'text-destroy-80 dark:text-destroy-30'
|
? 'text-destroy-80 dark:text-destroy-30'
|
||||||
: 'text-succeed-80 dark:text-succeed-30'
|
: 'text-succeed-80 dark:text-succeed-30'
|
||||||
}
|
}
|
||||||
bgClassName={
|
bgClassName={
|
||||||
hasIssues
|
'bg-transparent dark:bg-transparent ' +
|
||||||
|
(hasIssues
|
||||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormEvent, useState } from 'react'
|
import { FormEvent, useEffect, useRef, 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 } from '../lib/tauriFS'
|
import { FILE_EXT, getPartsCount, readProject } 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,10 +28,14 @@ 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)
|
||||||
|
|
||||||
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleRenameProject(e, project).then(() => setIsEditing(false))
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayedTime(date: Date) {
|
function getDisplayedTime(date: Date) {
|
||||||
@ -42,33 +46,56 @@ function ProjectCard({
|
|||||||
: date.toLocaleTimeString()
|
: date.toLocaleTimeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getNumberOfParts() {
|
||||||
|
const { kclFileCount, kclDirCount } = getPartsCount(
|
||||||
|
await readProject(project.path)
|
||||||
|
)
|
||||||
|
setNumberOfParts(kclFileCount)
|
||||||
|
setNumberOfFolders(kclDirCount)
|
||||||
|
}
|
||||||
|
void getNumberOfParts()
|
||||||
|
}, [project.path])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}
|
||||||
|
}, [inputRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
{...props}
|
{...props}
|
||||||
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-90 hover:border-chalkboard-30 dark:hover:border-chalkboard-80"
|
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-90 hover:border-energy-10 dark:hover:border-chalkboard-70 hover:bg-energy-10/20 dark:hover:bg-chalkboard-90"
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<form onSubmit={handleSave} className="flex gap-2 items-center">
|
<form onSubmit={handleSave} className="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
className="dark:bg-chalkboard-80 dark:border-chalkboard-40 min-w-0 p-1"
|
className="dark:bg-chalkboard-80 dark:border-chalkboard-40 min-w-0 p-1 selection:bg-energy-10/20 focus:outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
id="newProjectName"
|
id="newProjectName"
|
||||||
name="newProjectName"
|
name="newProjectName"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
defaultValue={project.name}
|
defaultValue={project.name}
|
||||||
autoFocus={true}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-1 items-center">
|
<div className="flex gap-1 items-center">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
type="submit"
|
type="submit"
|
||||||
icon={{ icon: faCheck, size: 'sm' }}
|
icon={{ icon: faCheck, size: 'sm', className: 'p-1' }}
|
||||||
className="!p-0"
|
className="!p-0"
|
||||||
></ActionButton>
|
></ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{ icon: faX, size: 'sm' }}
|
icon={{
|
||||||
|
icon: faX,
|
||||||
|
size: 'sm',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
className: 'p-1',
|
||||||
|
}}
|
||||||
className="!p-0"
|
className="!p-0"
|
||||||
onClick={() => setIsEditing(false)}
|
onClick={() => setIsEditing(false)}
|
||||||
/>
|
/>
|
||||||
@ -76,34 +103,54 @@ function ProjectCard({
|
|||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="p-1 flex flex-col gap-2">
|
<div className="p-1 flex flex-col h-full gap-2">
|
||||||
<Link
|
<Link
|
||||||
|
className="flex-1 text-liquid-100 after:content-[''] after:absolute after:inset-0"
|
||||||
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
||||||
className="flex-1 text-liquid-100"
|
|
||||||
>
|
>
|
||||||
{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">
|
||||||
Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)}
|
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
||||||
|
{numberOfFolders > 0 &&
|
||||||
|
`/ ${numberOfFolders} folder${
|
||||||
|
numberOfFolders === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
</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">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
|
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
|
||||||
|
</span>
|
||||||
|
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{ icon: faPenAlt, size: 'sm' }}
|
icon={{
|
||||||
onClick={() => setIsEditing(true)}
|
icon: faPenAlt,
|
||||||
|
className: 'p-1',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsEditing(true)
|
||||||
|
}}
|
||||||
className="!p-0"
|
className="!p-0"
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{
|
icon={{
|
||||||
icon: faTrashAlt,
|
icon: faTrashAlt,
|
||||||
size: 'sm',
|
className: 'p-1',
|
||||||
bgClassName: 'bg-destroy-80 hover:bg-destroy-70',
|
size: 'xs',
|
||||||
iconClassName:
|
bgClassName: 'bg-destroy-80',
|
||||||
'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',
|
iconClassName: 'text-destroy-20 dark:text-destroy-40',
|
||||||
}}
|
}}
|
||||||
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
onClick={() => setIsConfirmingDelete(true)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsConfirmingDelete(true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +183,8 @@ function ProjectCard({
|
|||||||
icon={{
|
icon={{
|
||||||
icon: faTrashAlt,
|
icon: faTrashAlt,
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
|
className: 'p-1',
|
||||||
|
size: 'sm',
|
||||||
iconClassName:
|
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',
|
'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',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ 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 = {
|
||||||
@ -13,7 +15,7 @@ const projectWellFormed = {
|
|||||||
path: '/some/path/Simple Box/main.kcl',
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
entrypoint_metadata: {
|
entrypointMetadata: {
|
||||||
accessedAt: now,
|
accessedAt: now,
|
||||||
blksize: 32,
|
blksize: 32,
|
||||||
blocks: 32,
|
blocks: 32,
|
||||||
@ -38,7 +40,11 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,7 +61,11 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Renders app name if given no project', () => {
|
test('Renders app name if given no project', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ProjectSidebarMenu />
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu />
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,7 +79,14 @@ 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>
|
||||||
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu
|
||||||
|
project={projectWellFormed}
|
||||||
|
renderAsLink={true}
|
||||||
|
/>
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +1,36 @@
|
|||||||
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 { ProjectWithEntryPointMetadata, paths } from '../Router'
|
import { IndexLoaderData, 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?: Partial<ProjectWithEntryPointMetadata>
|
project?: IndexLoaderData['project']
|
||||||
|
file?: IndexLoaderData['file']
|
||||||
}) => {
|
}) => {
|
||||||
return renderAsLink ? (
|
return renderAsLink ? (
|
||||||
<Link
|
<Link
|
||||||
to={paths.HOME}
|
to={paths.HOME}
|
||||||
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
|
||||||
data-testid="project-sidebar-link"
|
data-testid="project-sidebar-link"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/kitt-8bit-winking.svg"
|
src="/kitt-8bit-winking.svg"
|
||||||
alt="KittyCAD App"
|
alt="KittyCAD App"
|
||||||
className="h-9 w-auto"
|
className="w-auto h-9"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block"
|
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap 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'}
|
||||||
@ -35,17 +39,26 @@ const ProjectSidebarMenu = ({
|
|||||||
) : (
|
) : (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50 dark:hover:bg-chalkboard-90"
|
||||||
data-testid="project-sidebar-toggle"
|
data-testid="project-sidebar-toggle"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/kitt-8bit-winking.svg"
|
src="/kitt-8bit-winking.svg"
|
||||||
alt="KittyCAD App"
|
alt="KittyCAD App"
|
||||||
className="h-full w-auto"
|
className="w-auto h-full"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block">
|
<div className="flex flex-col items-start py-0.5">
|
||||||
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
||||||
</span>
|
{isTauri() && file?.name
|
||||||
|
? 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"
|
||||||
@ -56,7 +69,7 @@ const ProjectSidebarMenu = ({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
<Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -68,54 +81,71 @@ const ProjectSidebarMenu = ({
|
|||||||
leaveTo="opacity-0 -translate-x-4"
|
leaveTo="opacity-0 -translate-x-4"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
|
<Popover.Panel
|
||||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-md shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-chalkboard-40 dark:border-chalkboard-80"
|
||||||
<img
|
style={{ gridTemplateRows: 'auto 1fr auto' }}
|
||||||
src="/kitt-8bit-winking.svg"
|
>
|
||||||
alt="KittyCAD App"
|
{({ close }) => (
|
||||||
className="h-9 w-auto"
|
<>
|
||||||
/>
|
<div className="flex items-center gap-4 px-4 py-3">
|
||||||
|
<img
|
||||||
|
src="/kitt-8bit-winking.svg"
|
||||||
|
alt="KittyCAD App"
|
||||||
|
className="w-auto h-9"
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="m-0 text-energy-10 text-mono"
|
className="m-0 text-chalkboard-100 dark: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?.entrypoint_metadata && (
|
{project?.entrypointMetadata && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-energy-40 text-xs"
|
className="m-0 text-xs text-chalkboard-100 dark:text-energy-40"
|
||||||
data-testid="createdAt"
|
data-testid="createdAt"
|
||||||
>
|
>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
{project.entrypointMetadata.createdAt.toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isTauri() ? (
|
||||||
|
<FileTree
|
||||||
|
file={file}
|
||||||
|
className="overflow-hidden border-0 border-y border-chalkboard-30 dark:border-chalkboard-80"
|
||||||
|
closePanel={close}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 overflow-hidden" />
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
||||||
</div>
|
<ExportButton
|
||||||
<div className="p-4 flex flex-col gap-2">
|
className={{
|
||||||
<ExportButton
|
button: 'border-transparent dark:border-transparent',
|
||||||
className={{
|
}}
|
||||||
button:
|
>
|
||||||
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
Export Model
|
||||||
}}
|
</ExportButton>
|
||||||
>
|
{isTauri() && (
|
||||||
Export Model
|
<ActionButton
|
||||||
</ExportButton>
|
Element="link"
|
||||||
{isTauri() && (
|
to={paths.HOME}
|
||||||
<ActionButton
|
icon={{
|
||||||
Element="link"
|
icon: faHome,
|
||||||
to={paths.HOME}
|
className: 'p-1',
|
||||||
icon={{
|
size: 'sm',
|
||||||
icon: faHome,
|
}}
|
||||||
}}
|
className="border-transparent dark:border-transparent hover:bg-energy-10/20 dark:hover:bg-chalkboard-90"
|
||||||
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
|
>
|
||||||
>
|
Go to Home
|
||||||
Go to Home
|
</ActionButton>
|
||||||
</ActionButton>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { Value } from '../lang/abstractSyntaxTreeTypes'
|
import { type InstanceProps, create } from 'react-modal-promise'
|
||||||
|
import { Value } from '../lang/wasm'
|
||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
@ -9,6 +10,28 @@ 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,
|
||||||
@ -16,20 +39,7 @@ 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(
|
||||||
@ -98,7 +108,7 @@ export const SetAngleLengthModal = ({
|
|||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex">
|
<div className="mt-1 flex">
|
||||||
<button
|
<button
|
||||||
className="border border-gray-300 px-2"
|
className="border border-gray-300 px-2 text-gray-900"
|
||||||
onClick={() => setSign(-sign)}
|
onClick={() => setSign(-sign)}
|
||||||
>
|
>
|
||||||
{sign > 0 ? '+' : '-'}
|
{sign > 0 ? '+' : '-'}
|
||||||
@ -108,7 +118,7 @@ export const SetAngleLengthModal = ({
|
|||||||
type="text"
|
type="text"
|
||||||
name="val"
|
name="val"
|
||||||
id="val"
|
id="val"
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1"
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 text-gray-900"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue(e.target.value)
|
setValue(e.target.value)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { Value } from '../lang/abstractSyntaxTreeTypes'
|
import { type InstanceProps, create } from 'react-modal-promise'
|
||||||
|
import { Value } from '../lang/wasm'
|
||||||
import {
|
import {
|
||||||
AvailableVars,
|
AvailableVars,
|
||||||
addToInputHelper,
|
addToInputHelper,
|
||||||
@ -9,6 +10,30 @@ 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,
|
||||||
@ -17,25 +42,12 @@ 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(String(Math.abs(initialValue)))
|
const [value, setValue] = useState(
|
||||||
|
initialValue === undefined ? '' : String(Math.abs(initialValue))
|
||||||
|
)
|
||||||
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
|
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -75,7 +87,7 @@ export const GetInfoModal = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white/90 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-gray-900"
|
className="text-lg font-medium leading-6 text-gray-900"
|
||||||
@ -97,7 +109,7 @@ export const GetInfoModal = ({
|
|||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex">
|
<div className="mt-1 flex">
|
||||||
<button
|
<button
|
||||||
className="border border-gray-300 px-2 mr-1"
|
className="border border-gray-400 px-2 mr-1 text-gray-900"
|
||||||
onClick={() => setSign(-sign)}
|
onClick={() => setSign(-sign)}
|
||||||
>
|
>
|
||||||
{sign > 0 ? '+' : '-'}
|
{sign > 0 ? '+' : '-'}
|
||||||
@ -107,7 +119,7 @@ export const GetInfoModal = ({
|
|||||||
name="val"
|
name="val"
|
||||||
id="val"
|
id="val"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue(e.target.value)
|
setValue(e.target.value)
|
||||||
@ -127,7 +139,7 @@ export const GetInfoModal = ({
|
|||||||
name="segName"
|
name="segName"
|
||||||
id="segName"
|
id="segName"
|
||||||
disabled={!isSegNameEditable}
|
disabled={!isSegNameEditable}
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
|
||||||
value={segName}
|
value={segName}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSegName(e.target.value)
|
setSegName(e.target.value)
|
||||||
|
|||||||
@ -4,19 +4,26 @@ 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,24 +7,18 @@ 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, roundOff } from '../lib/utils'
|
import { getNormalisedCoordinates } 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 { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
|
||||||
import { modify_ast_for_sketch } from '../wasm-lib/pkg/wasm_lib'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { KCLError } from 'lang/errors'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||||
import { rangeTypeFix } from 'lang/abstractSyntaxTree'
|
import { changeSketchArguments } from 'lang/std/sketch'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }) => {
|
export const Stream = ({ className = '' }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -32,37 +26,21 @@ 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 {
|
const { settings } = useGlobalStateContext()
|
||||||
settings: {
|
const cameraControls = settings?.context?.cameraControls
|
||||||
context: { cameraControls },
|
const { send, state, context } = useModelingContext()
|
||||||
},
|
const { isExecuting } = useKclContext()
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -73,7 +51,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, engineCommandManager])
|
}, [mediaStream])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
@ -106,8 +84,14 @@ export const Stream = ({ className = '' }) => {
|
|||||||
interaction = 'zoom'
|
interaction = 'zoom'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
if (state.matches('Sketch.Move Tool')) {
|
||||||
engineCommandManager?.sendSceneCommand({
|
if (
|
||||||
|
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',
|
||||||
@ -115,13 +99,8 @@ export const Stream = ({ className = '' }) => {
|
|||||||
},
|
},
|
||||||
cmd_id: newId,
|
cmd_id: newId,
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (!state.matches('Sketch.Line Tool')) {
|
||||||
!(
|
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',
|
||||||
@ -139,7 +118,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',
|
||||||
@ -176,215 +155,211 @@ export const Stream = ({ className = '' }) => {
|
|||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!didDragInStream) {
|
if (!didDragInStream && state.matches('Sketch no face')) {
|
||||||
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 },
|
||||||
}
|
}
|
||||||
} else if (
|
engineCommandManager.sendSceneCommand(command)
|
||||||
(!didDragInStream &&
|
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
|
||||||
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 (
|
||||||
guiMode.mode === 'sketch' &&
|
!didDragInStream &&
|
||||||
guiMode.sketchMode === ('move' as any)
|
(state.matches('Sketch.SketchIdle') ||
|
||||||
|
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 () => {
|
||||||
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
|
if (!context.sketchPathToNode) return
|
||||||
if (!(guiMode.mode === 'sketch')) return
|
getNodeFromPath<VariableDeclarator>(
|
||||||
|
kclManager.ast,
|
||||||
if (guiMode.sketchMode === 'selectFace') return
|
context.sketchPathToNode,
|
||||||
|
|
||||||
// Check if the sketch group already exists.
|
|
||||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
||||||
ast,
|
|
||||||
guiMode.pathToNode,
|
|
||||||
'VariableDeclarator'
|
|
||||||
).node
|
|
||||||
const variableName = varDec?.id?.name
|
|
||||||
const sketchGroup = programMemory.root[variableName]
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
|
||||||
|
|
||||||
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',
|
|
||||||
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 _addStartSketch = addStartSketch(
|
|
||||||
ast,
|
|
||||||
[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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
const variableName = varDec.id.name
|
// Get the current plane string for plane we are on.
|
||||||
const sketchGroup = programMemory.root[variableName]
|
let currentPlaneString = ''
|
||||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||||
const initialCoords = sketchGroup.value[0].from
|
currentPlaneString = 'XY'
|
||||||
|
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
||||||
const isClose = compareVec2Epsilon(initialCoords, [
|
currentPlaneString = 'YZ'
|
||||||
coords[1].x,
|
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
||||||
coords[1].y,
|
currentPlaneString = 'XZ'
|
||||||
])
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
// Do not supporting editing/moving lines on a non-default plane.
|
||||||
|
// Eventually we can support this but for now we will just throw an
|
||||||
|
// error.
|
||||||
|
if (currentPlaneString === '') return
|
||||||
|
|
||||||
|
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'path_get_info',
|
||||||
|
path_id: context.sketchEnginePathId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const segmentsWithMappings = (
|
||||||
|
pathInfo?.data?.data?.segments as { command_id: string }[]
|
||||||
|
)
|
||||||
|
.filter(({ command_id }) => {
|
||||||
|
return command_id && engineCommandManager.artifactMap[command_id]
|
||||||
|
})
|
||||||
|
.map(({ command_id }) => command_id)
|
||||||
|
const segment2dInfo = await Promise.all(
|
||||||
|
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 = kclManager.safeParse(code)
|
||||||
|
if (!astWithCurrentRanges) return
|
||||||
|
const updateNode = getNodeFromPath<CallExpression>(
|
||||||
|
astWithCurrentRanges,
|
||||||
|
modded.pathToNode
|
||||||
|
).node
|
||||||
|
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
|
||||||
|
updateNode.start,
|
||||||
|
updateNode.end,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
kclManager.executeAstMock(modifiedAst, true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
engineCommandManager.sendSceneCommand(command)
|
||||||
|
}
|
||||||
|
|
||||||
setDidDragInStream(false)
|
setDidDragInStream(false)
|
||||||
setClickCoords(undefined)
|
setClickCoords(undefined)
|
||||||
}
|
}
|
||||||
@ -415,12 +390,15 @@ export const Stream = ({ className = '' }) => {
|
|||||||
onWheel={handleScroll}
|
onWheel={handleScroll}
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
onMouseMoveCapture={handleMouseMove}
|
onMouseMoveCapture={handleMouseMove}
|
||||||
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||||
|
disablePictureInPicture
|
||||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
<Loading>Loading stream...</Loading>
|
<Loading>
|
||||||
|
<span data-testid="loading-stream">Loading stream...</span>
|
||||||
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,23 +13,18 @@ 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 { Selections, useStore } from 'useStore'
|
import { 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 { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||||
import { useParams } from 'react-router-dom'
|
import { roundOff } from 'lib/utils'
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
|
||||||
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import {
|
|
||||||
EditorView,
|
|
||||||
addLineHighlight,
|
|
||||||
lineHighlightField,
|
|
||||||
} from 'editor/highlightextension'
|
|
||||||
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: {
|
||||||
@ -47,38 +42,28 @@ export const TextEditor = ({
|
|||||||
}: {
|
}: {
|
||||||
theme: Themes.Light | Themes.Dark
|
theme: Themes.Light | Themes.Dark
|
||||||
}) => {
|
}) => {
|
||||||
const pathParams = useParams()
|
|
||||||
const {
|
const {
|
||||||
code,
|
|
||||||
deferredSetCode,
|
|
||||||
editorView,
|
editorView,
|
||||||
engineCommandManager,
|
|
||||||
formatCode,
|
|
||||||
isLSPServerReady,
|
isLSPServerReady,
|
||||||
selectionRanges,
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
setEditorView,
|
setEditorView,
|
||||||
setIsLSPServerReady,
|
setIsLSPServerReady,
|
||||||
setSelectionRanges,
|
isShiftDown,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
code: s.code,
|
|
||||||
deferredSetCode: s.deferredSetCode,
|
|
||||||
editorView: s.editorView,
|
editorView: s.editorView,
|
||||||
engineCommandManager: s.engineCommandManager,
|
|
||||||
formatCode: s.formatCode,
|
|
||||||
isLSPServerReady: s.isLSPServerReady,
|
isLSPServerReady: s.isLSPServerReady,
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
|
||||||
setEditorView: s.setEditorView,
|
setEditorView: s.setEditorView,
|
||||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||||
setSelectionRanges: s.setSelectionRanges,
|
isShiftDown: s.isShiftDown,
|
||||||
}))
|
}))
|
||||||
|
const { code, errors } = useKclContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
settings: {
|
context: { selectionRanges, selectionRangeTypeMap },
|
||||||
context: { textWrapping },
|
send,
|
||||||
},
|
} = 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()
|
||||||
@ -104,7 +89,7 @@ export const TextEditor = ({
|
|||||||
// Here we initialize the plugin which will start the client.
|
// Here we initialize the plugin which will start the client.
|
||||||
// When we have multi-file support the name of the file will be a dep of
|
// When we have multi-file support the name of the file will be a dep of
|
||||||
// this use memo, as well as the directory structure, which I think is
|
// this use memo, as well as the directory structure, which I think is
|
||||||
// a good setup becuase it will restart the client but not the server :)
|
// a good setup because it will restart the client but not the server :)
|
||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
const kclLSP = useMemo(() => {
|
const kclLSP = useMemo(() => {
|
||||||
let plugin = null
|
let plugin = null
|
||||||
@ -123,78 +108,25 @@ 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 = (value: string, viewUpdate: ViewUpdate) => {
|
const onChange = (newCode: string) => {
|
||||||
deferredSetCode(value)
|
kclManager.setCodeAndExecute(newCode)
|
||||||
if (isTauri() && pathParams.id) {
|
|
||||||
// Save the file to disk
|
|
||||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
|
||||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
|
||||||
(err) => {
|
|
||||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
|
||||||
console.error('error saving file', err)
|
|
||||||
toast.error('Error saving file, please check file permissions')
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (editorView) {
|
|
||||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
|
||||||
}
|
|
||||||
} //, []);
|
} //, []);
|
||||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||||
if (!editorView) {
|
if (!editorView) {
|
||||||
setEditorView(viewUpdate.view)
|
setEditorView(viewUpdate.view)
|
||||||
}
|
}
|
||||||
const ranges = viewUpdate.state.selection.ranges
|
const eventInfo = processCodeMirrorRanges({
|
||||||
|
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
if (!eventInfo) return
|
||||||
|
|
||||||
const isChange =
|
send(eventInfo.modelingEvent)
|
||||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
eventInfo.engineEvents.forEach((event) =>
|
||||||
ranges.some(({ from, to }, i) => {
|
engineCommandManager.sendSceneCommand(event)
|
||||||
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(() => {
|
||||||
@ -211,7 +143,7 @@ export const TextEditor = ({
|
|||||||
{
|
{
|
||||||
key: editorShortcutMeta.formatCode.codeMirror,
|
key: editorShortcutMeta.formatCode.codeMirror,
|
||||||
run: () => {
|
run: () => {
|
||||||
formatCode()
|
kclManager.format()
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -235,7 +167,7 @@ export const TextEditor = ({
|
|||||||
extensions.push(
|
extensions.push(
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
linter((_view) => {
|
linter((_view) => {
|
||||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
return kclErrToDiagnostic(errors)
|
||||||
}),
|
}),
|
||||||
interact({
|
interact({
|
||||||
rules: [
|
rules: [
|
||||||
@ -274,7 +206,7 @@ export const TextEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}, [kclLSP, textWrapping])
|
}, [kclLSP, textWrapping, convertCallback])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,105 +1,79 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { Selections } from 'lib/selections'
|
||||||
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
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 { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
|
||||||
import { sketchButtonClassnames } from 'Toolbar'
|
|
||||||
|
|
||||||
export const EqualAngle = () => {
|
export function equalAngleInfo({
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
selectionRanges,
|
||||||
useStore((s) => ({
|
}: {
|
||||||
guiMode: s.guiMode,
|
selectionRanges: Selections
|
||||||
ast: s.ast,
|
}) {
|
||||||
updateAst: s.updateAst,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
selectionRanges: s.selectionRanges,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
const [enableEqual, setEnableEqual] = 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 varDecs = paths.map(
|
|
||||||
(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,
|
|
||||||
'equalAngle'
|
|
||||||
)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
const _enableEqual =
|
|
||||||
!!secondaryVarDecs.length &&
|
|
||||||
isAllTooltips &&
|
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
theTransforms.every(Boolean)
|
|
||||||
setEnableEqual(_enableEqual)
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
if (guiMode.mode !== 'sketch') return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
if (!(transformInfos && ast)) return
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
transformSecondarySketchLinesTagFirst({
|
|
||||||
ast,
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory,
|
|
||||||
})
|
|
||||||
updateAst(modifiedAst, true, {
|
|
||||||
callBack: updateCursors(setCursor, selectionRanges, 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>
|
|
||||||
)
|
)
|
||||||
|
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.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const transforms = getTransformInfos(
|
||||||
|
{
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
kclManager.ast,
|
||||||
|
'equalAngle'
|
||||||
|
)
|
||||||
|
|
||||||
|
const enabled =
|
||||||
|
!!secondaryVarDecs.length &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
transforms.every(Boolean)
|
||||||
|
return { enabled, transforms }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConstraintEqualAngle({
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
}): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const { transforms } = equalAngleInfo({ selectionRanges })
|
||||||
|
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
||||||
|
ast: kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
})
|
||||||
|
return { modifiedAst, pathToNodeMap }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,105 +1,83 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { Selections } from 'lib/selections'
|
||||||
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
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 { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
|
||||||
import { sketchButtonClassnames } from 'Toolbar'
|
|
||||||
|
|
||||||
export const EqualLength = () => {
|
export function setEqualLengthInfo({
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
selectionRanges,
|
||||||
useStore((s) => ({
|
}: {
|
||||||
guiMode: s.guiMode,
|
selectionRanges: Selections
|
||||||
ast: s.ast,
|
}) {
|
||||||
updateAst: s.updateAst,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
selectionRanges: s.selectionRanges,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
const [enableEqual, setEnableEqual] = 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 varDecs = paths.map(
|
|
||||||
(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,
|
|
||||||
'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 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.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const transforms = getTransformInfos(
|
||||||
|
{
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
kclManager.ast,
|
||||||
|
'equalLength'
|
||||||
|
)
|
||||||
|
|
||||||
|
const enabled =
|
||||||
|
!!secondaryVarDecs.length &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
transforms.every(Boolean)
|
||||||
|
|
||||||
|
return { enabled, transforms }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConstraintEqualLength({
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
}): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const { 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,84 +1,57 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { Selections } from 'lib/selections'
|
||||||
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
TransformInfo,
|
PathToNodeMap,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
|
||||||
import { sketchButtonClassnames } from 'Toolbar'
|
|
||||||
|
|
||||||
export const HorzVert = ({
|
export function horzVertInfo(
|
||||||
horOrVert,
|
selectionRanges: Selections,
|
||||||
}: {
|
|
||||||
horOrVert: 'vertical' | 'horizontal'
|
horOrVert: 'vertical' | 'horizontal'
|
||||||
}) => {
|
) {
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
useStore((s) => ({
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
guiMode: s.guiMode,
|
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
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(selectionRanges, ast, horOrVert)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
|
||||||
setEnableHorz(_enableHorz)
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
if (guiMode.mode !== 'sketch') return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!transformInfos || !ast) return
|
|
||||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
|
||||||
ast,
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory,
|
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
|
const nodes = paths.map(
|
||||||
|
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const theTransforms = getTransformInfos(
|
||||||
|
selectionRanges,
|
||||||
|
kclManager.ast,
|
||||||
|
horOrVert
|
||||||
|
)
|
||||||
|
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||||
|
return { enabled: _enableHorz, transforms: theTransforms }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConstraintHorzVert(
|
||||||
|
selectionRanges: Selections,
|
||||||
|
horOrVert: 'vertical' | 'horizontal',
|
||||||
|
ast: Program,
|
||||||
|
programMemory: ProgramMemory
|
||||||
|
): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const transformInfos = horzVertInfo(selectionRanges, horOrVert).transforms
|
||||||
|
return transformAstSketchLines({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { create } from 'react-modal-promise'
|
import { Selections } from 'lib/selections'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
|
||||||
BinaryPart,
|
|
||||||
Value,
|
|
||||||
VariableDeclarator,
|
|
||||||
} from '../../lang/abstractSyntaxTreeTypes'
|
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
@ -13,184 +8,170 @@ 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 } from '../SetHorVertDistanceModal'
|
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
const getModalInfo = create(GetInfoModal as any)
|
const getModalInfo = createInfoModal(GetInfoModal)
|
||||||
|
|
||||||
export const Intersect = () => {
|
export function intersectInfo({
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
selectionRanges,
|
||||||
useStore((s) => ({
|
}: {
|
||||||
guiMode: s.guiMode,
|
selectionRanges: Selections
|
||||||
ast: s.ast,
|
}) {
|
||||||
updateAst: s.updateAst,
|
if (selectionRanges.codeBasedSelections.length < 2) {
|
||||||
selectionRanges: s.selectionRanges,
|
return {
|
||||||
programMemory: s.programMemory,
|
enabled: false,
|
||||||
setCursor: s.setCursor,
|
transforms: [],
|
||||||
}))
|
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(
|
||||||
ast,
|
kclManager.ast,
|
||||||
programMemory,
|
kclManager.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,
|
...selectionRanges,
|
||||||
codeBasedSelections: [
|
codeBasedSelections: [
|
||||||
selectionRanges.codeBasedSelections?.[0],
|
selectionRanges.codeBasedSelections?.[0],
|
||||||
shouldUsePreviousSegment
|
shouldUsePreviousSegment
|
||||||
? {
|
? {
|
||||||
range: previousSegment.sourceRange,
|
range: previousSegment.sourceRange,
|
||||||
type: 'line-end',
|
type: 'line-end',
|
||||||
}
|
|
||||||
: selectionRanges.codeBasedSelections?.[1],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
setForcedSelectionRanges(_forcedSelectionRanges)
|
|
||||||
|
|
||||||
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
|
||||||
getNodePathFromSourceRange(ast, range)
|
|
||||||
)
|
|
||||||
const nodes = paths.map(
|
|
||||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
|
||||||
)
|
|
||||||
const varDecs = paths.map(
|
|
||||||
(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, {
|
: selectionRanges.codeBasedSelections?.[1],
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
],
|
||||||
})
|
}
|
||||||
}
|
|
||||||
}}
|
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
disabled={!enable}
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
title="Set Perpendicular Distance"
|
|
||||||
>
|
|
||||||
Set Perpendicular Distance
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
codeBasedSelections: _forcedSelectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
kclManager.ast,
|
||||||
|
'intersect'
|
||||||
|
)
|
||||||
|
|
||||||
|
const _enableEqual =
|
||||||
|
secondaryVarDecs.length === 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// transform again but forcing certain values
|
||||||
|
const finalValue = removeDoubleNegatives(
|
||||||
|
valueNode as BinaryPart,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap: _pathToNodeMap,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,78 +1,63 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { Selections } from 'lib/selections'
|
||||||
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
import { Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
TransformInfo,
|
PathToNodeMap,
|
||||||
getRemoveConstraintsTransforms,
|
getRemoveConstraintsTransforms,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
export const RemoveConstrainingValues = () => {
|
export function removeConstrainingValuesInfo({
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
selectionRanges,
|
||||||
useStore((s) => ({
|
}: {
|
||||||
guiMode: s.guiMode,
|
selectionRanges: Selections
|
||||||
ast: s.ast,
|
}) {
|
||||||
updateAst: s.updateAst,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
selectionRanges: s.selectionRanges,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const theTransforms = getRemoveConstraintsTransforms(
|
|
||||||
selectionRanges,
|
|
||||||
ast,
|
|
||||||
'removeConstrainingValues'
|
|
||||||
)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
|
||||||
setEnableHorz(_enableHorz)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
if (guiMode.mode !== 'sketch') return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!transformInfos || !ast) return
|
|
||||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
|
||||||
ast,
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory,
|
|
||||||
referenceSegName: '',
|
|
||||||
})
|
|
||||||
updateAst(modifiedAst, true, {
|
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
disabled={!enableHorz}
|
|
||||||
title="Remove Constraining Values"
|
|
||||||
>
|
|
||||||
Remove Constraining Values
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
|
const nodes = paths.map(
|
||||||
|
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||||
|
)
|
||||||
|
const isAllTooltips = nodes.every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'CallExpression' &&
|
||||||
|
toolTips.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transforms = getRemoveConstraintsTransforms(
|
||||||
|
selectionRanges,
|
||||||
|
kclManager.ast,
|
||||||
|
'removeConstrainingValues'
|
||||||
|
)
|
||||||
|
|
||||||
|
const enabled = isAllTooltips && transforms.every(Boolean)
|
||||||
|
return { enabled, transforms }
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return { enabled: false, transforms: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyRemoveConstrainingValues({
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
}): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const { transforms } = removeConstrainingValuesInfo({ selectionRanges })
|
||||||
|
return transformAstSketchLines({
|
||||||
|
ast: kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,145 +1,152 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { create } from 'react-modal-promise'
|
import { Selections } from 'lib/selections'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
TransformInfo,
|
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
ConstraintType,
|
PathToNodeMap,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
import {
|
||||||
|
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 { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
const getModalInfo = create(SetAngleLengthModal as any)
|
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||||
|
|
||||||
type ButtonType = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
type Constraint = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||||
|
|
||||||
const buttonLabels: Record<ButtonType, string> = {
|
export function absDistanceInfo({
|
||||||
xAbs: 'Set distance from X Axis',
|
selectionRanges,
|
||||||
yAbs: 'Set distance from Y Axis',
|
constraint,
|
||||||
snapToYAxis: 'Snap To Y Axis',
|
}: {
|
||||||
snapToXAxis: 'Snap To X Axis',
|
selectionRanges: Selections
|
||||||
}
|
constraint: Constraint
|
||||||
|
}) {
|
||||||
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
const disType =
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
constraint === 'xAbs' || constraint === 'yAbs'
|
||||||
useStore((s) => ({
|
? constraint
|
||||||
guiMode: s.guiMode,
|
: constraint === 'snapToYAxis'
|
||||||
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 [enableAngLen, setEnableAngLen] = useState(false)
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
useEffect(() => {
|
|
||||||
if (!ast) return
|
|
||||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
|
||||||
getNodePathFromSourceRange(ast, range)
|
|
||||||
)
|
|
||||||
const nodes = paths.map(
|
|
||||||
(pathToNode) =>
|
|
||||||
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
toolTips.includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(selectionRanges, ast, disType)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
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 _enableHorz =
|
|
||||||
isAllTooltips &&
|
|
||||||
theTransforms.every(Boolean) &&
|
|
||||||
selectionRanges.codeBasedSelections.length === 1 &&
|
|
||||||
(enableX || enableY)
|
|
||||||
setEnableAngLen(_enableHorz)
|
|
||||||
}, [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>
|
|
||||||
)
|
)
|
||||||
|
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(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
|
||||||
|
}
|
||||||
|
return { modifiedAst: _modifiedAst, pathToNodeMap }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConstraintAxisAlign({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
constraint: 'snapToYAxis' | 'snapToXAxis'
|
||||||
|
}): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const transformInfos = absDistanceInfo({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
}).transforms
|
||||||
|
|
||||||
|
let finalValue = createIdentifier('_0')
|
||||||
|
|
||||||
|
return transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
|
selectionRanges: selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,155 +1,136 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { create } from 'react-modal-promise'
|
import { Selections } from 'lib/selections'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
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 } from '../SetHorVertDistanceModal'
|
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
const getModalInfo = create(GetInfoModal as any)
|
const getModalInfo = createInfoModal(GetInfoModal)
|
||||||
|
|
||||||
export const SetAngleBetween = () => {
|
export function angleBetweenInfo({
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
selectionRanges,
|
||||||
useStore((s) => ({
|
}: {
|
||||||
guiMode: s.guiMode,
|
selectionRanges: Selections
|
||||||
ast: s.ast,
|
}) {
|
||||||
updateAst: s.updateAst,
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
selectionRanges: s.selectionRanges,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
const [enable, setEnable] = 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 varDecs = paths.map(
|
|
||||||
(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>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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.includes(node.callee.name as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const theTransforms = getTransformInfos(
|
||||||
|
{
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
kclManager.ast,
|
||||||
|
'setAngleBetween'
|
||||||
|
)
|
||||||
|
|
||||||
|
const _enableEqual =
|
||||||
|
selectionRanges.otherSelections.length === 0 &&
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap: _pathToNodeMap,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,188 +1,170 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
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,
|
||||||
ConstraintType,
|
PathToNodeMap,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { Selections } from 'lib/selections'
|
||||||
import { sketchButtonClassnames } from 'Toolbar'
|
|
||||||
|
|
||||||
const getModalInfo = create(GetInfoModal as any)
|
const getModalInfo = createInfoModal(GetInfoModal)
|
||||||
|
|
||||||
type ButtonType =
|
export function horzVertDistanceInfo({
|
||||||
| 'setHorzDistance'
|
selectionRanges,
|
||||||
| 'setVertDistance'
|
constraint,
|
||||||
| 'alignEndsHorizontally'
|
|
||||||
| 'alignEndsVertically'
|
|
||||||
|
|
||||||
const buttonLabels: Record<ButtonType, string> = {
|
|
||||||
setHorzDistance: 'Set Horizontal Distance',
|
|
||||||
setVertDistance: 'Set Vertical Distance',
|
|
||||||
alignEndsHorizontally: 'Align Ends Horizontally',
|
|
||||||
alignEndsVertically: 'Align Ends Vertically',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SetHorzVertDistance = ({
|
|
||||||
buttonType,
|
|
||||||
}: {
|
}: {
|
||||||
buttonType: ButtonType
|
selectionRanges: Selections
|
||||||
}) => {
|
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
}) {
|
||||||
useStore((s) => ({
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
guiMode: s.guiMode,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
const constraint: ConstraintType =
|
|
||||||
buttonType === 'setHorzDistance' || buttonType === 'setVertDistance'
|
|
||||||
? buttonType
|
|
||||||
: buttonType === 'alignEndsHorizontally'
|
|
||||||
? 'setVertDistance'
|
|
||||||
: 'setHorzDistance'
|
|
||||||
const [enable, setEnable] = 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 varDecs = paths.map(
|
|
||||||
(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: selectionRanges.codeBasedSelections.slice(1),
|
|
||||||
},
|
|
||||||
ast,
|
|
||||||
constraint
|
|
||||||
)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
const _enableEqual =
|
|
||||||
secondaryVarDecs.length === 1 &&
|
|
||||||
isAllTooltips &&
|
|
||||||
isOthersLinkedToPrimary &&
|
|
||||||
theTransforms.every(Boolean)
|
|
||||||
setEnable(_enableEqual)
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
if (guiMode.mode !== 'sketch') return null
|
|
||||||
|
|
||||||
const isAlign =
|
|
||||||
buttonType === 'alignEndsHorizontally' ||
|
|
||||||
buttonType === 'alignEndsVertically'
|
|
||||||
|
|
||||||
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 (!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>
|
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||||
|
},
|
||||||
|
kclManager.ast,
|
||||||
|
constraint
|
||||||
|
)
|
||||||
|
const _enableEqual =
|
||||||
|
secondaryVarDecs.length === 1 &&
|
||||||
|
isAllTooltips &&
|
||||||
|
isOthersLinkedToPrimary &&
|
||||||
|
theTransforms.every(Boolean)
|
||||||
|
return { enabled: _enableEqual, transforms: theTransforms }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyConstraintHorzVertDistance({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
|
||||||
|
isAlign = false,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||||
|
isAlign?: false
|
||||||
|
}): Promise<{
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
}> {
|
||||||
|
const transformInfos = horzVertDistanceInfo({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
}).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: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||||
|
} as any)
|
||||||
|
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) {
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
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: 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
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConstraintHorzVertAlign({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||||
|
}): {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
} {
|
||||||
|
const transformInfos = horzVertDistanceInfo({
|
||||||
|
selectionRanges,
|
||||||
|
constraint,
|
||||||
|
}).transforms
|
||||||
|
let finalValue = createLiteral(0)
|
||||||
|
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
||||||
|
ast: kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
modifiedAst: modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { toolTips } from '../../useStore'
|
||||||
import { create } from 'react-modal-promise'
|
import { Selections } from 'lib/selections'
|
||||||
import { toolTips, useStore } from '../../useStore'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import { Value } from '../../lang/abstractSyntaxTreeTypes'
|
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
} from '../../lang/queryAst'
|
} from '../../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
TransformInfo,
|
PathToNodeMap,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { SetAngleLengthModal } from '../SetAngleLengthModal'
|
import {
|
||||||
|
SetAngleLengthModal,
|
||||||
|
createSetAngleLengthModal,
|
||||||
|
} from '../SetAngleLengthModal'
|
||||||
import {
|
import {
|
||||||
createBinaryExpressionWithUnary,
|
createBinaryExpressionWithUnary,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -19,141 +21,126 @@ 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 { updateCursors } from '../../lang/util'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
const getModalInfo = create(SetAngleLengthModal as any)
|
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||||
|
|
||||||
type ButtonType = 'setAngle' | 'setLength'
|
export function angleLengthInfo({
|
||||||
|
selectionRanges,
|
||||||
const buttonLabels: Record<ButtonType, string> = {
|
angleOrLength = 'setLength',
|
||||||
setAngle: 'Set Angle',
|
|
||||||
setLength: 'Set Length',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SetAngleLength = ({
|
|
||||||
angleOrLength,
|
|
||||||
}: {
|
}: {
|
||||||
angleOrLength: ButtonType
|
selectionRanges: Selections
|
||||||
}) => {
|
angleOrLength?: 'setLength' | 'setAngle'
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
}) {
|
||||||
useStore((s) => ({
|
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||||
guiMode: s.guiMode,
|
getNodePathFromSourceRange(kclManager.ast, range)
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
}))
|
|
||||||
const [enableAngLen, setEnableAngLen] = 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, 'CallExpression').node
|
|
||||||
)
|
|
||||||
const isAllTooltips = nodes.every(
|
|
||||||
(node) =>
|
|
||||||
node?.type === 'CallExpression' &&
|
|
||||||
toolTips.includes(node.callee.name as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const theTransforms = getTransformInfos(selectionRanges, ast, angleOrLength)
|
|
||||||
setTransformInfos(theTransforms)
|
|
||||||
|
|
||||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
|
||||||
setEnableAngLen(_enableHorz)
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
if (guiMode.mode !== 'sketch') return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
if (!(transformInfos && ast)) return
|
|
||||||
const { valueUsedInTransform } = transformAstSketchLines({
|
|
||||||
ast: JSON.parse(JSON.stringify(ast)),
|
|
||||||
selectionRanges,
|
|
||||||
transformInfos,
|
|
||||||
programMemory,
|
|
||||||
referenceSegName: '',
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const isReferencingYAxis =
|
|
||||||
selectionRanges.otherSelections.length === 1 &&
|
|
||||||
selectionRanges.otherSelections[0] === 'y-axis'
|
|
||||||
const isReferencingYAxisAngle =
|
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
|
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(
|
||||||
|
selectionRanges,
|
||||||
|
kclManager.ast,
|
||||||
|
angleOrLength
|
||||||
|
)
|
||||||
|
const enabled =
|
||||||
|
selectionRanges.codeBasedSelections.length <= 1 &&
|
||||||
|
isAllTooltips &&
|
||||||
|
transforms.every(Boolean)
|
||||||
|
return { enabled, transforms }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyConstraintAngleLength({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength = 'setLength',
|
||||||
|
}: {
|
||||||
|
selectionRanges: Selections
|
||||||
|
angleOrLength?: 'setLength' | 'setAngle'
|
||||||
|
}): Promise<{
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
}> {
|
||||||
|
const { transforms } = angleLengthInfo({ selectionRanges, angleOrLength })
|
||||||
|
const { valueUsedInTransform } = transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const isReferencingYAxis =
|
||||||
|
selectionRanges.otherSelections.length === 1 &&
|
||||||
|
selectionRanges.otherSelections[0] === 'y-axis'
|
||||||
|
const isReferencingYAxisAngle =
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
let finalValue = removeDoubleNegatives(
|
||||||
|
valueNode as BinaryPart,
|
||||||
|
sign,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
isReferencingYAxisAngle ||
|
||||||
|
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||||
|
) {
|
||||||
|
finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue])
|
||||||
|
}
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||||
|
transformAstSketchLines({
|
||||||
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: finalValue,
|
||||||
|
})
|
||||||
|
if (variableName) {
|
||||||
|
const newBody = [..._modifiedAst.body]
|
||||||
|
newBody.splice(
|
||||||
|
newVariableInsertIndex,
|
||||||
|
0,
|
||||||
|
createVariableDeclaration(variableName, valueNode)
|
||||||
|
)
|
||||||
|
_modifiedAst.body = newBody
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('erorr', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
229
src/components/Tooltip.module.css
Normal file
229
src/components/Tooltip.module.css
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/components/Tooltip.tsx
Normal file
37
src/components/Tooltip.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// 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,6 +1,11 @@
|
|||||||
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 { BrowserRouter } from 'react-router-dom'
|
import {
|
||||||
|
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'
|
||||||
@ -93,11 +98,24 @@ 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
|
||||||
return (
|
// We have to use a memory router in the testing environment,
|
||||||
<BrowserRouter>
|
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
|
||||||
<CommandBarProvider>
|
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
||||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
const router = createMemoryRouter(
|
||||||
</CommandBarProvider>
|
createRoutesFromElements(
|
||||||
</BrowserRouter>
|
<Route
|
||||||
|
path="/file/:id"
|
||||||
|
element={
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialEntries: ['/file/new'],
|
||||||
|
initialIndex: 0,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
return <RouterProvider router={router} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,23 @@
|
|||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faBars, faBug, 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 {
|
const send = useGlobalStateContext()?.auth?.send
|
||||||
auth: { send },
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
// Fallback logic for displaying user's "name":
|
// Fallback logic for displaying user's "name":
|
||||||
// 1. user.name
|
// 1. user.name
|
||||||
@ -39,14 +38,14 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{user?.image && !imageLoadFailed ? (
|
{user?.image && !imageLoadFailed ? (
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="border-0 rounded-full w-fit min-w-max p-0 focus:outline-none group"
|
className="border-0 rounded-full w-fit min-w-max p-0 group"
|
||||||
data-testid="user-sidebar-toggle"
|
data-testid="user-sidebar-toggle"
|
||||||
>
|
>
|
||||||
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 group-focus:border-liquid-50 overflow-hidden">
|
<div className="rounded-full border overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={user?.image || ''}
|
src={user?.image || ''}
|
||||||
alt={user?.name || ''}
|
alt={user?.name || ''}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8 rounded-full"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
onError={() => setImageLoadFailed(true)}
|
onError={() => setImageLoadFailed(true)}
|
||||||
/>
|
/>
|
||||||
@ -83,11 +82,11 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
leaveTo="opacity-0 translate-x-4"
|
leaveTo="opacity-0 translate-x-4"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
|
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-30 dark:border-chalkboard-80 shadow-md rounded-l-md overflow-hidden">
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<>
|
<>
|
||||||
{user && (
|
{user && (
|
||||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
<div className="flex items-center gap-4 px-4 py-3 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
{user.image && !imageLoadFailed && (
|
{user.image && !imageLoadFailed && (
|
||||||
<div className="rounded-full shadow-inner overflow-hidden">
|
<div className="rounded-full shadow-inner overflow-hidden">
|
||||||
<img
|
<img
|
||||||
@ -101,15 +100,12 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p className="m-0 text-mono" data-testid="username">
|
||||||
className="m-0 text-liquid-10 text-mono"
|
|
||||||
data-testid="username"
|
|
||||||
>
|
|
||||||
{displayedName || ''}
|
{displayedName || ''}
|
||||||
</p>
|
</p>
|
||||||
{displayedName !== user.email && (
|
{displayedName !== user.email && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-liquid-40 text-xs"
|
className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs"
|
||||||
data-testid="email"
|
data-testid="email"
|
||||||
>
|
>
|
||||||
{user.email}
|
{user.email}
|
||||||
@ -121,39 +117,49 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
<div className="p-4 flex flex-col gap-2">
|
<div className="p-4 flex flex-col gap-2">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
icon={{ icon: faGear }}
|
icon={{ icon: 'gear' }}
|
||||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
className="border-transparent dark:border-transparent hover:bg-transparent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 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()
|
||||||
navigate(
|
const targetPath = location.pathname.includes(paths.FILE)
|
||||||
(location.pathname.endsWith('/')
|
? filePath + paths.SETTINGS
|
||||||
? location.pathname.slice(0, -1)
|
: paths.HOME + paths.SETTINGS
|
||||||
: location.pathname) + paths.SETTINGS
|
navigate(targetPath)
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="link"
|
Element="externalLink"
|
||||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||||
icon={{ icon: faGithub }}
|
icon={{ icon: faGithub, className: 'p-1', size: 'sm' }}
|
||||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
className="border-transparent dark:border-transparent"
|
||||||
>
|
>
|
||||||
Request a feature
|
Request a feature
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="externalLink"
|
||||||
|
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
||||||
|
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
|
||||||
|
className="border-transparent dark:border-transparent"
|
||||||
|
>
|
||||||
|
Report a bug
|
||||||
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send('Log out')}
|
onClick={() => send('Log out')}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faSignOutAlt,
|
icon: faSignOutAlt,
|
||||||
|
className: 'p-1',
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
|
size: 'sm',
|
||||||
iconClassName:
|
iconClassName:
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||||
}}
|
}}
|
||||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
|
||||||
|
data-testid="user-sidebar-sign-out"
|
||||||
>
|
>
|
||||||
Sign out
|
Sign out
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
63
src/components/WasmErrBanner.tsx
Normal file
63
src/components/WasmErrBanner.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
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: 'close',
|
||||||
|
className: 'p-1',
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -27,4 +27,7 @@ export const lineHighlightField = StateField.define({
|
|||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchDeco = Decoration.mark({ class: 'bg-yellow-200' })
|
const matchDeco = Decoration.mark({
|
||||||
|
class: 'bg-yellow-200',
|
||||||
|
attributes: { 'data-testid': 'hover-highlight' },
|
||||||
|
})
|
||||||
|
|||||||
@ -108,8 +108,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
messageString += message
|
messageString += message
|
||||||
// console.log(messageString)
|
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user