Compare commits
456 Commits
Author | SHA1 | Date | |
---|---|---|---|
bc6407be6e | |||
038409124a | |||
d5567f8602 | |||
df8c17ac18 | |||
9d40f282a8 | |||
a61d931826 | |||
418350ddbc | |||
d43abe20d9 | |||
84380f3da9 | |||
eea55ff2b1 | |||
10b6c1cfbc | |||
d5570e5c62 | |||
0c9589f7ee | |||
ddf66c1e0f | |||
cf1f2bd235 | |||
0b5bb5f77d | |||
0825cb5a59 | |||
4ec94a721c | |||
16dd5aab96 | |||
bf68a87897 | |||
c6e97e729a | |||
d2535bb8c2 | |||
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 | |||
ad482641ef | |||
9ee24845a1 | |||
e69d263252 | |||
111738f38e | |||
e34501cc5a | |||
c767c1c3a6 | |||
e399a8f938 | |||
59d5f2524a | |||
b47ebd14d2 | |||
e74bcd0695 | |||
22161ec386 | |||
ada46c4317 | |||
6675fa8d1e | |||
075d2debce | |||
488e41ac0e | |||
8147f5f1eb | |||
bc7e9d9789 | |||
8d493d6517 | |||
9fa98d6f3f | |||
24a31c94e7 | |||
76e3207251 | |||
e2237fa9f6 | |||
ae4aa82129 | |||
14b287a746 | |||
dd1b7631fa | |||
f98f782b40 | |||
01f5ecdc36 | |||
5297d3e142 | |||
f71f44968b | |||
7b79998c40 | |||
4632d407c1 | |||
58d7e59ca4 | |||
f592d8db84 | |||
31eca3728e | |||
c5d8779af4 | |||
cf686bdeb0 | |||
ae7143a94f | |||
f2b24849b3 | |||
35d6530406 | |||
01208221c7 | |||
fbbed3fbfb | |||
ce51f26701 | |||
caddac5059 | |||
54751aa7bb | |||
7b7d5e5f5e | |||
f7971bddef | |||
e4f2e66029 | |||
663c396128 | |||
8db86a6783 | |||
d7ad7c749e | |||
6e3c642d22 | |||
4d7433ff3a | |||
4e93146559 | |||
731a9bfbdb | |||
cdb4c36cf5 | |||
66ba60dc8e | |||
8fcc8cdd17 | |||
bba9bdc563 | |||
760a180f56 | |||
0eeff8cb45 | |||
3c76721159 | |||
6ac79ae645 | |||
90d7c33c92 | |||
e02bc76bdb | |||
0466f04d82 | |||
f8ed830b60 | |||
b7ca91bf6d | |||
2261f92b0b | |||
bbe9e621b1 | |||
bf087d760b | |||
a4353c63fd | |||
c438d11c3d | |||
43284e33c8 | |||
77dce7f0dd | |||
d559862051 | |||
7382ed87ba | |||
3324ed31de | |||
ba9dbc2205 | |||
b0028d4874 | |||
9e6be9651c | |||
b145ab0106 | |||
84e0fbb70f | |||
990605bbea | |||
d075c4ad13 | |||
a3f41f5519 | |||
cb173e2850 | |||
87cd3b67f4 | |||
fe3ee3806e | |||
c9ed6c724c | |||
a5fa259d55 | |||
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
8071eb6f8a | |||
11f789e980 | |||
3f82522fe9 | |||
c5cb0e2fd4 | |||
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c | |||
3da6fc3b7e | |||
34dd15ead7 | |||
b3d441e9d6 | |||
4b3dc3756c | |||
10027b98b5 | |||
da17dad63b | |||
fba6c422a8 | |||
0b4b93932d | |||
f42900ec46 | |||
eeca624ba6 | |||
84d08bad16 | |||
1181f33e9d | |||
797e200d08 | |||
d2f231066b |
3
.codespellrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[codespell]
|
||||||
|
ignore-words-list: crate,everytime
|
||||||
|
skip: **/target,node_modules,build
|
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
|||||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
|
||||||
VITE_KC_SENTRY_DSN=
|
VITE_KC_SENTRY_DSN=
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
|
|
||||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||||
|
16
.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
5
.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:
|
||||||
@ -24,7 +27,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
|
18
.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:
|
||||||
@ -24,7 +27,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@ -40,7 +43,18 @@ jobs:
|
|||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
- name: Install ffmpeg
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install \
|
||||||
|
ffmpeg \
|
||||||
|
libavformat-dev \
|
||||||
|
libavutil-dev \
|
||||||
|
libclang-dev \
|
||||||
|
libswscale-dev \
|
||||||
|
--no-install-recommends
|
||||||
|
|
||||||
- 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
@ -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
|
||||||
|
|
5
.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:
|
||||||
@ -27,7 +30,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib', 'src-tauri']
|
dir: ['src/wasm-lib', 'src-tauri']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
18
.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:
|
||||||
@ -26,7 +29,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@ -41,11 +44,22 @@ jobs:
|
|||||||
- uses: taiki-e/install-action@nextest
|
- uses: taiki-e/install-action@nextest
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
- name: Install ffmpeg
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install \
|
||||||
|
ffmpeg \
|
||||||
|
libavformat-dev \
|
||||||
|
libavutil-dev \
|
||||||
|
libclang-dev \
|
||||||
|
libswscale-dev \
|
||||||
|
--no-install-recommends
|
||||||
- name: cargo test
|
- name: cargo test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
cargo test --all
|
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
|
||||||
|
|
||||||
|
314
.github/workflows/ci.yml
vendored
@ -7,41 +7,79 @@ 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@v3
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- run: yarn fmt-check
|
- run: yarn fmt-check
|
||||||
|
|
||||||
|
check-types:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn install
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
|
- run: yarn build:wasm
|
||||||
|
- 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@v3
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
- run: yarn tsc
|
- run: yarn build:wasm
|
||||||
|
|
||||||
- run: yarn simpleserver:ci
|
- run: yarn simpleserver:ci
|
||||||
|
|
||||||
@ -49,111 +87,224 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn test:cov
|
- run: yarn test:cov
|
||||||
|
|
||||||
- run: yarn test:rust
|
|
||||||
|
prepare-json-files:
|
||||||
|
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.export_version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Set nightly version
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||||
|
echo "$(jq --arg url 'https://dl.zoo.dev/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
|
- id: export_version
|
||||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
build-apps:
|
build-test-apps:
|
||||||
needs: [check-format, build-test-web]
|
needs: [prepare-json-files]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-20.04, windows-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: install ubuntu system dependencies
|
- name: Copy updated .json files
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: github.event_name == 'schedule'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
ls -l artifact
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
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'
|
||||||
|
|
||||||
- name: wasm prep
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
|
- 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: macos sed
|
- name: Run vite build (build:both)
|
||||||
if: matrix.os == 'macos-latest'
|
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sed -i '' 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
|
||||||
|
|
||||||
- name: ubuntu and windows sed
|
|
||||||
if: matrix.os != 'macos-latest'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sed -i 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
|
||||||
|
|
||||||
- name: Fix format
|
- name: Fix format
|
||||||
run: yarn fmt
|
run: yarn fmt
|
||||||
|
|
||||||
- name: Build the app for the current platform (no upload)
|
- name: Install Universal target (MacOS only)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Prepare certificate and variables (Windows only)
|
||||||
|
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||||
|
cat /d/Certificate_pkcs12.p12
|
||||||
|
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
|
||||||
|
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
|
||||||
|
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
|
||||||
|
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
|
||||||
|
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
|
||||||
|
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Setup certicate with SSM KSP (Windows only)
|
||||||
|
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||||
|
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
|
||||||
|
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||||
|
smksp_registrar.exe list
|
||||||
|
smctl.exe keypair ls
|
||||||
|
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||||
|
smksp_cert_sync.exe
|
||||||
|
shell: cmd
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
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:
|
||||||
|
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: 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' }}/zoo-modeling-app"
|
||||||
|
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 }}
|
||||||
|
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||||
|
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' }}
|
||||||
|
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Generate the update static endpoint
|
- name: Generate the update static endpoint
|
||||||
run: |
|
run: |
|
||||||
ls -l artifact
|
ls -l artifact/*/*oo*
|
||||||
ls -l artifact/*
|
|
||||||
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/nsis/*.nsis.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://${WEBSITE_DIR}/${VERSION}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "v${VERSION_NO_V}" \
|
--arg version "${VERSION}" \
|
||||||
|
--arg pub_date "${PUB_DATE}" \
|
||||||
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_sig "$DARWIN_SIG" \
|
--arg darwin_sig "$DARWIN_SIG" \
|
||||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
|
||||||
--arg linux_sig "$LINUX_SIG" \
|
--arg linux_sig "$LINUX_SIG" \
|
||||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||||
--arg windows_sig "$WINDOWS_SIG" \
|
--arg windows_sig "$WINDOWS_SIG" \
|
||||||
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||||
'{
|
'{
|
||||||
"version": $version,
|
"version": $version,
|
||||||
|
"pub_date": $pub_date,
|
||||||
|
"notes": $notes,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"darwin-x86_64": {
|
"darwin-x86_64": {
|
||||||
"signature": $darwin_sig,
|
"signature": $darwin_sig,
|
||||||
"url": $darwin_url
|
"url": $darwin_url
|
||||||
},
|
},
|
||||||
|
"darwin-aarch64": {
|
||||||
|
"signature": $darwin_sig,
|
||||||
|
"url": $darwin_url
|
||||||
|
},
|
||||||
"linux-x86_64": {
|
"linux-x86_64": {
|
||||||
"signature": $linux_sig,
|
"signature": $linux_sig,
|
||||||
"url": $linux_url
|
"url": $linux_url
|
||||||
@ -166,31 +317,66 @@ jobs:
|
|||||||
}' > last_update.json
|
}' > last_update.json
|
||||||
cat last_update.json
|
cat last_update.json
|
||||||
|
|
||||||
|
- name: Generate the download static endpoint
|
||||||
|
run: |
|
||||||
|
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||||
|
jq --null-input \
|
||||||
|
--arg version "${VERSION}" \
|
||||||
|
--arg pub_date "${PUB_DATE}" \
|
||||||
|
--arg notes "${NOTES}" \
|
||||||
|
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
|
||||||
|
--arg linux_url "$RELEASE_DIR/appimage/zoo-modeling-app_${VERSION_NO_V}_amd64.AppImage" \
|
||||||
|
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
|
||||||
|
'{
|
||||||
|
"version": $version,
|
||||||
|
"pub_date": $pub_date,
|
||||||
|
"notes": $notes,
|
||||||
|
"platforms": {
|
||||||
|
"dmg-universal": {
|
||||||
|
"url": $darwin_url
|
||||||
|
},
|
||||||
|
"appimage-x86_64": {
|
||||||
|
"url": $linux_url
|
||||||
|
},
|
||||||
|
"msi-x86_64": {
|
||||||
|
"url": $windows_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > 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 }}'
|
||||||
|
|
||||||
- name: Set up Google Cloud SDK
|
- name: Set up Google Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@v1.1.1
|
uses: google-github-actions/setup-gcloud@v2.0.0
|
||||||
with:
|
with:
|
||||||
project_id: kittycadapi
|
project_id: kittycadapi
|
||||||
|
|
||||||
- name: Upload release files to public bucket
|
- name: Upload release files to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||||
with:
|
with:
|
||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/*'
|
glob: '*/*oo*'
|
||||||
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@v2.0.0
|
||||||
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
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||||
|
with:
|
||||||
|
path: last_download.json
|
||||||
|
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/*/*
|
files: artifact/*/*oo*
|
||||||
|
116
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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'
|
||||||
|
- uses: KittyCAD/action-install-cli@v0.2.16
|
||||||
|
- 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 }}
|
||||||
|
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||||
|
- 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
|
2
.github/workflows/update-dev-branch.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.5.0
|
- uses: actions/checkout@v4
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
# checkout our branch
|
# checkout our branch
|
||||||
|
22
.gitignore
vendored
@ -22,9 +22,31 @@ 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
|
||||||
|
# exports from snapshot-tests.spec.ts
|
||||||
|
e2e/playwright/export-snapshots/*.ply
|
||||||
|
e2e/playwright/export-snapshots/*.obj
|
||||||
|
e2e/playwright/export-snapshots/*.step
|
||||||
|
e2e/playwright/export-snapshots/*.stl
|
||||||
|
e2e/playwright/export-snapshots/*binary.gltf
|
||||||
|
e2e/playwright/export-snapshots/*embedded.gltf
|
||||||
|
|
||||||
|
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
@ -5,3 +5,9 @@ coverage
|
|||||||
# Ignore Rust projects:
|
# Ignore Rust projects:
|
||||||
*.rs
|
*.rs
|
||||||
target
|
target
|
||||||
|
src/wasm-lib/pkg
|
||||||
|
src/wasm-lib/kcl/bindings
|
||||||
|
e2e/playwright/export-snapshots
|
||||||
|
|
||||||
|
# XState generated files
|
||||||
|
src/machines/modelingMachine.typegen.ts
|
||||||
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2023 The Zoo 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.
|
261
README.md
@ -1,48 +1,73 @@
|
|||||||
## Kurt demo project
|

|
||||||
|
|
||||||
live at [app.kittycad.io](https://app.kittycad.io/)
|
## Zoo Modeling App
|
||||||
|
|
||||||
Not sure what to call this, it's both a language/interpreter and a UI that uses the language as the source of truth model the user build with direct-manipulation with the UI.
|
live at [app.zoo.dev](https://app.zoo.dev/)
|
||||||
|
|
||||||
It might make sense to split this repo up at some point, but not the lang and the UI are all togther in a react app
|
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
|
||||||
|
|
||||||
Originally Presented on 10/01/2023
|
Modeling App is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
|
||||||
|
|
||||||
[Video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
|
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
|
||||||
|
- This makes version control—which is a solved problem in software engineering—trivial for CAD
|
||||||
|
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
|
||||||
|
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in Modeling App
|
||||||
|
- Everything graphics _has_ to be built for the GPU
|
||||||
|
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
|
||||||
|
- Make the resource-intensive pieces of an application auto-scaling
|
||||||
|
- One of the bottlenecks of today's hardware design tools is that they all rely on the local machine's resources to do the hardest parts, which include geometry rendering and analysis. Our geometry engine parallelizes rendering and just sends video frames back to the app (seriously, inspect source, it's just a `<video>` element), and our API will offload analysis as we build it in
|
||||||
|
|
||||||
[demo-slides.pdf](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
|
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
|
||||||
|
|
||||||
## To run, there are a couple steps since we're compiling rust to WASM, you'll need to have rust stuff installed, then
|
Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
|
||||||
|
|
||||||
|
The 3D view in Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- UI
|
||||||
|
- [React](https://react.dev/)
|
||||||
|
- [Headless UI](https://headlessui.com/)
|
||||||
|
- [TailwindCSS](https://tailwindcss.com/)
|
||||||
|
- [XState](https://xstate.js.org/)
|
||||||
|
- Networking
|
||||||
|
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
|
||||||
|
- Code Editor
|
||||||
|
- [CodeMirror](https://codemirror.net/)
|
||||||
|
- Custom WASM LSP Server
|
||||||
|
- Modeling
|
||||||
|
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
|
||||||
|
|
||||||
|
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
|
||||||
|
|
||||||
|
[Original demo slides](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
|
||||||
|
|
||||||
|
## 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. We always use the latest stable version of Rust, so you may need to run `rustup update stable`. Then, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
then
|
|
||||||
|
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)
|
||||||
|
|
||||||
finally
|
finally, to run the web app only, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
|
|
||||||
```
|
|
||||||
yarn simpleserver
|
|
||||||
```
|
|
||||||
in one terminal
|
|
||||||
and
|
|
||||||
```
|
|
||||||
yarn test
|
|
||||||
```
|
|
||||||
in another.
|
|
||||||
|
|
||||||
If you want to edit the rust files, you can cd into `src/wasm-lib` and then use the usual rust commands, `cargo build`, `cargo test`, when you want to bring the changes back to the web-app, a fresh `yarn build:wasm` in the root will be needed.
|
|
||||||
|
|
||||||
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
|
|
||||||
|
|
||||||
## Developing in Chrome
|
## Developing in Chrome
|
||||||
|
|
||||||
Chrome is in the process of rolling out a new default which
|
Chrome is in the process of rolling out a new default which
|
||||||
@ -52,13 +77,34 @@ enable third-party cookies. You can enable third-party cookies by clicking on
|
|||||||
the eye with a slash through it in the URL bar, and clicking on "Enable
|
the eye with a slash through it in the URL bar, and clicking on "Enable
|
||||||
Third-Party Cookies".
|
Third-Party Cookies".
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
First, start the dev server following "Running a development build" above.
|
||||||
|
|
||||||
|
Then in another terminal tab, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.)
|
||||||
|
|
||||||
@ -67,22 +113,175 @@ To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access t
|
|||||||
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
|
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
|
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
|
||||||
|
|
||||||
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
|
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
|
||||||
|
|
||||||
|
## Before submitting a PR
|
||||||
|
|
||||||
|
Before you submit a contribution PR to this repo, please ensure that:
|
||||||
|
|
||||||
|
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
|
||||||
|
- You have separated out refactoring commits from feature commits as much as possible
|
||||||
|
- You have run all of the following commands locally:
|
||||||
|
- `yarn fmt`
|
||||||
|
- `yarn tsc`
|
||||||
|
- `yarn test`
|
||||||
|
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
|
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
|
||||||
|
|
||||||
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
|
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
|
||||||
|
|
||||||
|
## Fuzzing the parser
|
||||||
|
|
||||||
|
Make sure you install cargo fuzz:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo install cargo-fuzz
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd src/wasm-lib/kcl
|
||||||
|
|
||||||
|
# list the fuzz targets
|
||||||
|
$ cargo fuzz list
|
||||||
|
|
||||||
|
# run the parser fuzzer
|
||||||
|
$ cargo +nightly fuzz run parser
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on fuzzing you can check out
|
||||||
|
[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"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets2.env
|
||||||
|
```
|
||||||
|
then replace "your-token" with a dev token from dev.zoo.dev/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>
|
||||||
|
BIN
app-icon.png
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 120 KiB |
75
docs/kcl/types.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Types
|
||||||
|
|
||||||
|
`KCL` defines the following types and keywords the language.
|
||||||
|
|
||||||
|
All these types can be nested in various forms where nesting applies. Like
|
||||||
|
arrays can hold objects and vice versa.
|
||||||
|
|
||||||
|
## Boolean
|
||||||
|
|
||||||
|
`true` or `false` work when defining values.
|
||||||
|
|
||||||
|
## Variable declaration
|
||||||
|
|
||||||
|
Variables are defined with the `let` keyword like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
let myBool = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Array
|
||||||
|
|
||||||
|
An array is defined with `[]` braces. What is inside the brackets can
|
||||||
|
be of any type. For example, the following is completely valid:
|
||||||
|
|
||||||
|
```
|
||||||
|
let myArray = ["thing", 2, false]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to get a value from an array you can use the index like so:
|
||||||
|
`myArray[0]`.
|
||||||
|
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
An object is defined with `{}` braces. Here is an example object:
|
||||||
|
|
||||||
|
```
|
||||||
|
let myObj = {a: 0, b: "thing"}
|
||||||
|
```
|
||||||
|
|
||||||
|
We support two different ways of getting properties from objects, you can call
|
||||||
|
`myObj.a` or `myObj["a"]` both work.
|
||||||
|
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
We also have support for defining your own functions. Functions can take in any
|
||||||
|
type of argument. Below is an example of the syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
fn myFn = (x) => {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see above `myFn` just returns whatever it is given.
|
||||||
|
|
||||||
|
|
||||||
|
## Binary expressions
|
||||||
|
|
||||||
|
You can also do math! Let's show an example below:
|
||||||
|
|
||||||
|
```
|
||||||
|
let myMathExpression = 3 + 1 * 2 / 3 - 7
|
||||||
|
```
|
||||||
|
|
||||||
|
You can nest expressions in parenthesis as well:
|
||||||
|
|
||||||
|
```
|
||||||
|
let myMathExpression = 3 + (1 * 2 / (3 - 7))
|
||||||
|
```
|
||||||
|
|
||||||
|
Please if you find any issues using any of the above expressions or syntax
|
||||||
|
please file an issue with the `ast` label on the [modeling-app
|
||||||
|
repo](https://github.com/KittyCAD/modeling-app/issues/new).
|
BIN
e2e/playwright/export-snapshots/gltf-binary.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
e2e/playwright/export-snapshots/gltf-embedded.png
Normal file
After Width: | Height: | Size: 80 KiB |
2976
e2e/playwright/export-snapshots/gltf-standard-2.gltf
Normal file
BIN
e2e/playwright/export-snapshots/gltf-standard.gltf
Normal file
BIN
e2e/playwright/export-snapshots/obj-.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/ply-ascii.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/ply-binary_big_endian.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/ply-binary_little_endian.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
e2e/playwright/export-snapshots/step-.png
Normal file
After Width: | Height: | Size: 80 KiB |
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, 1);
|
||||||
|
#62 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#63 = LINE('NONE', #62, #61);
|
||||||
|
#64 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#65 = VECTOR('NONE', #64, 1);
|
||||||
|
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
||||||
|
#67 = LINE('NONE', #66, #65);
|
||||||
|
#68 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#69 = VECTOR('NONE', #68, 1);
|
||||||
|
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
||||||
|
#71 = LINE('NONE', #70, #69);
|
||||||
|
#72 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#73 = VECTOR('NONE', #72, 1);
|
||||||
|
#74 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
|
#75 = LINE('NONE', #74, #73);
|
||||||
|
#76 = DIRECTION('NONE', (1, 0, 0));
|
||||||
|
#77 = VECTOR('NONE', #76, 1);
|
||||||
|
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
||||||
|
#79 = LINE('NONE', #78, #77);
|
||||||
|
#80 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#81 = VECTOR('NONE', #80, 1);
|
||||||
|
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
||||||
|
#83 = LINE('NONE', #82, #81);
|
||||||
|
#84 = DIRECTION('NONE', (1, 0, 0));
|
||||||
|
#85 = VECTOR('NONE', #84, 1);
|
||||||
|
#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, 1);
|
||||||
|
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
||||||
|
#91 = LINE('NONE', #90, #89);
|
||||||
|
#92 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#93 = VECTOR('NONE', #92, 1);
|
||||||
|
#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, 1);
|
||||||
|
#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, 1);
|
||||||
|
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
||||||
|
#103 = LINE('NONE', #102, #101);
|
||||||
|
#104 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#105 = VECTOR('NONE', #104, 1);
|
||||||
|
#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, 1);
|
||||||
|
#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, 1);
|
||||||
|
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
||||||
|
#115 = LINE('NONE', #114, #113);
|
||||||
|
#116 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#117 = VECTOR('NONE', #116, 1);
|
||||||
|
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
||||||
|
#119 = LINE('NONE', #118, #117);
|
||||||
|
#120 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#121 = VECTOR('NONE', #120, 1);
|
||||||
|
#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, 1);
|
||||||
|
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
||||||
|
#127 = LINE('NONE', #126, #125);
|
||||||
|
#128 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#129 = VECTOR('NONE', #128, 1);
|
||||||
|
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
||||||
|
#131 = LINE('NONE', #130, #129);
|
||||||
|
#132 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#133 = VECTOR('NONE', #132, 1);
|
||||||
|
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
|
||||||
|
#135 = LINE('NONE', #134, #133);
|
||||||
|
#136 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
||||||
|
#137 = VECTOR('NONE', #136, 1);
|
||||||
|
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
||||||
|
#139 = LINE('NONE', #138, #137);
|
||||||
|
#140 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#141 = VECTOR('NONE', #140, 1);
|
||||||
|
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
||||||
|
#143 = LINE('NONE', #142, #141);
|
||||||
|
#144 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
||||||
|
#145 = VECTOR('NONE', #144, 1);
|
||||||
|
#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, 1);
|
||||||
|
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
||||||
|
#151 = LINE('NONE', #150, #149);
|
||||||
|
#152 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#153 = VECTOR('NONE', #152, 1);
|
||||||
|
#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, 1);
|
||||||
|
#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, 1);
|
||||||
|
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
||||||
|
#163 = LINE('NONE', #162, #161);
|
||||||
|
#164 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#165 = VECTOR('NONE', #164, 1);
|
||||||
|
#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, 1);
|
||||||
|
#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, 1);
|
||||||
|
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
||||||
|
#175 = LINE('NONE', #174, #173);
|
||||||
|
#176 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#177 = VECTOR('NONE', #176, 1);
|
||||||
|
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
||||||
|
#179 = LINE('NONE', #178, #177);
|
||||||
|
#180 = DIRECTION('NONE', (0, 1, 0));
|
||||||
|
#181 = VECTOR('NONE', #180, 1);
|
||||||
|
#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, 1);
|
||||||
|
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
||||||
|
#187 = LINE('NONE', #186, #185);
|
||||||
|
#188 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#189 = VECTOR('NONE', #188, 1);
|
||||||
|
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
||||||
|
#191 = LINE('NONE', #190, #189);
|
||||||
|
#192 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#193 = VECTOR('NONE', #192, 1);
|
||||||
|
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
|
||||||
|
#195 = LINE('NONE', #194, #193);
|
||||||
|
#196 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
|
||||||
|
#197 = VECTOR('NONE', #196, 1);
|
||||||
|
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
||||||
|
#199 = LINE('NONE', #198, #197);
|
||||||
|
#200 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#201 = VECTOR('NONE', #200, 1);
|
||||||
|
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
||||||
|
#203 = LINE('NONE', #202, #201);
|
||||||
|
#204 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
|
||||||
|
#205 = VECTOR('NONE', #204, 1);
|
||||||
|
#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, 1);
|
||||||
|
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
||||||
|
#211 = LINE('NONE', #210, #209);
|
||||||
|
#212 = DIRECTION('NONE', (0, 0, 1));
|
||||||
|
#213 = VECTOR('NONE', #212, 1);
|
||||||
|
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
||||||
|
#215 = LINE('NONE', #214, #213);
|
||||||
|
#216 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
|
#217 = VECTOR('NONE', #216, 1);
|
||||||
|
#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, 1);
|
||||||
|
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
||||||
|
#223 = LINE('NONE', #222, #221);
|
||||||
|
#224 = DIRECTION('NONE', (0, -1, 0));
|
||||||
|
#225 = VECTOR('NONE', #224, 1);
|
||||||
|
#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;
|
BIN
e2e/playwright/export-snapshots/stl-ascii.png
Normal file
After Width: | Height: | Size: 57 KiB |
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.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
e2e/playwright/export-snapshots/stl-binary.stl
Normal file
737
e2e/playwright/flow-tests.spec.ts
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
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
|
||||||
|
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Ctrl+/' })
|
||||||
|
.or(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('Select an option')
|
||||||
|
await expect(themeInput).toBeVisible()
|
||||||
|
await expect(themeInput).toBeFocused()
|
||||||
|
// Select dark theme
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can extrude from the command bar', async ({ page, context }) => {
|
||||||
|
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], %)
|
||||||
|
|> close(%)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
await page.keyboard.press('Meta+K')
|
||||||
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
|
|
||||||
|
// Search for extrude command and choose it
|
||||||
|
await page.getByRole('option', { name: 'Extrude' }).click()
|
||||||
|
await expect(page.locator('#arg-form > label')).toContainText(
|
||||||
|
'Please select one face'
|
||||||
|
)
|
||||||
|
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
||||||
|
|
||||||
|
// Click to select face and set distance
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||||
|
await u.waitForCmdReceive('select_add')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Review step and argument hotkeys
|
||||||
|
await page.keyboard.press('2')
|
||||||
|
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check that the code was updated
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|
|> line([25.1, 0.41], %)
|
||||||
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(5, %)`
|
||||||
|
)
|
||||||
|
})
|
21
e2e/playwright/secrets.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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 || ''
|
||||||
|
secrets.snapshottoken = process.env.snapshottoken || ''
|
||||||
|
// add more env vars here to make them available in CI
|
||||||
|
}
|
||||||
|
|
||||||
|
export { secrets }
|
388
e2e/playwright/snapshot-tests.spec.ts
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
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'
|
||||||
|
import { spawn } from 'child_process'
|
||||||
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
|
||||||
|
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
|
||||||
|
// And you will need to have the KittyCAD CLI installed
|
||||||
|
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: APP_NAME }).click()
|
||||||
|
|
||||||
|
interface Paths {
|
||||||
|
modelPath: string
|
||||||
|
imagePath: string
|
||||||
|
outputType: string
|
||||||
|
}
|
||||||
|
const doExport = async (
|
||||||
|
output: Models['OutputFormat_type']
|
||||||
|
): Promise<Paths> => {
|
||||||
|
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 = '', isImage = false) =>
|
||||||
|
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||||
|
'storage' in output ? output.storage : ''
|
||||||
|
}${extra}.${isImage ? 'png' : 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)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modelPath: downloadLocation,
|
||||||
|
imagePath: downloadLocationer('', true),
|
||||||
|
outputType: output.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||||
|
axis: 'z',
|
||||||
|
direction: 'positive',
|
||||||
|
}
|
||||||
|
const sysType: Models['System_type'] = {
|
||||||
|
forward: axisDirectionPair,
|
||||||
|
up: axisDirectionPair,
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportLocations: Paths[] = []
|
||||||
|
|
||||||
|
// 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
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'step',
|
||||||
|
coords: sysType,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
storage: 'ascii',
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
storage: 'binary_little_endian',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'ply',
|
||||||
|
storage: 'binary_big_endian',
|
||||||
|
coords: sysType,
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'stl',
|
||||||
|
storage: 'ascii',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'stl',
|
||||||
|
storage: 'binary',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
selection: { type: 'default_scene' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
// obj seems to be a little flaky, times out tests sometimes
|
||||||
|
type: 'obj',
|
||||||
|
coords: sysType,
|
||||||
|
units: 'in',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'embedded',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
exportLocations.push(
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'binary',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: gltfs don't seem to work with snap shots. push onto exportLocations once it's figured out
|
||||||
|
await doExport({
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'standard',
|
||||||
|
presentation: 'pretty',
|
||||||
|
})
|
||||||
|
|
||||||
|
// close page to disconnect websocket since we can only have one open atm
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||||
|
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||||
|
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||||
|
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||||
|
const child = spawn(cliCommand, { shell: true })
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.on('error', (code: any, msg: any) => {
|
||||||
|
console.log('error', code, msg)
|
||||||
|
reject()
|
||||||
|
})
|
||||||
|
child.on('exit', (code, msg) => {
|
||||||
|
console.log('exit', code, msg)
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(`exit code ${code} for model ${modelPath}`)
|
||||||
|
} else {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||||
|
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 54 KiB |
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
@ -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')
|
||||||
|
})
|
||||||
|
})
|
11
index.html
@ -7,12 +7,17 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="An open-source CAD modeling tool from the future by KittyCAD."
|
content="An open-source CAD modeling tool from the future by Zoo."
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
|
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
|
||||||
<title>KittyCAD Modeling App</title>
|
<script
|
||||||
|
defer
|
||||||
|
data-domain="app.kittycad.io"
|
||||||
|
src="https://plausible.corp.kittycad.io/js/script.js"
|
||||||
|
></script>
|
||||||
|
<title>Zoo Modeling App</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="body-bg">
|
<body class="body-bg">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
103
package.json
@ -1,36 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.3.1",
|
"version": "0.14.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.35",
|
"@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",
|
||||||
"@sentry/react": "^7.65.0",
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@sentry/react": "^7.77.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",
|
||||||
@ -40,37 +43,43 @@
|
|||||||
"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": "vite",
|
"start": "vite",
|
||||||
|
"start:prod": "vite preview --port=3000",
|
||||||
|
"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",
|
||||||
"build:both:local": "yarn build:wasm && vite build",
|
"build:both:local": "yarn build:wasm && vite build",
|
||||||
|
"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",
|
||||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||||
"simpleserver": "http-server ./public --cors -p 3000",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"fmt": "prettier --write ./src",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt": "prettier --write ./src && prettier --write ./e2e",
|
||||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
|
||||||
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
"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-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\"",
|
||||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
||||||
@ -95,30 +104,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
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
42
public/expectations.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## Alpha Users Expectations
|
||||||
|
|
||||||
|
### Welcome
|
||||||
|
|
||||||
|
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
|
||||||
|
|
||||||
|
### Zoo Modeling App (ZMA)
|
||||||
|
|
||||||
|
What we are introducing to you is our Zoo Modeling App (ZMA). ZMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. ZMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
|
||||||
|
|
||||||
|
### Why Code?
|
||||||
|
|
||||||
|
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isn’t as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
|
||||||
|
|
||||||
|
- Automation and parametric design
|
||||||
|
- Customization and flexibility
|
||||||
|
- Algorithmic and generative design
|
||||||
|
- Reproducibility
|
||||||
|
- Easier integration with other tools
|
||||||
|
|
||||||
|
### Before You Use ZMA
|
||||||
|
|
||||||
|
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
||||||
|
|
||||||
|
- ZMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. ZMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
|
||||||
|
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
|
||||||
|
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure it’s not a duplicate or it’s feasible for the current state of the app.
|
||||||
|
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
|
||||||
|
- **High:** The bug is blocking you from continuing.
|
||||||
|
- Example: Every time I click the extrude button with two faces selected, the app crashes.
|
||||||
|
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
|
||||||
|
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
|
||||||
|
- **Low:** The bug is annoying but doesn’t affect workflow or block you from continuing (usually you can say “It would be nice if ___, but it’s not needed”)
|
||||||
|
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
|
||||||
|
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
|
||||||
|
|
||||||
|
### Thank You!
|
||||||
|
|
||||||
|
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!
|
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 |
46
public/kcma-logomark-dark.svg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/kcma-logomark-outlined.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/kcma-logomark.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
46
public/kcma-logomark.svg
Normal file
After Width: | Height: | Size: 16 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "KCMA",
|
"short_name": "ZMA",
|
||||||
"name": "KittyCAD Modeling App",
|
"name": "Zoo Modeling App",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
BIN
public/onboarding-bracket-dark.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
public/onboarding-bracket.png
Normal file
After Width: | Height: | Size: 142 KiB |
26
public/roadmap.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
## Zoo Modeling App Roadmap
|
||||||
|
|
||||||
|
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.
|
||||||
|
|
||||||
|
### Current Priority List
|
||||||
|
|
||||||
|
1. [Sketch on Face](https://github.com/KittyCAD/modeling-app/discussions/477)
|
||||||
|
2. [Revolve](https://github.com/KittyCAD/modeling-app/discussions/496)
|
||||||
|
3. [Fillet](https://github.com/KittyCAD/modeling-app/discussions/501)
|
||||||
|
4. [Linear Pattern](https://github.com/KittyCAD/modeling-app/discussions/256)
|
||||||
|
5. [Circular Pattern](https://github.com/KittyCAD/modeling-app/discussions/257)
|
||||||
|
6. [Mirror-Sketch](https://github.com/KittyCAD/modeling-app/discussions/507)
|
||||||
|
7. [Chamfer](https://github.com/KittyCAD/modeling-app/discussions/502)
|
||||||
|
8. [Sweep](https://github.com/KittyCAD/modeling-app/discussions/498)
|
||||||
|
9. [Draft](https://github.com/KittyCAD/modeling-app/discussions/495)
|
||||||
|
10. [Shell](https://github.com/KittyCAD/modeling-app/discussions/503)
|
||||||
|
11. [Union](https://github.com/KittyCAD/modeling-app/discussions/509)
|
||||||
|
12. [Mirror-Model](https://github.com/KittyCAD/modeling-app/discussions/508)
|
||||||
|
13. [Subtract](https://github.com/KittyCAD/modeling-app/discussions/510)
|
||||||
|
14. [Intersect](https://github.com/KittyCAD/modeling-app/discussions/511)
|
||||||
|
15. [Offset](https://github.com/KittyCAD/modeling-app/discussions/512)
|
||||||
|
16. [Thicken](https://github.com/KittyCAD/modeling-app/discussions/499)
|
||||||
|
17. [Import](https://github.com/KittyCAD/modeling-app/discussions/478)
|
||||||
|
18. [Assemblies](https://github.com/KittyCAD/modeling-app/discussions/494)
|
||||||
|
19. [External Thread](https://github.com/KittyCAD/modeling-app/discussions/505)
|
||||||
|
|
13
public/zma-logomark-dark.svg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/zma-logomark-outlined.png
Normal file
After Width: | Height: | Size: 11 KiB |
13
public/zma-logomark.svg
Normal file
After Width: | Height: | Size: 13 KiB |
7
public/zoo-logo.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="438" height="145" viewBox="0 0 438 145" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M88.2136 25.3021V3.12744H0.595269V34.3994H79.827L0.609484 120.312H0.595269V120.326L0.581055 120.34L0.595269 120.355V141.364H20.8936L41.3341 119.189V141.364H128.952V110.092H49.7349L128.952 24.1649V3.12744L108.64 3.15587L88.2136 25.3021Z" fill="white"/>
|
||||||
|
<path d="M167.36 72.4372C167.36 49.7366 185.824 31.2719 208.525 31.2719C216.514 31.2719 223.976 33.5605 230.288 37.5121L251.78 14.3709C239.698 5.34466 224.73 0 208.525 0C168.582 0 136.088 32.4944 136.088 72.4372C136.088 90.5465 142.769 107.135 153.828 119.857L175.32 96.7156C170.316 89.9069 167.36 81.5061 167.36 72.4372Z" fill="white"/>
|
||||||
|
<path d="M241.745 48.1442C246.734 54.9671 249.691 63.3679 249.691 72.4368C249.691 95.1232 231.226 113.588 208.525 113.588C200.537 113.588 193.088 111.299 186.777 107.348L165.271 130.503C177.353 139.515 192.321 144.86 208.525 144.86C248.468 144.86 280.963 112.365 280.963 72.4368C280.963 54.3133 274.282 37.7249 263.223 25.0029L241.745 48.1442Z" fill="white"/>
|
||||||
|
<path d="M419.312 25.0029L397.834 48.1442C402.823 54.9671 405.779 63.3679 405.779 72.4368C405.779 95.1232 387.315 113.588 364.614 113.588C356.626 113.588 349.177 111.299 342.866 107.348L321.359 130.503C333.442 139.515 348.41 144.86 364.614 144.86C404.557 144.86 437.051 112.365 437.051 72.4368C437.051 54.3133 430.371 37.7249 419.312 25.0029Z" fill="white"/>
|
||||||
|
<path d="M323.449 72.4372C323.449 49.7366 341.913 31.2719 364.614 31.2719C372.603 31.2719 380.065 33.5605 386.376 37.5121L407.869 14.3709C395.786 5.34466 380.819 0 364.614 0C324.671 0 292.177 32.4944 292.177 72.4372C292.177 90.5465 298.858 107.135 309.916 119.857L331.409 96.7156C326.405 89.9069 323.449 81.5061 323.449 72.4372Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
1283
src-tauri/Cargo.lock
generated
@ -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,17 +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"
|
||||||
oauth2 = "4.4.1"
|
kittycad = "0.2.42"
|
||||||
|
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.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
tauri = { version = "1.5.3", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
||||||
tokio = { version = "1.29.1", features = ["time"] }
|
|
||||||
toml = "0.6.0"
|
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
tokio = { version = "1.34.0", features = ["time"] }
|
||||||
|
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.
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 69 KiB |
@ -1,11 +1,14 @@
|
|||||||
// 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;
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use tauri::{InvokeError, Manager};
|
use tauri::{InvokeError, Manager};
|
||||||
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
/// This command returns the a json string parse from a toml file at the path.
|
/// This command returns the a json string parse from a toml file at the path.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -67,10 +70,23 @@ 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.
|
||||||
|
// TODO: find a better way to share this value with tauri e2e tests
|
||||||
|
// 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())
|
||||||
|
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||||
|
} else {
|
||||||
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.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
|
||||||
@ -85,19 +101,65 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///This command returns the KittyCAD user info given a token.
|
||||||
|
/// The string returned from this method is the user info as a json string.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_user(
|
||||||
|
token: Option<String>,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Result<kittycad::types::User, InvokeError> {
|
||||||
|
// Use the host passed in if it's set.
|
||||||
|
// Otherwise, use the default host.
|
||||||
|
let host = if hostname.is_empty() {
|
||||||
|
DEFAULT_HOST.to_string()
|
||||||
|
} else {
|
||||||
|
hostname.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change the baseURL to the one we want.
|
||||||
|
let mut baseurl = host.to_string();
|
||||||
|
if !host.starts_with("http://") && !host.starts_with("https://") {
|
||||||
|
baseurl = format!("https://{host}");
|
||||||
|
if host.starts_with("localhost") {
|
||||||
|
baseurl = format!("http://{host}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Getting user info...");
|
||||||
|
|
||||||
|
// use kittycad library to fetch the user info from /user/me
|
||||||
|
let mut client = kittycad::Client::new(token.unwrap());
|
||||||
|
|
||||||
|
if baseurl != DEFAULT_HOST {
|
||||||
|
client.set_base_url(&baseurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_info: kittycad::types::User = client
|
||||||
|
.users()
|
||||||
|
.get_self()
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
|
||||||
|
Ok(user_info)
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_user,
|
||||||
|
login,
|
||||||
|
read_toml,
|
||||||
|
read_txt_file
|
||||||
|
])
|
||||||
.plugin(tauri_plugin_fs_extra::init())
|
.plugin(tauri_plugin_fs_extra::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -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": "zoo-modeling-app",
|
||||||
"version": "0.3.1"
|
"version": "0.14.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -36,6 +35,9 @@
|
|||||||
"https://api.dev.kittycad.io/*"
|
"https://api.dev.kittycad.io/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"os": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
},
|
},
|
||||||
@ -69,30 +71,20 @@
|
|||||||
},
|
},
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all",
|
"targets": "all"
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": null,
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"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": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 1200,
|
"height": 1200,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "KittyCAD Modeling",
|
"title": "Zoo Modeling App",
|
||||||
"width": 1800
|
"width": 1800
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling"
|
"productName": "Zoo Modeling App"
|
||||||
}
|
}
|
||||||
}
|
}
|
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.zoo.dev/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,7 +1,6 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling"
|
"productName": "Zoo Modeling App"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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/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
|
||||||
|
const router = createMemoryRouter(
|
||||||
|
createRoutesFromElements(
|
||||||
|
<Route
|
||||||
|
path="/file/:id"
|
||||||
|
element={
|
||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
<GlobalStateProvider>
|
||||||
|
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
||||||
|
</GlobalStateProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
}
|
||||||
)
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialEntries: ['/file/new'],
|
||||||
|
initialIndex: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return <RouterProvider router={router} />
|
||||||
}
|
}
|
||||||
|
516
src/App.tsx
@ -1,39 +1,16 @@
|
|||||||
import {
|
import { useCallback, MouseEventHandler } from 'react'
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
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 { asyncParser } from './lang/abstractSyntaxTree'
|
import { PaneType, useStore } from './useStore'
|
||||||
import { _executor } from './lang/executor'
|
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
|
||||||
import { ViewUpdate, EditorView } from '@codemirror/view'
|
|
||||||
import {
|
|
||||||
lineHighlightField,
|
|
||||||
addLineHighlight,
|
|
||||||
} from './editor/highlightextension'
|
|
||||||
import { PaneType, Selections, useStore } from './useStore'
|
|
||||||
import Server from './editor/lsp/server'
|
|
||||||
import Client from './editor/lsp/client'
|
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
import { MemoryPanel } from './components/MemoryPanel'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
import { Stream } from './components/Stream'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
import { FromServer, IntoServer } from './editor/lsp/codec'
|
import { EngineCommand } from './lang/std/engineConnection'
|
||||||
import {
|
import { throttle } from './lib/utils'
|
||||||
EngineCommand,
|
|
||||||
EngineCommandManager,
|
|
||||||
} from './lang/std/engineConnection'
|
|
||||||
import { isOverlap, throttle } from './lib/utils'
|
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
|
||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import {
|
import {
|
||||||
faCode,
|
faCode,
|
||||||
@ -41,106 +18,42 @@ import {
|
|||||||
faSquareRootVariable,
|
faSquareRootVariable,
|
||||||
} 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 { TEST } from './env'
|
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { Themes, getSystemTheme } from './lib/theme'
|
import { useLoaderData } from 'react-router-dom'
|
||||||
import { isTauri } from './lib/isTauri'
|
|
||||||
import { useLoaderData, useParams } from 'react-router-dom'
|
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
|
||||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding'
|
import { onboardingPaths } from 'routes/Onboarding'
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import kclLanguage from 'editor/lsp/language'
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import { CodeMenu } from 'components/CodeMenu'
|
||||||
|
import { TextEditor } from 'components/TextEditor'
|
||||||
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
|
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 pathParams = useParams()
|
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
editorView,
|
buttonDownInStream,
|
||||||
setEditorView,
|
|
||||||
setSelectionRanges,
|
|
||||||
selectionRanges,
|
|
||||||
addLog,
|
|
||||||
addKCLError,
|
|
||||||
code,
|
|
||||||
setCode,
|
|
||||||
setAst,
|
|
||||||
setError,
|
|
||||||
setProgramMemory,
|
|
||||||
resetLogs,
|
|
||||||
resetKCLErrors,
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
setArtifactMap,
|
|
||||||
engineCommandManager,
|
|
||||||
setEngineCommandManager,
|
|
||||||
setHighlightRange,
|
|
||||||
setCursor2,
|
|
||||||
sourceRangeMap,
|
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
isStreamReady,
|
|
||||||
isLSPServerReady,
|
|
||||||
setIsLSPServerReady,
|
|
||||||
isMouseDownInStream,
|
|
||||||
formatCode,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
|
||||||
setStreamDimensions,
|
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
setEditorView: s.setEditorView,
|
|
||||||
setSelectionRanges: s.setSelectionRanges,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
setGuiMode: s.setGuiMode,
|
|
||||||
addLog: s.addLog,
|
|
||||||
code: s.code,
|
|
||||||
setCode: s.setCode,
|
|
||||||
setAst: s.setAst,
|
|
||||||
setError: s.setError,
|
|
||||||
setProgramMemory: s.setProgramMemory,
|
|
||||||
resetLogs: s.resetLogs,
|
|
||||||
resetKCLErrors: s.resetKCLErrors,
|
|
||||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
|
||||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
|
||||||
engineCommandManager: s.engineCommandManager,
|
|
||||||
setEngineCommandManager: s.setEngineCommandManager,
|
|
||||||
setHighlightRange: s.setHighlightRange,
|
|
||||||
isShiftDown: s.isShiftDown,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
setCursor2: s.setCursor2,
|
|
||||||
sourceRangeMap: s.sourceRangeMap,
|
|
||||||
setMediaStream: s.setMediaStream,
|
|
||||||
isStreamReady: s.isStreamReady,
|
|
||||||
setIsStreamReady: s.setIsStreamReady,
|
|
||||||
isLSPServerReady: s.isLSPServerReady,
|
|
||||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
|
||||||
formatCode: s.formatCode,
|
|
||||||
addKCLError: s.addKCLError,
|
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const {
|
const { settings } = useGlobalStateContext()
|
||||||
auth: {
|
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
||||||
context: { token },
|
settings?.context || {}
|
||||||
},
|
const { state, send } = useModelingContext()
|
||||||
settings: {
|
|
||||||
context: { showDebugPanel, theme, onboardingStatus },
|
|
||||||
},
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
@ -157,258 +70,41 @@ export function App() {
|
|||||||
useHotkeys('shift + l', () => togglePane('logs'))
|
useHotkeys('shift + l', () => togglePane('logs'))
|
||||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||||
useHotkeys('shift + d', () => togglePane('debug'))
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
|
useHotkeys('esc', () => send('Cancel'))
|
||||||
|
|
||||||
const paneOpacity =
|
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||||
onboardingStatus === onboardingPaths.CAMERA
|
(p) => p === onboardingStatus
|
||||||
|
)
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: didDragInStream
|
: didDragInStream
|
||||||
? 'opacity-40'
|
? 'opacity-40'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// Use file code loaded from disk
|
useEngineConnectionSubscriptions()
|
||||||
// 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])
|
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
|
||||||
setCode(value)
|
|
||||||
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) => {
|
|
||||||
if (!editorView) {
|
|
||||||
setEditorView(viewUpdate.view)
|
|
||||||
}
|
|
||||||
const ranges = viewUpdate.state.selection.ranges
|
|
||||||
|
|
||||||
const isChange =
|
|
||||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
|
||||||
ranges.some(({ from, to }, i) => {
|
|
||||||
return (
|
|
||||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
|
||||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isChange) return
|
|
||||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
|
||||||
({ from, to }) => {
|
|
||||||
if (selectionRangeTypeMap[to]) {
|
|
||||||
return {
|
|
||||||
type: selectionRangeTypeMap[to],
|
|
||||||
range: [from, to],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'default',
|
|
||||||
range: [from, to],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const idBasedSelections = codeBasedSelections
|
|
||||||
.map(({ type, range }) => {
|
|
||||||
const hasOverlap = Object.entries(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 streamWidth = streamRef?.current?.offsetWidth
|
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
|
||||||
|
|
||||||
const width = streamWidth ? streamWidth : 0
|
|
||||||
const quadWidth = Math.round(width / 4) * 4
|
|
||||||
const height = streamHeight ? streamHeight : 0
|
|
||||||
const quadHeight = Math.round(height / 4) * 4
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setStreamDimensions({
|
|
||||||
streamWidth: quadWidth,
|
|
||||||
streamHeight: quadHeight,
|
|
||||||
})
|
|
||||||
if (!width || !height) return
|
|
||||||
const eng = new EngineCommandManager({
|
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
width: quadWidth,
|
|
||||||
height: quadHeight,
|
|
||||||
token,
|
|
||||||
})
|
|
||||||
setEngineCommandManager(eng)
|
|
||||||
return () => {
|
|
||||||
eng?.tearDown()
|
|
||||||
}
|
|
||||||
}, [quadWidth, quadHeight])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isStreamReady) return
|
|
||||||
if (!engineCommandManager) return
|
|
||||||
let unsubFn: any[] = []
|
|
||||||
const asyncWrap = async () => {
|
|
||||||
try {
|
|
||||||
if (!code) {
|
|
||||||
setAst(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const _ast = await asyncParser(code)
|
|
||||||
setAst(_ast)
|
|
||||||
resetLogs()
|
|
||||||
resetKCLErrors()
|
|
||||||
engineCommandManager.endSession()
|
|
||||||
engineCommandManager.startNewSession()
|
|
||||||
const programMemory = await _executor(
|
|
||||||
_ast,
|
|
||||||
{
|
|
||||||
root: {
|
|
||||||
_0: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 0,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_90: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 90,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_180: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 180,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_270: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 270,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
|
|
||||||
const { artifactMap, sourceRangeMap } =
|
|
||||||
await engineCommandManager.waitForAllCommands()
|
|
||||||
|
|
||||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
|
||||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
|
||||||
event: 'highlight_set_entity',
|
|
||||||
callback: ({ data }) => {
|
|
||||||
if (!data?.entity_id) {
|
|
||||||
setHighlightRange([0, 0])
|
|
||||||
} else {
|
|
||||||
const sourceRange = sourceRangeMap[data.entity_id]
|
|
||||||
setHighlightRange(sourceRange)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
|
||||||
event: 'select_with_point',
|
|
||||||
callback: ({ data }) => {
|
|
||||||
if (!data?.entity_id) {
|
|
||||||
setCursor2()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sourceRange = sourceRangeMap[data.entity_id]
|
|
||||||
setCursor2({ range: sourceRange, type: 'default' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
unsubFn.push(unSubHover, unSubClick)
|
|
||||||
if (programMemory !== undefined) {
|
|
||||||
setProgramMemory(programMemory)
|
|
||||||
}
|
|
||||||
|
|
||||||
setError()
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof KCLError) {
|
|
||||||
addKCLError(e)
|
|
||||||
} else {
|
|
||||||
setError('problem')
|
|
||||||
console.log(e)
|
|
||||||
addLog(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncWrap()
|
|
||||||
return () => {
|
|
||||||
unsubFn.forEach((fn) => fn())
|
|
||||||
}
|
|
||||||
}, [code, isStreamReady, engineCommandManager])
|
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager?.sendSceneCommand(message)
|
engineCommandManager.sendSceneCommand(message)
|
||||||
}, 16)
|
}, 16)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
clientX,
|
e.nativeEvent.preventDefault()
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
shiftKey,
|
|
||||||
currentTarget,
|
|
||||||
nativeEvent,
|
|
||||||
}) => {
|
|
||||||
nativeEvent.preventDefault()
|
|
||||||
if (isMouseDownInStream) {
|
|
||||||
setDidDragInStream(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX: e.clientX,
|
||||||
clientY,
|
clientY: e.clientY,
|
||||||
el: currentTarget,
|
el: e.currentTarget,
|
||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
|
if (buttonDownInStream === undefined) {
|
||||||
if (isMouseDownInStream) {
|
if (state.matches('Sketch.Line Tool')) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: newCmdId,
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_move',
|
type: 'mouse_move',
|
||||||
interaction,
|
|
||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
@ -420,129 +116,93 @@ export function App() {
|
|||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (state.matches('Sketch.Move Tool')) {
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: newCmdId,
|
||||||
|
cmd: {
|
||||||
|
type: 'handle_mouse_drag_move',
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||||
|
let interaction: CameraDragInteractionType_type
|
||||||
|
|
||||||
|
const eWithButton = { ...e, button: buttonDownInStream }
|
||||||
|
|
||||||
|
if (interactionGuards.pan.callback(eWithButton)) {
|
||||||
|
interaction = 'pan'
|
||||||
|
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||||
|
interaction = 'rotate'
|
||||||
|
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||||
|
interaction = 'zoom'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraExtensions = useMemo(() => {
|
debounceSocketSend({
|
||||||
if (TEST) return []
|
type: 'modeling_cmd_req',
|
||||||
return [
|
cmd: {
|
||||||
lintGutter(),
|
type: 'camera_drag_move',
|
||||||
linter((_view) => {
|
interaction,
|
||||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
window: { x, y },
|
||||||
}),
|
},
|
||||||
EditorView.lineWrapping,
|
cmd_id: newCmdId,
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
|
||||||
// But the server happens async so we break this into two parts.
|
|
||||||
// Below is the client and server promise.
|
|
||||||
const { lspClient } = useMemo(() => {
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
|
||||||
const fromServer: FromServer = FromServer.create()
|
|
||||||
const client = new Client(fromServer, intoServer)
|
|
||||||
if (!TEST) {
|
|
||||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
|
||||||
lspServer.start()
|
|
||||||
setIsLSPServerReady(true)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client })
|
|
||||||
return { lspClient }
|
|
||||||
}, [setIsLSPServerReady])
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// 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 :)
|
|
||||||
// We do not want to restart the server, its just wasteful.
|
|
||||||
const kclLSP = useMemo(() => {
|
|
||||||
let plugin = null
|
|
||||||
if (isLSPServerReady && !TEST) {
|
|
||||||
// Set up the lsp plugin.
|
|
||||||
const lsp = kclLanguage({
|
|
||||||
// When we have more than one file, we'll need to change this.
|
|
||||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
|
||||||
workspaceFolders: null,
|
|
||||||
client: lspClient,
|
|
||||||
})
|
|
||||||
|
|
||||||
plugin = lsp
|
|
||||||
}
|
}
|
||||||
return plugin
|
|
||||||
}, [lspClient, isLSPServerReady])
|
|
||||||
|
|
||||||
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={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={project}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<Resizable
|
||||||
className={
|
className={
|
||||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||||
(isMouseDownInStream || onboardingStatus === 'camera'
|
+paneOpacity
|
||||||
? ' pointer-events-none '
|
|
||||||
: ' ') +
|
|
||||||
paneOpacity
|
|
||||||
}
|
}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '400px',
|
width: '550px',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
}}
|
}}
|
||||||
minWidth={200}
|
minWidth={200}
|
||||||
maxWidth={600}
|
maxWidth={800}
|
||||||
minHeight={'auto'}
|
minHeight={'auto'}
|
||||||
maxHeight={'auto'}
|
maxHeight={'auto'}
|
||||||
handleClasses={{
|
handleClasses={{
|
||||||
right:
|
right:
|
||||||
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
|
||||||
|
(buttonDownInStream || onboardingStatus === 'camera'
|
||||||
|
? 'pointer-events-none '
|
||||||
|
: 'pointer-events-auto'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col justify-between">
|
<div
|
||||||
|
id="code-pane"
|
||||||
|
className="h-full flex flex-col justify-between pointer-events-none"
|
||||||
|
>
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
title="Code"
|
title="Code"
|
||||||
icon={faCode}
|
icon={faCode}
|
||||||
className="open:!mb-2 overflow-x-hidden"
|
className="open:!mb-2"
|
||||||
open={openPanes.includes('code')}
|
open={openPanes.includes('code')}
|
||||||
|
menu={<CodeMenu />}
|
||||||
>
|
>
|
||||||
<div className="px-2 py-1">
|
<TextEditor theme={editorTheme} />
|
||||||
<button
|
|
||||||
// disabled={!shouldFormat}
|
|
||||||
onClick={formatCode}
|
|
||||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
|
||||||
>
|
|
||||||
format
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="code-mirror-override"
|
|
||||||
className="overflow-x-hidden h-full"
|
|
||||||
>
|
|
||||||
<CodeMirror
|
|
||||||
className="h-full overflow-hidden-x"
|
|
||||||
value={code}
|
|
||||||
extensions={
|
|
||||||
kclLSP
|
|
||||||
? [kclLSP, lineHighlightField, ...extraExtensions]
|
|
||||||
: [lineHighlightField, ...extraExtensions]
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
onUpdate={onUpdate}
|
|
||||||
theme={editorTheme}
|
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<MemoryPanel
|
<MemoryPanel
|
||||||
@ -573,7 +233,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
open={openPanes.includes('debug')}
|
open={openPanes.includes('debug')}
|
||||||
/>
|
/>
|
||||||
|
12
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 isLoggedIn = state.matches('checkIfLoggedIn')
|
|
||||||
|
|
||||||
return isLoggedIn ? (
|
return isLoggingIn ? (
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>
|
||||||
|
<span data-testid="initial-load">Loading Modeling App...</span>
|
||||||
|
</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
)
|
)
|
||||||
|
@ -31,15 +31,20 @@ 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,
|
||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar/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,18 +137,27 @@ 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 />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.FILE + '/:id',
|
path: paths.FILE + '/:id',
|
||||||
element: (
|
element: (
|
||||||
<Auth>
|
<Auth>
|
||||||
|
<FileMachineProvider>
|
||||||
|
<KclContextProvider>
|
||||||
|
<ModelingMachineProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
|
</ModelingMachineProvider>
|
||||||
|
<WasmErrBanner />
|
||||||
|
</KclContextProvider>
|
||||||
|
</FileMachineProvider>
|
||||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
id: paths.FILE,
|
id: paths.FILE,
|
||||||
loader: async ({
|
loader: async ({
|
||||||
request,
|
request,
|
||||||
@ -167,21 +184,42 @@ const router = createBrowserRouter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.id && params.id !== 'new') {
|
const defaultDir = persistedSettings.defaultDirectory || ''
|
||||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
|
||||||
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT)
|
if (params.id && params.id !== BROWSER_FILE_NAME) {
|
||||||
const entrypoint_metadata = await metadata(
|
const decodedId = decodeURIComponent(params.id)
|
||||||
params.id + '/' + PROJECT_ENTRYPOINT
|
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}`
|
||||||
|
)}`
|
||||||
)
|
)
|
||||||
const children = await readDir(params.id)
|
|
||||||
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
|
const code = await readTextFile(decodedId)
|
||||||
|
const entrypointMetadata = await metadata(
|
||||||
|
projectPath + sep + PROJECT_ENTRYPOINT
|
||||||
|
)
|
||||||
|
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,60 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
|
403
src/Toolbar.tsx
@ -1,242 +1,221 @@
|
|||||||
import { useStore, toolTips } 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 usePlatform from 'hooks/usePlatform'
|
||||||
import { Intersect } from './components/Toolbar/Intersect'
|
|
||||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
|
||||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
|
||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
|
||||||
import { Fragment, useEffect } from 'react'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
|
||||||
import styles from './Toolbar.module.css'
|
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const platform = usePlatform()
|
||||||
setGuiMode,
|
const { commandBarSend } = useCommandsContext()
|
||||||
guiMode,
|
const { state, send, context } = useModelingContext()
|
||||||
selectionRanges,
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
ast,
|
const bgClassName =
|
||||||
updateAst,
|
'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'
|
||||||
programMemory,
|
const pathId = useMemo(
|
||||||
} = useStore((s) => ({
|
() =>
|
||||||
guiMode: s.guiMode,
|
isCursorInSketchCommandRange(
|
||||||
setGuiMode: s.setGuiMode,
|
engineCommandManager.artifactMap,
|
||||||
selectionRanges: s.selectionRanges,
|
context.selectionRanges
|
||||||
ast: s.ast,
|
),
|
||||||
updateAst: s.updateAst,
|
[engineCommandManager.artifactMap, context.selectionRanges]
|
||||||
programMemory: s.programMemory,
|
)
|
||||||
}))
|
|
||||||
|
|
||||||
useEffect(() => {
|
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||||
console.log('guiMode', guiMode)
|
const span = toolbarButtonsRef.current
|
||||||
}, [guiMode])
|
if (!span) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
function ToolbarButtons() {
|
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButtons({
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLElement>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<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' }}
|
||||||
|
>
|
||||||
|
{state.nextEvents.includes('Enter sketch') && (
|
||||||
|
<li className="contents">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
|
icon={{
|
||||||
|
icon: 'sketch',
|
||||||
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Start Sketch
|
<span data-testid="start-sketch">Start Sketch</span>
|
||||||
</button>
|
</ActionButton>
|
||||||
|
</li>
|
||||||
)}
|
)}
|
||||||
{guiMode.mode === 'canEditExtrude' && (
|
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||||
<button
|
<li className="contents">
|
||||||
onClick={() => {
|
<ActionButton
|
||||||
if (!ast) return
|
Element="button"
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
ast,
|
icon={{
|
||||||
selectionRanges.codeBasedSelections[0].range
|
icon: 'sketch',
|
||||||
)
|
bgClassName,
|
||||||
const { modifiedAst } = sketchOnExtrudedFace(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
programMemory
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
SketchOnFace
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setGuiMode({
|
|
||||||
mode: 'sketch',
|
|
||||||
sketchMode: 'sketchEdit',
|
|
||||||
pathToNode: guiMode.pathToNode,
|
|
||||||
rotation: guiMode.rotation,
|
|
||||||
position: guiMode.position,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit Sketch
|
Edit Sketch
|
||||||
</button>
|
</ActionButton>
|
||||||
|
</li>
|
||||||
)}
|
)}
|
||||||
{guiMode.mode === 'canEditSketch' && (
|
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||||
<>
|
<li className="contents">
|
||||||
<button
|
<ActionButton
|
||||||
onClick={() => {
|
Element="button"
|
||||||
if (!ast) return
|
onClick={() => send({ type: 'Cancel' })}
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
icon={{
|
||||||
ast,
|
icon: 'arrowLeft',
|
||||||
selectionRanges.codeBasedSelections[0].range
|
bgClassName,
|
||||||
)
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ExtrudeSketch
|
Exit Sketch
|
||||||
</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, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ExtrudeSketch (w/o pipe)
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
{state.matches('Sketch') && !state.matches('idle') && (
|
||||||
{guiMode.mode === 'sketch' && (
|
<li className="contents">
|
||||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
<ActionButton
|
||||||
Exit sketch
|
Element="button"
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{toolTips
|
|
||||||
.filter(
|
|
||||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
|
||||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
|
||||||
)
|
|
||||||
.map((sketchFnName) => {
|
|
||||||
if (
|
|
||||||
guiMode.mode !== 'sketch' ||
|
|
||||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={sketchFnName}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setGuiMode({
|
state.matches('Sketch.Line Tool')
|
||||||
...guiMode,
|
? send('CancelSketch')
|
||||||
...(guiMode.sketchMode === sketchFnName
|
: send('Equip tool')
|
||||||
? {
|
|
||||||
sketchMode: 'sketchEdit',
|
|
||||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
|
||||||
}
|
}
|
||||||
: {
|
aria-pressed={state.matches('Sketch.Line Tool')}
|
||||||
sketchMode: sketchFnName,
|
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||||
isTooltip: true,
|
icon={{
|
||||||
}),
|
icon: 'line',
|
||||||
|
bgClassName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Line
|
||||||
|
</ActionButton>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{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={() =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={!state.can('Extrude')}
|
||||||
|
title={
|
||||||
|
state.can('Extrude')
|
||||||
|
? 'extrude'
|
||||||
|
: 'sketches need to be closed, or not already extruded'
|
||||||
|
}
|
||||||
|
icon={{
|
||||||
|
icon: 'extrude',
|
||||||
|
bgClassName,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{sketchFnName}
|
Extrude
|
||||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
</ActionButton>
|
||||||
</button>
|
</li>
|
||||||
)
|
)}
|
||||||
})}
|
</ul>
|
||||||
<ConvertToVariable />
|
|
||||||
<HorzVert horOrVert="horizontal" />
|
|
||||||
<HorzVert horOrVert="vertical" />
|
|
||||||
<EqualLength />
|
|
||||||
<EqualAngle />
|
|
||||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
|
||||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
|
||||||
<SetAbsDistance buttonType="snapToYAxis" />
|
|
||||||
<SetAbsDistance buttonType="xAbs" />
|
|
||||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
|
||||||
<SetAbsDistance buttonType="snapToXAxis" />
|
|
||||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
|
||||||
<SetAbsDistance buttonType="yAbs" />
|
|
||||||
<SetAngleLength angleOrLength="setAngle" />
|
|
||||||
<SetAngleLength angleOrLength="setLength" />
|
|
||||||
<Intersect />
|
|
||||||
<RemoveConstrainingValues />
|
|
||||||
<SetAngleBetween />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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}>
|
|
||||||
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
|
||||||
</span>
|
|
||||||
<menu className="flex flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
|
||||||
<ToolbarButtons />
|
<ToolbarButtons />
|
||||||
</menu>
|
</menu>
|
||||||
<Popover.Button
|
<ActionButton
|
||||||
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
Element="button"
|
||||||
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faSearch} />
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
</Popover.Button>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="transition ease-out duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
|
||||||
</Transition>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="opacity-0 translate-y-1 scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 scale-100"
|
|
||||||
leave="transition ease-out duration-75"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-2"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
|
||||||
<section className="flex justify-between items-center">
|
|
||||||
<p
|
|
||||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
|
||||||
>
|
|
||||||
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
|
||||||
</p>
|
|
||||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
|
||||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
|
||||||
</Popover.Button>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<ToolbarButtons />
|
|
||||||
</section>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</Popover>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
@ -4,16 +4,17 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
|
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
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 {
|
||||||
icon?: SolidIconDefinition | BrandIconDefinition
|
icon?: SolidIconDefinition | BrandIconDefinition | CustomIconName
|
||||||
className?: string
|
className?: string
|
||||||
bgClassName?: string
|
bgClassName?: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
@ -28,25 +29,33 @@ export const ActionIcon = ({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
children,
|
children,
|
||||||
}: ActionIconProps) => {
|
}: ActionIconProps) => {
|
||||||
|
// By default, we reverse the icon color and background color in dark mode
|
||||||
|
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
||||||
|
|
||||||
|
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`p-${
|
`w-fit inline-grid place-content-center ${className} ` +
|
||||||
size === 'xl' ? '2' : '1'
|
computedBgClassName
|
||||||
} w-fit inline-grid place-content-center ${className} ` +
|
|
||||||
(bgClassName ||
|
|
||||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children || (
|
{children ? (
|
||||||
|
children
|
||||||
|
) : typeof icon === 'string' ? (
|
||||||
|
<CustomIcon
|
||||||
|
name={icon}
|
||||||
|
width={iconSizes[size]}
|
||||||
|
height={iconSizes[size]}
|
||||||
|
className={computedIconClassName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
width={iconSizes[size]}
|
width={iconSizes[size]}
|
||||||
height={iconSizes[size]}
|
height={iconSizes[size]}
|
||||||
className={
|
className={computedIconClassName}
|
||||||
iconClassName ||
|
|
||||||
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
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 { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -19,34 +23,51 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const {
|
const platform = usePlatform()
|
||||||
auth: {
|
const { commandBarSend } = useCommandsContext()
|
||||||
context: { user },
|
const { auth } = useGlobalStateContext()
|
||||||
},
|
const user = auth?.context?.user
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={
|
className={
|
||||||
(showToolbar ? '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-2 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-4xl">
|
{showToolbar ? (
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
</div>
|
) : (
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
|
className="text-sm self-center flex items-center w-fit gap-3"
|
||||||
|
>
|
||||||
|
Command Palette{' '}
|
||||||
|
<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">
|
||||||
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
|
</kbd>
|
||||||
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 ml-auto">
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || (
|
{children || (
|
||||||
<div className="ml-auto">
|
<>
|
||||||
|
<NetworkHealthIndicator />
|
||||||
<UserSidebarMenu user={user} />
|
<UserSidebarMenu user={user} />
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
194
src/components/AstExplorer.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
|
export function AstExplorer() {
|
||||||
|
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||||
|
const { context } = useModelingContext()
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
// TODO maybe need to have callback to make sure it stays in sync
|
||||||
|
kclManager.ast,
|
||||||
|
context.selectionRanges.codeBasedSelections?.[0]?.range
|
||||||
|
)
|
||||||
|
const node = getNodeFromPath(kclManager.ast, pathToNode).node
|
||||||
|
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" style={{ width: '300px' }}>
|
||||||
|
<div className="">
|
||||||
|
filter out keys:<div className="w-2 inline-block"></div>
|
||||||
|
{['start', 'end', 'type'].map((key) => {
|
||||||
|
return (
|
||||||
|
<label key={key} className="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-checkbox"
|
||||||
|
checked={filterKeys.includes(key)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (filterKeys.includes(key)) {
|
||||||
|
setFilterKeys(filterKeys.filter((k) => k !== key))
|
||||||
|
} else {
|
||||||
|
setFilterKeys([...filterKeys, key])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="mr-2">{key}</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="h-full relative"
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
setHighlightRange([0, 0])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||||
|
<DisplayObj
|
||||||
|
obj={kclManager.ast}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayBody({
|
||||||
|
body,
|
||||||
|
filterKeys,
|
||||||
|
node,
|
||||||
|
}: {
|
||||||
|
body: { start: number; end: number; [key: string]: any }[]
|
||||||
|
filterKeys: string[]
|
||||||
|
node: any
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{body.map((b, index) => {
|
||||||
|
return (
|
||||||
|
<div className="my-2" key={index}>
|
||||||
|
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayObj({
|
||||||
|
obj,
|
||||||
|
filterKeys,
|
||||||
|
node,
|
||||||
|
}: {
|
||||||
|
obj: { start: number; end: number; [key: string]: any }
|
||||||
|
filterKeys: string[]
|
||||||
|
node: any
|
||||||
|
}) {
|
||||||
|
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||||
|
const { send } = useModelingContext()
|
||||||
|
const ref = useRef<HTMLPreElement>(null)
|
||||||
|
const [hasCursor, setHasCursor] = useState(false)
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
node?.start === obj?.start &&
|
||||||
|
node?.end === obj?.end &&
|
||||||
|
node.type === obj?.type
|
||||||
|
) {
|
||||||
|
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
|
||||||
|
setHasCursor(true)
|
||||||
|
} else {
|
||||||
|
setHasCursor(false)
|
||||||
|
}
|
||||||
|
}, [node.start, node.end, node.type])
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
ref={ref}
|
||||||
|
className={`ml-2 border-l border-violet-600 pl-1 ${
|
||||||
|
hasCursor ? 'bg-violet-100/25' : ''
|
||||||
|
}`}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
send({
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'singleCodeCursor',
|
||||||
|
selection: {
|
||||||
|
type: 'default',
|
||||||
|
range: [obj?.start || 0, obj.end || 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<button
|
||||||
|
className="m-0 p-0 border-0"
|
||||||
|
onClick={() => setIsCollapsed(false)}
|
||||||
|
>
|
||||||
|
{'>'}type: {obj.type}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="flex">
|
||||||
|
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
|
||||||
|
<ul className="inline-block">
|
||||||
|
{Object.entries(obj).map(([key, value]) => {
|
||||||
|
if (filterKeys.includes(key)) {
|
||||||
|
return null
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{`${key}: [`}
|
||||||
|
<DisplayBody
|
||||||
|
body={value}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
{']'}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
value?.end
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{key}:
|
||||||
|
<DisplayObj
|
||||||
|
obj={value}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'number'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{key}: {value}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
@ -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: {} }
|
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,
|
||||||
@ -198,33 +203,33 @@ export const CreateNewVariable = ({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
shouldCreateVariable,
|
shouldCreateVariable,
|
||||||
setShouldCreateVariable,
|
setShouldCreateVariable = () => {},
|
||||||
showCheckbox = true,
|
showCheckbox = true,
|
||||||
}: {
|
}: {
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
shouldCreateVariable: boolean
|
shouldCreateVariable?: boolean
|
||||||
setShouldCreateVariable: (a: boolean) => void
|
setShouldCreateVariable?: (a: boolean) => void
|
||||||
showCheckbox?: boolean
|
showCheckbox?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label
|
<label
|
||||||
htmlFor="create-new-variable"
|
htmlFor="create-new-variable"
|
||||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
className="block mt-3 font-mono text-gray-900"
|
||||||
>
|
>
|
||||||
Create new variable
|
Create new variable
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex flex-1">
|
<div className="mt-1 flex gap-2 items-center">
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
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 flex-shrink"
|
|
||||||
checked={shouldCreateVariable}
|
checked={shouldCreateVariable}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setShouldCreateVariable(e.target.checked)
|
setShouldCreateVariable(e.target.checked)
|
||||||
}}
|
}}
|
||||||
|
className="bg-white text-gray-900"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
@ -232,7 +237,10 @@ export const CreateNewVariable = ({
|
|||||||
disabled={!shouldCreateVariable}
|
disabled={!shouldCreateVariable}
|
||||||
name="create-new-variable"
|
name="create-new-variable"
|
||||||
id="create-new-variable"
|
id="create-new-variable"
|
||||||
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
|
autoFocus={true}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
|
||||||
!shouldCreateVariable ? 'opacity-50' : ''
|
!shouldCreateVariable ? 'opacity-50' : ''
|
||||||
}`}
|
}`}
|
||||||
value={newVariableName}
|
value={newVariableName}
|
||||||
|
19
src/components/CodeMenu.module.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.button {
|
||||||
|
@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 ui-active:bg-energy-10/50 ui-active:text-inherit;
|
||||||
|
@apply transition-colors ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .button {
|
||||||
|
@apply text-chalkboard-30;
|
||||||
|
@apply ui-active:bg-chalkboard-80 ui-active:text-energy-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button small {
|
||||||
|
@apply text-chalkboard-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .button small {
|
||||||
|
@apply text-chalkboard-40;
|
||||||
|
}
|
84
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Menu } from '@headlessui/react'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faEllipsis,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import styles from './CodeMenu.module.css'
|
||||||
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
|
import { editorShortcutMeta } from './TextEditor'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
|
||||||
|
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
|
useConvertToVariable()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (e.eventPhase === 3 && target.closest('a') === null) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Button className="p-0 border-none relative">
|
||||||
|
<ActionIcon
|
||||||
|
icon={faEllipsis}
|
||||||
|
className="p-1"
|
||||||
|
size="sm"
|
||||||
|
bgClassName={
|
||||||
|
'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'}
|
||||||
|
/>
|
||||||
|
</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.Item>
|
||||||
|
<button
|
||||||
|
onClick={() => kclManager.format()}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
<span>Format code</span>
|
||||||
|
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
{convertToVarEnabled && (
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
onClick={handleConvertToVarClick}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
<span>Convert to Variable</span>
|
||||||
|
<small>{editorShortcutMeta.convertToVariable.display}</small>
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item>
|
||||||
|
<a
|
||||||
|
className={styles.button}
|
||||||
|
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span>Read the KCL docs</span>
|
||||||
|
<small>
|
||||||
|
On GitHub
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1 align-text-top"
|
||||||
|
width={12}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
.panel {
|
.panel {
|
||||||
@apply relative overflow-auto z-0;
|
@apply relative z-0;
|
||||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
@apply sticky top-0 z-10 cursor-pointer;
|
@apply sticky top-0 z-10 cursor-pointer;
|
||||||
@apply flex items-center gap-2 w-full p-2;
|
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||||
@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 bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ export interface CollapsiblePanelProps
|
|||||||
title: string
|
title: string
|
||||||
icon?: IconDefinition
|
icon?: IconDefinition
|
||||||
open?: boolean
|
open?: boolean
|
||||||
|
menu?: React.ReactNode
|
||||||
|
detailsTestId?: string
|
||||||
iconClassNames?: {
|
iconClassNames?: {
|
||||||
bg?: string
|
bg?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@ -18,21 +20,28 @@ export const PanelHeader = ({
|
|||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
|
<div className="flex gap-2 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 border border-transparent 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}
|
||||||
|
</div>
|
||||||
|
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -43,14 +52,24 @@ export const CollapsiblePanel = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
|
detailsTestId,
|
||||||
...props
|
...props
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
{...props}
|
{...props}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
data-testid={detailsTestId}
|
||||||
|
className={
|
||||||
|
styles.panel + ' pointer-events-auto group ' + (className || '')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
<PanelHeader
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
iconClassNames={iconClassNames}
|
||||||
|
menu={menu}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
|
@ -1,290 +0,0 @@
|
|||||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
Fragment,
|
|
||||||
SetStateAction,
|
|
||||||
createContext,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import { ActionIcon } from './ActionIcon'
|
|
||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import { Command, SubCommand } from '../lib/commands'
|
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
|
|
||||||
export type SortedCommand = {
|
|
||||||
item: Partial<Command | SubCommand> & { name: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandsContext = createContext(
|
|
||||||
{} as {
|
|
||||||
commands: Command[]
|
|
||||||
addCommands: (commands: Command[]) => void
|
|
||||||
removeCommands: (commands: Command[]) => void
|
|
||||||
commandBarOpen: boolean
|
|
||||||
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commands, internalSetCommands] = useState([] as Command[])
|
|
||||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
|
||||||
|
|
||||||
const addCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
|
||||||
}
|
|
||||||
const removeCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
prevCommands.filter((command) => !newCommands.includes(command))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandsContext.Provider
|
|
||||||
value={{
|
|
||||||
commands,
|
|
||||||
addCommands,
|
|
||||||
removeCommands,
|
|
||||||
commandBarOpen,
|
|
||||||
setCommandBarOpen,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandBar = () => {
|
|
||||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
|
||||||
useHotkeys('meta+k', () => {
|
|
||||||
if (commands.length === 0) return
|
|
||||||
setCommandBarOpen(!commandBarOpen)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
|
|
||||||
null
|
|
||||||
)
|
|
||||||
// keep track of the current subcommand index
|
|
||||||
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
|
||||||
const [subCommandData, setSubCommandData] = useState<{
|
|
||||||
[key: string]: string
|
|
||||||
}>({})
|
|
||||||
|
|
||||||
// if the subcommand index is null, we're not in a subcommand
|
|
||||||
const inSubCommand =
|
|
||||||
selectedCommand &&
|
|
||||||
'meta' in selectedCommand.item &&
|
|
||||||
selectedCommand.item.meta?.args !== undefined &&
|
|
||||||
subCommandIndex !== undefined
|
|
||||||
const currentSubCommand =
|
|
||||||
inSubCommand && 'meta' in selectedCommand.item
|
|
||||||
? selectedCommand.item.meta?.args[subCommandIndex]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
|
|
||||||
const availableCommands =
|
|
||||||
inSubCommand && currentSubCommand
|
|
||||||
? currentSubCommand.type === 'string'
|
|
||||||
? query
|
|
||||||
? [{ name: query }]
|
|
||||||
: currentSubCommand.options
|
|
||||||
: currentSubCommand.options
|
|
||||||
: commands
|
|
||||||
|
|
||||||
const fuse = new Fuse(availableCommands || [], {
|
|
||||||
keys: ['name', 'description'],
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredCommands = query
|
|
||||||
? fuse.search(query)
|
|
||||||
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
|
||||||
|
|
||||||
function clearState() {
|
|
||||||
setQuery('')
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
setSelectedCommand(null)
|
|
||||||
setSubCommandIndex(undefined)
|
|
||||||
setSubCommandData({})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommandSelection(entry: SortedCommand) {
|
|
||||||
// If we have subcommands and have not yet gathered all the
|
|
||||||
// data required from them, set the selected command to the
|
|
||||||
// current command and increment the subcommand index
|
|
||||||
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
|
||||||
setSelectedCommand(entry)
|
|
||||||
setSubCommandIndex(0)
|
|
||||||
setQuery('')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { item } = entry
|
|
||||||
// If we have just selected a command with no subcommands, run it
|
|
||||||
const isCommandWithoutSubcommands =
|
|
||||||
'callback' in item && !('meta' in item && item.meta)
|
|
||||||
if (isCommandWithoutSubcommands) {
|
|
||||||
if (item.callback === undefined) return
|
|
||||||
item.callback()
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have subcommands and have not yet gathered all the
|
|
||||||
// data required from them, set the selected command to the
|
|
||||||
// current command and increment the subcommand index
|
|
||||||
if (
|
|
||||||
selectedCommand &&
|
|
||||||
subCommandIndex !== undefined &&
|
|
||||||
'meta' in selectedCommand.item
|
|
||||||
) {
|
|
||||||
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
|
||||||
|
|
||||||
if (subCommand) {
|
|
||||||
const newSubCommandData = {
|
|
||||||
...subCommandData,
|
|
||||||
[subCommand.name]: item.name,
|
|
||||||
}
|
|
||||||
const newSubCommandIndex = subCommandIndex + 1
|
|
||||||
|
|
||||||
// If we have subcommands and have gathered all the data required
|
|
||||||
// from them, run the command with the gathered data
|
|
||||||
if (
|
|
||||||
selectedCommand.item.callback &&
|
|
||||||
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
|
||||||
) {
|
|
||||||
selectedCommand.item.callback(newSubCommandData)
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
} else {
|
|
||||||
// Otherwise, set the subcommand data and increment the subcommand index
|
|
||||||
setSubCommandData(newSubCommandData)
|
|
||||||
setSubCommandIndex(newSubCommandIndex)
|
|
||||||
setQuery('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayValue(command: Command) {
|
|
||||||
if (command.meta?.displayValue === undefined || !command.meta.args)
|
|
||||||
return command.name
|
|
||||||
return command.meta?.displayValue(
|
|
||||||
command.meta.args.map((c) =>
|
|
||||||
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root
|
|
||||||
show={
|
|
||||||
commandBarOpen &&
|
|
||||||
availableCommands?.length !== undefined &&
|
|
||||||
availableCommands.length > 0
|
|
||||||
}
|
|
||||||
as={Fragment}
|
|
||||||
afterLeave={() => clearState()}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
onClose={() => {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
clearState()
|
|
||||||
}}
|
|
||||||
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
|
|
||||||
>
|
|
||||||
<Transition.Child
|
|
||||||
enter="duration-100 ease-out"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="duration-75 ease-in"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
|
||||||
</Transition.Child>
|
|
||||||
<Transition.Child
|
|
||||||
enter="duration-100 ease-out"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="duration-75 ease-in"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Combobox
|
|
||||||
value={selectedCommand}
|
|
||||||
onChange={handleCommandSelection}
|
|
||||||
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
|
||||||
as="div"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
|
||||||
<div>
|
|
||||||
{inSubCommand && (
|
|
||||||
<p className="text-liquid-70 dark:text-liquid-30">
|
|
||||||
{selectedCommand.item &&
|
|
||||||
getDisplayValue(selectedCommand.item as Command)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<Combobox.Input
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
className="bg-transparent focus:outline-none w-full"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.metaKey && event.key === 'k')
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
if (
|
|
||||||
inSubCommand &&
|
|
||||||
event.key === 'Backspace' &&
|
|
||||||
!event.currentTarget.value
|
|
||||||
) {
|
|
||||||
setSubCommandIndex(subCommandIndex - 1)
|
|
||||||
setSelectedCommand(null)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
displayValue={(command: SortedCommand) =>
|
|
||||||
command !== null ? command.item.name : ''
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
inSubCommand
|
|
||||||
? `Enter <${currentSubCommand?.name}>`
|
|
||||||
: 'Search for a command'
|
|
||||||
}
|
|
||||||
value={query}
|
|
||||||
autoCapitalize="off"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
|
||||||
{filteredCommands?.map((commandResult) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={commandResult.item.name}
|
|
||||||
value={commandResult}
|
|
||||||
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
|
|
||||||
>
|
|
||||||
<p>{commandResult.item.name}</p>
|
|
||||||
{(commandResult.item as SubCommand).description && (
|
|
||||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
|
||||||
{(commandResult.item as SubCommand).description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Combobox>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandBarProvider
|
|
114
src/components/CommandBar/CommandArgOptionInput.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Combobox } from '@headlessui/react'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { CommandArgumentOption } from 'lib/commandTypes'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
function CommandArgOptionInput({
|
||||||
|
options,
|
||||||
|
argName,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
options: CommandArgumentOption<unknown>[]
|
||||||
|
argName: string
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (data: unknown) => void
|
||||||
|
placeholder?: string
|
||||||
|
}) {
|
||||||
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
|
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
|
||||||
|
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
|
||||||
|
commandBarState.context.argumentsToSubmit[argName] ||
|
||||||
|
options[0].value
|
||||||
|
)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||||
|
|
||||||
|
const fuse = new Fuse(options, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
inputRef.current?.select()
|
||||||
|
}, [inputRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
|
setFilteredOptions(query.length > 0 ? results : options)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
||||||
|
setArgValue(option)
|
||||||
|
onSubmit(option.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(argValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
||||||
|
<Combobox value={argValue} onChange={handleSelectOption} name="options">
|
||||||
|
<div className="flex items-center mx-4 mt-4 mb-2">
|
||||||
|
<label
|
||||||
|
htmlFor="option-input"
|
||||||
|
className="capitalize 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"
|
||||||
|
>
|
||||||
|
{argName}
|
||||||
|
</label>
|
||||||
|
<Combobox.Input
|
||||||
|
id="option-input"
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k')
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
(argValue as CommandArgumentOption<unknown>)?.name ||
|
||||||
|
placeholder ||
|
||||||
|
'Select an option for ' + argName
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandArgOptionInput
|
166
src/components/CommandBar/CommandBar.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
|
import { Fragment, createContext, useEffect } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
|
import { EventFrom, StateFrom } from 'xstate'
|
||||||
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
|
import CommandComboBox from '../CommandComboBox'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import CommandBarReview from './CommandBarReview'
|
||||||
|
|
||||||
|
type CommandsContextType = {
|
||||||
|
commandBarState: StateFrom<typeof commandBarMachine>
|
||||||
|
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandsContext = createContext<CommandsContextType>({
|
||||||
|
commandBarState: commandBarMachine.initialState,
|
||||||
|
commandBarSend: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CommandBarProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||||
|
guards: {
|
||||||
|
'Arguments are ready': (context, _) => {
|
||||||
|
return context.selectedCommand?.args
|
||||||
|
? context.argumentsToSubmit.length ===
|
||||||
|
Object.keys(context.selectedCommand.args)?.length
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
'Command has no arguments': (context, _event) => {
|
||||||
|
return (
|
||||||
|
!context.selectedCommand?.args ||
|
||||||
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the command bar when navigating
|
||||||
|
useEffect(() => {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandsContext.Provider
|
||||||
|
value={{
|
||||||
|
commandBarState,
|
||||||
|
commandBarSend,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CommandBar />
|
||||||
|
</CommandsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandBar = () => {
|
||||||
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { selectedCommand, currentArgument, commands },
|
||||||
|
} = commandBarState
|
||||||
|
const isSelectionArgument = currentArgument?.inputType === 'selection'
|
||||||
|
const WrapperComponent = isSelectionArgument ? Popover : Dialog
|
||||||
|
|
||||||
|
useHotkeys(['mod+k', 'mod+/'], () => {
|
||||||
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
if (commandBarState.matches('Closed')) {
|
||||||
|
commandBarSend({ type: 'Open' })
|
||||||
|
} else {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function stepBack() {
|
||||||
|
if (!currentArgument) {
|
||||||
|
if (commandBarState.matches('Review')) {
|
||||||
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
|
||||||
|
commandBarSend({
|
||||||
|
type: commandBarState.matches('Review')
|
||||||
|
? 'Edit argument'
|
||||||
|
: 'Change current argument',
|
||||||
|
data: {
|
||||||
|
arg: {
|
||||||
|
name: entries[entries.length - 1][0],
|
||||||
|
...entries[entries.length - 1][1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
commandBarSend({ type: 'Deselect command' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
const index = entries.findIndex(
|
||||||
|
([key, _]) => key === currentArgument.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
commandBarSend({ type: 'Deselect command' })
|
||||||
|
} else {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Change current argument',
|
||||||
|
data: {
|
||||||
|
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root
|
||||||
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
afterLeave={() => {
|
||||||
|
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||||
|
commandBarSend({ type: 'Clear' })
|
||||||
|
}}
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<WrapperComponent
|
||||||
|
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||||
|
onClose={() => {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||||
|
(isSelectionArgument ? 'pointer-events-none' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<WrapperComponent.Panel
|
||||||
|
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
{commandBarState.matches('Selecting command') ? (
|
||||||
|
<CommandComboBox options={commands} />
|
||||||
|
) : commandBarState.matches('Gathering arguments') ? (
|
||||||
|
<CommandBarArgument stepBack={stepBack} />
|
||||||
|
) : (
|
||||||
|
commandBarState.matches('Review') && (
|
||||||
|
<CommandBarReview stepBack={stepBack} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</WrapperComponent.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</WrapperComponent>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarProvider
|