Compare commits
438 Commits
v0.0.3
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
a73e3ae1b7 | |||
fc38fff327 | |||
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 | |||
86d40c964f | |||
2604449239 | |||
e992a96d3b | |||
22c4406105 | |||
ad3f0fda6a | |||
cccedceea0 | |||
ed68a34560 | |||
00ee913e3f | |||
46cc67e2db | |||
ff1be34f54 | |||
848bf61277 | |||
043333d3bc | |||
19d90b8081 | |||
4837c52908 | |||
afcf820bdd | |||
18959510f8 | |||
798cbe968a | |||
9cbc088ba3 | |||
2693a5609b | |||
3507da7b39 | |||
56cfb6d1f0 | |||
2b974ef1de | |||
253f1992fd | |||
76d3794b45 | |||
e52c8c9db6 | |||
eb48d51309 | |||
f3274e03ff | |||
46937199a3 | |||
e2a4798c2f | |||
659e6d5b45 | |||
1fbd0ad675 | |||
743ea1af4d | |||
2b1a556b81 | |||
853389ba22 | |||
023af60781 | |||
18db6f2dc1 | |||
4afec15323 | |||
152108f7a5 | |||
32d928ae0c | |||
6f0fae625f | |||
9bc47cf14a | |||
eea47aae1e | |||
25b9b4cf98 | |||
0f3f0b3b68 | |||
33eb6126d4 | |||
dccb83f614 | |||
b56a3398ad | |||
11658e2ff5 | |||
de255acc59 | |||
d33ddb2f1b | |||
a0730ded4e | |||
afd2b507ef | |||
8983a8231b | |||
9dd708db5d | |||
c25dd1800c | |||
e56e7ba0fa | |||
dbe4e7faa6 | |||
148e125dd7 | |||
75bb91c7e1 | |||
6809a46b6a | |||
965d2b23cf | |||
25392824cb | |||
43b1272538 | |||
24268fa744 | |||
63dd417d33 | |||
a8d23f5c0d | |||
6ea3a37e8a | |||
01f95d4ace | |||
3b9094e0dd | |||
a6d0f17970 | |||
108827075d | |||
aa24b9d6bd | |||
ff08c30ddc | |||
ea35db506b | |||
81a744faa5 | |||
7866686a1d | |||
19761baba6 |
3
.codespellrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[codespell]
|
||||||
|
ignore-words-list: crate,everytime
|
||||||
|
skip: **/target,node_modules,build
|
@ -1,3 +1,6 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
VITE_KC_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_CONNECTION_TIMEOUT_MS=5000
|
||||||
|
VITE_KC_SENTRY_DSN=
|
||||||
|
@ -1,3 +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://kittycad.io
|
||||||
|
VITE_KC_SKIP_AUTH=false
|
||||||
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||||
|
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
src/wasm-lib/*
|
23
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'npm' # See documentation for possible values
|
||||||
|
directory: '/' # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||||
|
directory: '/' # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||||
|
directory: '/src/wasm-lib/' # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||||
|
directory: '/src-tauri/' # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
81
.github/workflows/build.yml
vendored
@ -1,81 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-latest, ubuntu-20.04, windows-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: install ubuntu system dependencies
|
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
|
||||||
|
|
||||||
- run: yarn install
|
|
||||||
|
|
||||||
- name: Rust setup
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Rust cache
|
|
||||||
uses: swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src-tauri -> target'
|
|
||||||
|
|
||||||
- name: wasm prep
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
|
||||||
npx wasm-pack build --target web --out-dir pkg
|
|
||||||
cd ../../
|
|
||||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
|
||||||
|
|
||||||
- name: macos sed
|
|
||||||
if: matrix.os == 'macos-latest'
|
|
||||||
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
|
|
||||||
run: yarn fmt
|
|
||||||
|
|
||||||
- name: Build the app for the current platform (no upload)
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
with:
|
|
||||||
path: src-tauri/target/release/bundle
|
|
||||||
name: modeling-app_macos_linux_windows
|
|
||||||
|
|
||||||
- name: Build the app for the current platform and upload to release
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
releaseId: ${{ github.event.release.id }}
|
|
50
.github/workflows/cargo-build.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-build.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-build.yml
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo build
|
||||||
|
jobs:
|
||||||
|
cargobuild:
|
||||||
|
name: cargo build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src/wasm-lib']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
if: matrix.dir == 'src-tauri'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
- name: Run cargo build
|
||||||
|
run: |
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo build --all
|
||||||
|
shell: bash
|
60
.github/workflows/cargo-clippy.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- '**.rs'
|
||||||
|
- .github/workflows/cargo-clippy.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- '**.rs'
|
||||||
|
- .github/workflows/cargo-build.yml
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo clippy
|
||||||
|
jobs:
|
||||||
|
cargoclippy:
|
||||||
|
name: cargo clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src/wasm-lib']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: clippy
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
if: matrix.dir == 'src-tauri'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Rust Cache
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
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
|
||||||
|
|
48
.github/workflows/cargo-fmt.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- '**.rs'
|
||||||
|
- .github/workflows/cargo-fmt.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- '**.rs'
|
||||||
|
- .github/workflows/cargo-fmt.yml
|
||||||
|
permissions:
|
||||||
|
packages: read
|
||||||
|
contents: read
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo fmt
|
||||||
|
jobs:
|
||||||
|
cargofmt:
|
||||||
|
name: cargo fmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src/wasm-lib', 'src-tauri']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
- name: Run cargo fmt
|
||||||
|
run: |
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo fmt -- --check
|
||||||
|
shell: bash
|
65
.github/workflows/cargo-test.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions: read-all
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo test
|
||||||
|
jobs:
|
||||||
|
cargotest:
|
||||||
|
name: cargo test
|
||||||
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src/wasm-lib']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- name: install dependencies
|
||||||
|
if: matrix.dir == 'src-tauri'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||||
|
- uses: taiki-e/install-action@nextest
|
||||||
|
- name: Rust Cache
|
||||||
|
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
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo nextest run --workspace --no-fail-fast -P ci
|
||||||
|
env:
|
||||||
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
|
|
381
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
release:
|
||||||
|
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:
|
||||||
|
check-format:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn install
|
||||||
|
- 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@v4
|
||||||
|
- 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:
|
||||||
|
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 simpleserver:ci
|
||||||
|
|
||||||
|
- run: yarn test:nowatch
|
||||||
|
|
||||||
|
- run: yarn test:cov
|
||||||
|
|
||||||
|
|
||||||
|
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.kittycad.io/releases/modeling-app/nightly/last_update.json' \
|
||||||
|
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
package.json
|
||||||
|
src-tauri/tauri.conf.json
|
||||||
|
src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- id: export_version
|
||||||
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
|
build-test-apps:
|
||||||
|
needs: [prepare-json-files]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: Copy updated .json files
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
ls -l artifact
|
||||||
|
cp artifact/package.json package.json
|
||||||
|
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
||||||
|
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
|
- name: Install ubuntu system dependencies
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: >
|
||||||
|
sudo apt-get update &&
|
||||||
|
sudo apt-get install -y
|
||||||
|
libgtk-3-dev
|
||||||
|
libgtksourceview-3.0-dev
|
||||||
|
webkit2gtk-4.0
|
||||||
|
libappindicator3-dev
|
||||||
|
webkit2gtk-driver
|
||||||
|
xvfb
|
||||||
|
|
||||||
|
- name: Sync node version and setup cache
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||||
|
|
||||||
|
- run: yarn install
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src-tauri -> target'
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
|
- name: Run build:wasm manually
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
|
||||||
|
run: |
|
||||||
|
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||||
|
echo "building with ${{ env.MODE }}"
|
||||||
|
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
|
||||||
|
cd ../../
|
||||||
|
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||||
|
|
||||||
|
- name: Fix format
|
||||||
|
run: yarn fmt
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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:
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||||
|
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||||
|
with:
|
||||||
|
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||||
|
|
||||||
|
- name: Install tauri-driver for e2e tests (linux only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: tauri-driver
|
||||||
|
|
||||||
|
- name: Run e2e tests (linux only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: xvfb-run yarn test:e2e
|
||||||
|
env:
|
||||||
|
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||||
|
|
||||||
|
|
||||||
|
publish-apps-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||||
|
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
|
||||||
|
env:
|
||||||
|
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' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: Generate the update static endpoint
|
||||||
|
run: |
|
||||||
|
ls -l artifact/*/*itty*
|
||||||
|
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||||
|
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||||
|
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||||
|
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||||
|
jq --null-input \
|
||||||
|
--arg version "${VERSION}" \
|
||||||
|
--arg pub_date "${PUB_DATE}" \
|
||||||
|
--arg notes "${NOTES}" \
|
||||||
|
--arg darwin_sig "$DARWIN_SIG" \
|
||||||
|
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
||||||
|
--arg linux_sig "$LINUX_SIG" \
|
||||||
|
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||||
|
--arg windows_sig "$WINDOWS_SIG" \
|
||||||
|
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||||
|
'{
|
||||||
|
"version": $version,
|
||||||
|
"pub_date": $pub_date,
|
||||||
|
"notes": $notes,
|
||||||
|
"platforms": {
|
||||||
|
"darwin-x86_64": {
|
||||||
|
"signature": $darwin_sig,
|
||||||
|
"url": $darwin_url
|
||||||
|
},
|
||||||
|
"darwin-aarch64": {
|
||||||
|
"signature": $darwin_sig,
|
||||||
|
"url": $darwin_url
|
||||||
|
},
|
||||||
|
"linux-x86_64": {
|
||||||
|
"signature": $linux_sig,
|
||||||
|
"url": $linux_url
|
||||||
|
},
|
||||||
|
"windows-x86_64": {
|
||||||
|
"signature": $windows_sig,
|
||||||
|
"url": $windows_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > last_update.json
|
||||||
|
cat last_update.json
|
||||||
|
|
||||||
|
- name: Generate the download static endpoint
|
||||||
|
run: |
|
||||||
|
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
||||||
|
jq --null-input \
|
||||||
|
--arg version "${VERSION}" \
|
||||||
|
--arg pub_date "${PUB_DATE}" \
|
||||||
|
--arg notes "${NOTES}" \
|
||||||
|
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||||
|
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
|
||||||
|
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${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
|
||||||
|
uses: 'google-github-actions/auth@v1.1.1'
|
||||||
|
with:
|
||||||
|
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||||
|
|
||||||
|
- name: Set up Google Cloud SDK
|
||||||
|
uses: google-github-actions/setup-gcloud@v1.1.1
|
||||||
|
with:
|
||||||
|
project_id: kittycadapi
|
||||||
|
|
||||||
|
- name: Upload release files to public bucket
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
|
with:
|
||||||
|
path: artifact
|
||||||
|
glob: '*/*itty*'
|
||||||
|
parent: false
|
||||||
|
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||||
|
|
||||||
|
- name: Upload update endpoint to public bucket
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
|
with:
|
||||||
|
path: last_update.json
|
||||||
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
|
- name: Upload download endpoint to public bucket
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
|
with:
|
||||||
|
path: last_download.json
|
||||||
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
|
- name: Upload release files to Github
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: artifact/*/*itty*
|
16
.github/workflows/format.yml
vendored
@ -1,16 +0,0 @@
|
|||||||
# on pull requests, setup node, run `yarn prettier --check`
|
|
||||||
|
|
||||||
name: Check formatting
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: yarn install
|
|
||||||
- run: yarn fmt-check
|
|
21
.github/workflows/test.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
# on pull requests, setup node, run `yarn install` and `yarn test:nowatch`
|
|
||||||
|
|
||||||
name: Test
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: yarn install
|
|
||||||
- run: yarn build:wasm
|
|
||||||
- run: yarn tsc
|
|
||||||
- run: yarn simpleserver:ci
|
|
||||||
- run: yarn test:nowatch
|
|
||||||
- run: yarn test:cov
|
|
||||||
- run: yarn test:rust
|
|
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
|
||||||
|
8
.gitignore
vendored
@ -22,6 +22,14 @@ 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/kcl/bindings
|
||||||
public/wasm_lib_bg.wasm
|
public/wasm_lib_bg.wasm
|
||||||
|
src/wasm-lib/lcov.info
|
||||||
|
@ -5,3 +5,8 @@ coverage
|
|||||||
# Ignore Rust projects:
|
# Ignore Rust projects:
|
||||||
*.rs
|
*.rs
|
||||||
target
|
target
|
||||||
|
src/wasm-lib/pkg
|
||||||
|
src/wasm-lib/kcl/bindings
|
||||||
|
|
||||||
|
# XState generated files
|
||||||
|
src/machines/modelingMachine.typegen.ts
|
||||||
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2023 The KittyCAD Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
149
README.md
@ -1,55 +1,110 @@
|
|||||||
## Kurt demo project
|

|
||||||
|
|
||||||
live at [untitled-app.kittycad.io](https://untitled-app.kittycad.io/)
|
## KittyCAD 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.kittycad.io](https://app.kittycad.io/)
|
||||||
|
|
||||||
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 [KittyCAD team](https://kittycad.io).
|
||||||
|
|
||||||
Originally Presented on 10/01/2023
|
The KittyCAD 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 KittyCAD 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
|
KittyCAD 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 KittyCAD 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
|
## Developing in Chrome
|
||||||
```
|
|
||||||
yarn simpleserver
|
Chrome is in the process of rolling out a new default which
|
||||||
```
|
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
||||||
in one terminal
|
If you're having trouble logging into the `modeling-app`, you may need to
|
||||||
and
|
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
|
||||||
|
Third-Party Cookies".
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
First, start the dev server following "Running a development build" above.
|
||||||
|
|
||||||
|
Then in another terminal tab, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn test
|
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.)
|
||||||
|
|
||||||
@ -58,22 +113,66 @@ 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).
|
||||||
|
BIN
app-icon.png
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 207 KiB |
24399
docs/kcl/std.json
Normal file
4709
docs/kcl/std.md
Normal file
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).
|
11
e2e/tauri/specs/signin.e2e.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
describe('Modeling App', () => {
|
||||||
|
it('open the sign in page', async () => {
|
||||||
|
const button = await $('#signin')
|
||||||
|
expect(button).toHaveText('Sign in')
|
||||||
|
|
||||||
|
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||||
|
await button.waitForClickable()
|
||||||
|
await browser.execute('arguments[0].click();', button)
|
||||||
|
// TODO: handle auth
|
||||||
|
})
|
||||||
|
})
|
@ -11,6 +11,7 @@
|
|||||||
/>
|
/>
|
||||||
<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>
|
||||||
<title>KittyCAD Modeling App</title>
|
<title>KittyCAD Modeling App</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="body-bg">
|
<body class="body-bg">
|
||||||
|
85
package.json
@ -1,27 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.0.3",
|
"version": "0.11.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@codemirror/autocomplete": "^6.10.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
"@fortawesome/free-brands-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.13",
|
||||||
"@kittycad/lib": "^0.0.27",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
|
"@kittycad/lib": "^0.0.45",
|
||||||
|
"@lezer/javascript": "^1.4.7",
|
||||||
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@replit/codemirror-interact": "^6.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",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
"@uiw/react-codemirror": "^4.21.20",
|
||||||
"@uiw/react-codemirror": "^4.15.1",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"crypto-js": "^4.1.1",
|
"@xstate/react": "^3.2.2",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"debounce-promise": "^3.1.2",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"re-resizable": "^6.9.9",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
|
"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",
|
||||||
@ -31,16 +43,20 @@
|
|||||||
"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",
|
||||||
"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-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",
|
||||||
"zustand": "^4.1.4"
|
"xstate": "^4.38.2",
|
||||||
|
"zustand": "^4.4.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@ -48,17 +64,21 @@
|
|||||||
"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 && cargo clippy)",
|
"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": "wdio run wdio.conf.js",
|
||||||
"simpleserver": "http-server ./public --cors -p 3000",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src",
|
"fmt": "prettier --write ./src",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src",
|
||||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
"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",
|
||||||
"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": "(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",
|
||||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg",
|
"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",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@ -83,28 +103,33 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.22.9",
|
"@babel/preset-env": "^7.22.9",
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.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/react-modal": "^3.16.0",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.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.0.3",
|
||||||
"@vitest/coverage-istanbul": "^0.34.1",
|
"@vitest/coverage-istanbul": "^0.34.1",
|
||||||
"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.11.0",
|
||||||
"happy-dom": "^10.8.0",
|
"happy-dom": "^10.8.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"postcss": "^8.4.19",
|
"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.2.4",
|
||||||
"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",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19",
|
||||||
|
"@wdio/cli": "^7.7.3",
|
||||||
|
"@wdio/local-runner": "^7.7.3",
|
||||||
|
"@wdio/mocha-framework": "^7.7.3",
|
||||||
|
"@wdio/spec-reporter": "^7.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
public/Icon/Icon/Projects/Create File.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 475 B |
3
public/Icon/Icon/Projects/Create Folder.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 469 B |
3
public/Icon/Icon/Projects/File.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 200 B |
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).
|
||||||
|
|
||||||
|
### KittyCAD Modeling App (KCMA)
|
||||||
|
|
||||||
|
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA 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 KCMA
|
||||||
|
|
||||||
|
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
||||||
|
|
||||||
|
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA 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!
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 15 KiB |
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.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
46
public/kcma-logomark.svg
Normal file
After Width: | Height: | Size: 16 KiB |
@ -1,26 +1,45 @@
|
|||||||
<svg width="316" height="75" viewBox="0 0 316 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M7.33449 67.7274V65.5747H3.02906V63.4219H0.876343V18.2149H3.02906V16.0622H5.18177V13.9095H7.33449V11.7568H9.4872V7.45137H11.6399V5.29866H13.7926V3.14594H15.9453V0.993229H18.0981V3.14594H20.2508V5.29866H22.4035V7.45137H24.5562V9.60409H31.0143V7.45137H33.1671V5.29866H35.3198V3.14594H37.4725V0.993229H39.6252V3.14594H41.7779V5.29866H43.9306V7.45137H46.0833V11.7568H48.2361V13.9095H50.3888V16.0622H52.5415V18.2149H54.6942V63.4219H52.5415V65.5747H48.2361V67.7274H41.7779V69.8801H43.9306V74.1855H31.0143V69.8801H33.1671V67.7274H22.4035V69.8801H24.5562V74.1855H11.6399V69.8801H13.7926V67.7274H7.33449Z" fill="#101412"/>
|
<path d="M2.52 26.04V25.2H0.84V24.36H0V6.72H0.84V5.88H1.68V5.04H2.52V4.2H3.36V3.36H17.64V4.2H18.48V5.04H19.32V5.88H20.16V6.72H21V24.36H20.16V25.2H18.48V26.04H15.96V26.88H16.8V28.56H11.76V26.88H12.6V26.04H8.4V26.88H9.24V28.56H4.2V26.88H5.04V26.04H2.52Z" fill="#101412"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5563 11.7568H31.0145V9.60409H33.1672V7.45137H35.3199V5.29866H37.4726V3.14594H39.6253V5.29866H41.778V7.45137H43.9307V11.7568H46.0835V13.9095H48.2361V16.0622H50.3888V18.2149H52.5415V20.3677V22.5204V50.5057V52.6584V54.8111V63.4219H48.2361V65.5747H39.6253V67.7274V69.8801H41.778V72.0328H33.1671V69.8801H35.3199V67.7274V65.5747H20.2509V67.7274V69.8801H22.4036V72.0328H13.7927V69.8801H15.9454V67.7274V65.5747H7.33454V63.4219H3.02911V54.8111V52.6584V50.5057V22.5204V20.3677V18.2149H5.18183V16.0622H7.33454V13.9095H9.48726V11.7568L9.48731 11.7568H11.64V7.45137H13.7927V5.29866H15.9455V3.14594H18.0982V5.29866H20.2509V7.45137H22.4036V9.60409H24.5563V11.7568ZM5.18191 56.9638V59.1165H15.9455V56.9638H5.18191ZM9.48734 61.2692V63.4219H15.9455V61.2692H9.48734ZM39.6253 59.1165V56.9638H41.7781V59.1165H39.6253ZM43.9308 56.9638V59.1165H46.0835V56.9638H43.9308ZM48.2362 59.1165V56.9638H50.3889V59.1165H48.2362ZM39.6253 61.2692V63.4219H46.0835V61.2692H39.6253ZM20.2509 59.1165H35.3199V63.4219H20.2509V59.1165Z" fill="#D0FF00"/>
|
<path d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
<path d="M3.02911 52.6584V50.5057H52.5415V52.6584H3.02911Z" fill="#B1E515"/>
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
<path d="M9.48725 26.8258V24.6731H46.0834V26.8258H48.2361V44.0475H46.0834V46.2002H9.48725V44.0475H7.33453V26.8258H9.48725Z" fill="#1F2320"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.839996 20.58V24.36H2.52V25.2H18.48V24.36H20.16V20.58H0.839996ZM7.56 24.36V22.68H13.44V24.36H7.56Z" fill="#9BADB7"/>
|
||||||
<path d="M35.3198 35.4367V26.8258H39.6252V35.4367H35.3198Z" fill="#D0FF00"/>
|
<path d="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
<path d="M24.5562 35.4367H31.0144V37.5894H28.8617V41.8948H35.3198V39.7421H37.4725V41.8948H35.3198V44.0475H20.2508V41.8948H18.0981V39.7421H20.2508V41.8948H26.709V37.5894H24.5562V35.4367Z" fill="#D0FF00"/>
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
<path d="M20.2508 33.2839V31.1312H13.7927V33.2839H11.64V31.1312H13.7927V28.9785H20.2508V31.1312H22.4035V33.2839H20.2508Z" fill="#D0FF00"/>
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
<path d="M48.2361 18.2149V16.0622H50.3888V18.2149H48.2361Z" fill="#92C51B"/>
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
<path d="M7.33448 18.2149V16.0622H5.18176V18.2149H7.33448Z" fill="#92C51B"/>
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
<path d="M46.0834 16.0622V13.9095H48.2361V16.0622H46.0834Z" fill="#92C51B"/>
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
<path d="M9.48725 16.0622V13.9095H7.33453V16.0622H9.48725Z" fill="#92C51B"/>
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
<rect x="26.709" y="11.7568" width="2.15271" height="4.30543" fill="#B1E515"/>
|
<path d="M18.48 5.88V5.04H17.64V4.2H3.36V5.04H2.52V5.88H1.68V6.72H0.839996V8.4H20.16V6.72H19.32V5.88H18.48Z" fill="#FBF580"/>
|
||||||
<path d="M35.3197 16.0622V13.9095H37.4725V11.7568H39.6252V13.9095H41.7779V16.0622H35.3197Z" fill="#101412"/>
|
<path d="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
<path d="M15.9453 13.9095V11.7568H18.098V13.9095H20.2507V16.0622H13.7926V13.9095H15.9453Z" fill="#101412"/>
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
<path d="M9.48718 52.6584V50.5057H15.9453V52.6584H9.48718Z" fill="#92C51B"/>
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
<rect x="24.5562" y="11.7568" width="6.45814" height="2.15271" fill="#92C51B"/>
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
<path d="M77.4822 18.0099V33.8657L92.8664 17.1258L99.4091 22.077L86.9721 35.5161L103.535 58.0914H92.6306L81.0777 41.8231L77.4822 45.7133V58.0914H68.8175V18.0099H77.4822Z" fill="#FFFFFA"/>
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
<path d="M115.158 20.7213C115.158 22.0574 114.666 23.1969 113.684 24.14C112.741 25.0438 111.601 25.4957 110.265 25.4957C108.929 25.4957 107.809 25.0438 106.906 24.14C106.002 23.1969 105.55 22.0574 105.55 20.7213C105.55 19.3853 106.002 18.2457 106.906 17.3026C107.809 16.3595 108.929 15.888 110.265 15.888C111.601 15.888 112.741 16.3595 113.684 17.3026C114.666 18.2457 115.158 19.3853 115.158 20.7213ZM114.627 29.5039V58.0914H105.962V29.5039H114.627Z" fill="#FFFFFA"/>
|
<path d="M9.24 13.44H11.76V14.28H10.92V15.96H13.44V15.12H14.28V15.96H13.44V16.8H7.56V15.96H6.72V15.12H7.56V15.96H10.08V14.28H9.24V13.44Z" fill="#DBFF3C"/>
|
||||||
<path d="M133.871 59.0935C130.335 59.0935 127.407 58.1897 125.089 56.3821C122.809 54.5745 121.67 51.922 121.67 48.4247V36.636H117.603V29.9165H121.67V22.8433L130.276 21.4286V29.9165H136.052L137.938 36.636H130.276V47.128C130.276 48.5033 130.629 49.6429 131.337 50.5467C132.044 51.4112 133.066 51.8434 134.402 51.8434C134.913 51.8434 135.463 51.7648 136.052 51.6077C136.642 51.4505 137.231 51.2343 137.82 50.9593L140.355 57.0894C139.687 57.6395 138.705 58.1111 137.408 58.504C136.111 58.897 134.932 59.0935 133.871 59.0935Z" fill="#FFFFFA"/>
|
<path d="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
<path d="M156.465 59.0935C152.929 59.0935 150.001 58.1897 147.683 56.3821C145.404 54.5745 144.264 51.922 144.264 48.4247V36.636H140.197V29.9165H144.264V22.8433L152.87 21.4286V29.9165H158.646L160.532 36.636H152.87V47.128C152.87 48.5033 153.223 49.6429 153.931 50.5467C154.638 51.4112 155.66 51.8434 156.996 51.8434C157.507 51.8434 158.057 51.7648 158.646 51.6077C159.236 51.4505 159.825 51.2343 160.415 50.9593L162.949 57.0894C162.281 57.6395 161.299 58.1111 160.002 58.504C158.705 58.897 157.526 59.0935 156.465 59.0935Z" fill="#FFFFFA"/>
|
<path d="M3.36 5.88V5.04H4.2V3.36H5.04V1.68H5.88V0.839996H6.72V1.68H7.56V3.36H8.4V5.04H9.24V5.88H3.36Z" fill="#DBFF3C"/>
|
||||||
<path d="M172.163 59.0345L173.165 56.6178L162.791 30.5649L171.515 29.5039C172.576 32.3332 173.637 35.1625 174.698 37.9917C175.759 40.821 176.8 43.6503 177.822 46.4796L183.834 29.5039H192.793L180.062 61.687C179.119 64.0054 177.488 65.9898 175.169 67.6403C172.851 69.33 170.375 70.4892 167.742 71.1179L164.736 64.1036C166.151 63.5535 167.625 62.8658 169.157 62.0406C170.69 61.2154 171.692 60.2134 172.163 59.0345Z" fill="#FFFFFA"/>
|
<path d="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
<path d="M203.975 58.0914L197.64 51.7563V21.3723L203.975 15.0371H223.072L229.438 21.3723V31.2748H220.735V25.6162L218.859 23.7402H208.219L206.343 25.6162V47.5124L208.219 49.3883H218.859L220.735 47.5124V41.8538H229.438V51.7563L223.072 58.0914H203.975Z" fill="#FFFFFA"/>
|
<path d="M13.44 1.68V0H15.96V1.68H16.8V3.36H17.64V4.62H16.8V3.36H15.96V1.68H15.12V0.84H14.28V1.68H13.44V3.36H12.6V4.62H11.76V3.36H12.6V1.68H13.44Z" fill="#92C51B"/>
|
||||||
<path d="M236.208 58.0914V21.3723L242.544 15.0371H262.41L268.745 21.3723V58.0914H260.073V45.4212H244.881V58.0914H236.208ZM244.881 36.7488H260.073V25.6162L258.197 23.7402H246.757L244.881 25.6162V36.7488Z" fill="#FFFFFA"/>
|
<path d="M5.04 1.68V0H7.56V1.68H8.4V3.36H9.24V4.62H8.4V3.36H7.56V1.68H6.72V0.84H5.88V1.68H5.04V3.36H4.2V4.62H3.36V3.36H4.2V1.68H5.04Z" fill="#92C51B"/>
|
||||||
<path d="M276.098 58.0914V15.0371H301.716L308.051 21.3723V51.7563L301.716 58.0914H276.098ZM284.802 49.3883H297.503L299.379 47.5124V25.6162L297.503 23.7402H284.802V49.3883Z" fill="#FFFFFA"/>
|
<rect x="9.24" y="5.03999" width="2.52" height="0.84" fill="#D0CC6A"/>
|
||||||
|
<path d="M13.44 5.88V5.04H14.28V4.2H15.12V5.04H15.96V5.88H13.44Z" fill="#76AA1D"/>
|
||||||
|
<path d="M5.88 5.04V4.2H6.72V5.04H7.56V5.88H5.04V5.04H5.88Z" fill="#76AA1D"/>
|
||||||
|
<path d="M17.64 5.88V5.04H16.8V4.2H17.64V5.04H18.48V5.88H17.64Z" fill="#76AA1D"/>
|
||||||
|
<path d="M3.36 5.04V5.88H2.52V5.04H3.36V4.2H4.2V5.04H3.36Z" fill="#76AA1D"/>
|
||||||
|
<path d="M8.4 4.2H9.24V5.04H10.08V5.88H9.24V5.04H8.4V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M11.76 4.2H12.6V5.04H11.76V5.88H10.92V5.04H11.76V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M14.28 10.92H13.44V13.44H14.28V10.92Z" fill="#92C51B"/>
|
||||||
|
<path d="M1.68 21V19.32H0.839996V21H1.68Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M19.32 21V19.32H20.16V21H19.32Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M3.36 20.16V19.32H5.88V20.16H3.36Z" fill="#D56161"/>
|
||||||
|
<path d="M3.36 21V20.16H5.88V21H3.36Z" fill="#AC3232"/>
|
||||||
|
<path d="M29.991 6.64V12.827L35.994 6.295L38.547 8.227L33.694 13.471L40.157 22.28H35.902L31.394 15.932L29.991 17.45V22.28H26.61V6.64H29.991Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M44.6921 7.698C44.6921 8.21933 44.5005 8.664 44.1171 9.032C43.7491 9.38466 43.3045 9.561 42.7831 9.561C42.2618 9.561 41.8248 9.38466 41.4721 9.032C41.1195 8.664 40.9431 8.21933 40.9431 7.698C40.9431 7.17666 41.1195 6.732 41.4721 6.364C41.8248 5.996 42.2618 5.812 42.7831 5.812C43.3045 5.812 43.7491 5.996 44.1171 6.364C44.5005 6.732 44.6921 7.17666 44.6921 7.698ZM44.4851 11.125V22.28H41.1041V11.125H44.4851Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M51.9943 22.671C50.6143 22.671 49.4719 22.3183 48.5673 21.613C47.6779 20.9077 47.2333 19.8727 47.2333 18.508V13.908H45.6463V11.286H47.2333V8.526L50.5913 7.974V11.286H52.8453L53.5813 13.908H50.5913V18.002C50.5913 18.5387 50.7293 18.9833 51.0053 19.336C51.2813 19.6733 51.6799 19.842 52.2013 19.842C52.4006 19.842 52.6153 19.8113 52.8453 19.75C53.0753 19.6887 53.3053 19.6043 53.5353 19.497L54.5243 21.889C54.2636 22.1037 53.8803 22.2877 53.3743 22.441C52.8683 22.5943 52.4083 22.671 51.9943 22.671Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M60.8106 22.671C59.4306 22.671 58.2883 22.3183 57.3836 21.613C56.4943 20.9077 56.0496 19.8727 56.0496 18.508V13.908H54.4626V11.286H56.0496V8.526L59.4076 7.974V11.286H61.6616L62.3976 13.908H59.4076V18.002C59.4076 18.5387 59.5456 18.9833 59.8216 19.336C60.0976 19.6733 60.4963 19.842 61.0176 19.842C61.217 19.842 61.4316 19.8113 61.6616 19.75C61.8916 19.6887 62.1216 19.6043 62.3516 19.497L63.3406 21.889C63.08 22.1037 62.6966 22.2877 62.1906 22.441C61.6846 22.5943 61.2246 22.671 60.8106 22.671Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M66.936 22.648L67.327 21.705L63.279 11.539L66.683 11.125C67.097 12.229 67.511 13.333 67.925 14.437C68.339 15.541 68.7453 16.645 69.144 17.749L71.49 11.125H74.986L70.018 23.683C69.65 24.5877 69.0137 25.362 68.109 26.006C67.2043 26.6653 66.2383 27.1177 65.211 27.363L64.038 24.626C64.59 24.4113 65.165 24.143 65.763 23.821C66.361 23.499 66.752 23.108 66.936 22.648Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M79.3491 22.28L76.8771 19.808V7.952L79.3491 5.48H86.8011L89.2851 7.952V11.816H85.8891V9.608L85.1571 8.876H81.0051L80.2731 9.608V18.152L81.0051 18.884H85.1571L85.8891 18.152V15.944H89.2851V19.808L86.8011 22.28H79.3491Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M91.9268 22.28V7.952L94.3988 5.48H102.151L104.623 7.952V22.28H101.239V17.336H95.3108V22.28H91.9268ZM95.3108 13.952H101.239V9.608L100.507 8.876H96.0428L95.3108 9.608V13.952Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M107.492 22.28V5.48H117.488L119.96 7.952V19.808L117.488 22.28H107.492ZM110.888 18.884H115.844L116.576 18.152V9.608L115.844 8.876H110.888V18.884Z" fill="#FFFFFA"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@ -1,26 +1,45 @@
|
|||||||
<svg width="788" height="183" viewBox="0 0 788 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M16.6075 166.835V161.454H5.84388V156.072H0.462097V43.0543H5.84388V37.6725H11.2257V32.2907H16.6075V26.9089H21.9892V16.1454H27.371V10.7636H32.7528V5.38179H38.1346V0H43.5164V5.38179H48.8982V10.7636H54.28V16.1454H59.6617V21.5271H75.8071V16.1454H81.1889V10.7636H86.5707V5.38179H91.9525V0H97.3342V5.38179H102.716V10.7636H108.098V16.1454H113.48V26.9089H118.861V32.2907H124.243V37.6725H129.625V43.0543H135.007V156.072H129.625V161.454H118.861V166.835H102.716V172.217H108.098V182.981H75.8071V172.217H81.1889V166.835H54.28V172.217H59.6617V182.981H27.371V172.217H32.7528V166.835H16.6075Z" fill="#101412"/>
|
<path d="M2.52 26.04V25.2H0.84V24.36H0V6.72H0.84V5.88H1.68V5.04H2.52V4.2H3.36V3.36H17.64V4.2H18.48V5.04H19.32V5.88H20.16V6.72H21V24.36H20.16V25.2H18.48V26.04H15.96V26.88H16.8V28.56H11.76V26.88H12.6V26.04H8.4V26.88H9.24V28.56H4.2V26.88H5.04V26.04H2.52Z" fill="#101412"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9892 26.9095L21.9892 26.9102V32.2919H16.6075V37.6737H11.2257V43.0555H5.84388V48.4364V53.8191V123.781V129.163V129.164V134.545V156.073H16.6075V161.455H38.1346V166.836V172.217H32.7528V177.599H54.28V172.217H48.8982V166.836V161.455H86.5707V166.836V172.217H81.1889V177.599H102.716V172.217H97.3342V166.836V161.455H118.861V156.073H129.625V134.545V129.164V129.163V123.781V53.8191V48.4364V43.0555H124.243V37.6737H118.861V32.2919H113.48V26.9102V26.9095H108.098V16.1459H102.716V10.7641H97.3342V5.38232H91.9525V10.7641H86.5707V16.1459H81.1889V21.5277H75.8071V26.9102H59.6617V21.5277H54.28V16.1459H48.8982V10.7641H43.5164V5.38232H38.1346V10.7641H32.7528V16.1459H27.371V26.9095H21.9892ZM11.2257 129.164H124.243V129.163H11.2257V129.164ZM11.2257 139.927V145.309H38.1346V139.927H11.2257ZM21.9893 150.691V156.072H38.1346V150.691H21.9893ZM97.3343 145.309V139.927H102.716V145.309H97.3343ZM108.098 139.927V145.309H113.48V139.927H108.098ZM118.861 145.309V139.927H124.243V145.309H118.861ZM97.3343 150.691V156.072H113.48V150.691H97.3343ZM48.8982 145.309H86.5707V156.073H48.8982V145.309Z" fill="#D0FF00"/>
|
<path d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
<path d="M5.84388 129.163V123.781H129.625V129.163H5.84388Z" fill="#B1E515"/>
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
<path d="M21.9892 64.5812V59.1995H113.48V64.5812H118.861V107.636H113.48V113.017H21.9892V107.636H16.6075V64.5812H21.9892Z" fill="#1F2320"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.839996 20.58V24.36H2.52V25.2H18.48V24.36H20.16V20.58H0.839996ZM7.56 24.36V22.68H13.44V24.36H7.56Z" fill="#9BADB7"/>
|
||||||
<path d="M86.5707 86.1092V64.582H97.3343V86.1092H86.5707Z" fill="#D0FF00"/>
|
<path d="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
<path d="M59.6617 86.1092H75.8071V91.491H70.4253V102.255H86.5707V96.8727H91.9525V102.255H86.5707V107.636H48.8982V102.255H43.5164V96.8727H48.8982V102.255H65.0435V91.491H59.6617V86.1092Z" fill="#D0FF00"/>
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
<path d="M48.8982 80.7274V75.3456H32.7528V80.7274H27.371V75.3456H32.7528V69.9638H48.8982V75.3456H54.28V80.7274H48.8982Z" fill="#D0FF00"/>
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
<path d="M118.861 43.0534V37.6716H124.243V43.0534H118.861Z" fill="#92C51B"/>
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
<path d="M16.6075 43.0534V37.6716H11.2257V43.0534H16.6075Z" fill="#92C51B"/>
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
<path d="M113.48 37.6728V32.291H118.861V37.6728H113.48Z" fill="#92C51B"/>
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
<path d="M21.9892 37.6728V32.291H16.6075V37.6728H21.9892Z" fill="#92C51B"/>
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
<rect x="65.0435" y="26.9087" width="5.38179" height="10.7636" fill="#B1E515"/>
|
<path d="M18.48 5.88V5.04H17.64V4.2H3.36V5.04H2.52V5.88H1.68V6.72H0.839996V8.4H20.16V6.72H19.32V5.88H18.48Z" fill="#FBF580"/>
|
||||||
<path d="M86.5707 37.6723V32.2905H91.9525V26.9087H97.3342V32.2905H102.716V37.6723H86.5707Z" fill="#101412"/>
|
<path d="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
<path d="M38.1346 32.2905V26.9087H43.5164V32.2905H48.8982V37.6723H32.7528V32.2905H38.1346Z" fill="#101412"/>
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
<path d="M21.9892 129.163V123.781H38.1346V129.163H21.9892Z" fill="#92C51B"/>
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
<rect x="59.6617" y="26.9087" width="16.1454" height="5.38179" fill="#92C51B"/>
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
<path d="M191.977 42.5414V82.1808L230.437 40.331L246.794 52.7091L215.701 86.3068L257.109 142.745H229.848L200.966 102.074L191.977 111.8V142.745H170.315V42.5414H191.977Z" fill="#101412"/>
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
<path d="M286.165 49.3199C286.165 52.66 284.937 55.5089 282.481 57.8666C280.124 60.1261 277.275 61.2559 273.935 61.2559C270.594 61.2559 267.795 60.1261 265.535 57.8666C263.276 55.5089 262.146 52.66 262.146 49.3199C262.146 45.9797 263.276 43.1308 265.535 40.7731C267.795 38.4153 270.594 37.2365 273.935 37.2365C277.275 37.2365 280.124 38.4153 282.481 40.7731C284.937 43.1308 286.165 45.9797 286.165 49.3199ZM284.839 71.2763V142.745H263.177V71.2763H284.839Z" fill="#101412"/>
|
<path d="M9.24 13.44H11.76V14.28H10.92V15.96H13.44V15.12H14.28V15.96H13.44V16.8H7.56V15.96H6.72V15.12H7.56V15.96H10.08V14.28H9.24V13.44Z" fill="#DBFF3C"/>
|
||||||
<path d="M332.949 145.25C324.108 145.25 316.789 142.991 310.993 138.472C305.295 133.953 302.446 127.322 302.446 118.578V89.1066H292.278V72.3078H302.446V54.6248L323.96 51.0882V72.3078H338.402L343.117 89.1066H323.96V115.336C323.96 118.775 324.845 121.624 326.613 123.883C328.381 126.044 330.935 127.125 334.276 127.125C335.553 127.125 336.928 126.929 338.402 126.536C339.875 126.143 341.349 125.602 342.822 124.915L349.159 140.24C347.489 141.615 345.033 142.794 341.791 143.777C338.549 144.759 335.602 145.25 332.949 145.25Z" fill="#101412"/>
|
<path d="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
<path d="M389.435 145.25C380.593 145.25 373.274 142.991 367.478 138.472C361.781 133.953 358.932 127.322 358.932 118.578V89.1066H348.764V72.3078H358.932V54.6248L380.446 51.0882V72.3078H394.887L399.602 89.1066H380.446V115.336C380.446 118.775 381.33 121.624 383.098 123.883C384.867 126.044 387.421 127.125 390.761 127.125C392.038 127.125 393.413 126.929 394.887 126.536C396.361 126.143 397.834 125.602 399.308 124.915L405.644 140.24C403.974 141.615 401.518 142.794 398.276 143.777C395.034 144.759 392.087 145.25 389.435 145.25Z" fill="#101412"/>
|
<path d="M3.36 5.88V5.04H4.2V3.36H5.04V1.68H5.88V0.839996H6.72V1.68H7.56V3.36H8.4V5.04H9.24V5.88H3.36Z" fill="#DBFF3C"/>
|
||||||
<path d="M428.679 145.103L431.184 139.061L405.249 73.9287L427.058 71.2763C429.711 78.3495 432.363 85.4227 435.016 92.4959C437.668 99.5691 440.272 106.642 442.826 113.715L457.856 71.2763H480.255L448.425 151.734C446.068 157.53 441.991 162.491 436.195 166.617C430.398 170.841 424.209 173.739 417.627 175.311L410.112 157.776C413.649 156.4 417.333 154.681 421.164 152.618C424.995 150.555 427.5 148.05 428.679 145.103Z" fill="#101412"/>
|
<path d="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
<path d="M508.208 142.745L492.371 126.907V50.9472L508.208 35.1094H555.953L571.867 50.9472V75.7034H550.109V61.557L545.42 56.8672H518.818L514.128 61.557V116.297L518.818 120.987H545.42L550.109 116.297V102.151H571.867V126.907L555.953 142.745H508.208Z" fill="#101412"/>
|
<path d="M13.44 1.68V0H15.96V1.68H16.8V3.36H17.64V4.62H16.8V3.36H15.96V1.68H15.12V0.84H14.28V1.68H13.44V3.36H12.6V4.62H11.76V3.36H12.6V1.68H13.44Z" fill="#92C51B"/>
|
||||||
<path d="M588.792 142.745V50.9472L604.63 35.1094H654.296L670.134 50.9472V142.745H648.453V111.069H610.473V142.745H588.792ZM610.473 89.3885H648.453V61.557L643.763 56.8672H615.163L610.473 61.557V89.3885Z" fill="#101412"/>
|
<path d="M5.04 1.68V0H7.56V1.68H8.4V3.36H9.24V4.62H8.4V3.36H7.56V1.68H6.72V0.84H5.88V1.68H5.04V3.36H4.2V4.62H3.36V3.36H4.2V1.68H5.04Z" fill="#92C51B"/>
|
||||||
<path d="M688.517 142.745V35.1094H752.561L768.399 50.9472V126.907L752.561 142.745H688.517ZM710.275 120.987H742.028L746.718 116.297V61.557L742.028 56.8672H710.275V120.987Z" fill="#101412"/>
|
<rect x="9.24" y="5.03999" width="2.52" height="0.84" fill="#D0CC6A"/>
|
||||||
|
<path d="M13.44 5.88V5.04H14.28V4.2H15.12V5.04H15.96V5.88H13.44Z" fill="#76AA1D"/>
|
||||||
|
<path d="M5.88 5.04V4.2H6.72V5.04H7.56V5.88H5.04V5.04H5.88Z" fill="#76AA1D"/>
|
||||||
|
<path d="M17.64 5.88V5.04H16.8V4.2H17.64V5.04H18.48V5.88H17.64Z" fill="#76AA1D"/>
|
||||||
|
<path d="M3.36 5.04V5.88H2.52V5.04H3.36V4.2H4.2V5.04H3.36Z" fill="#76AA1D"/>
|
||||||
|
<path d="M8.4 4.2H9.24V5.04H10.08V5.88H9.24V5.04H8.4V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M11.76 4.2H12.6V5.04H11.76V5.88H10.92V5.04H11.76V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M14.28 10.92H13.44V13.44H14.28V10.92Z" fill="#92C51B"/>
|
||||||
|
<path d="M1.68 21V19.32H0.839996V21H1.68Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M19.32 21V19.32H20.16V21H19.32Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M3.36 20.16V19.32H5.88V20.16H3.36Z" fill="#D56161"/>
|
||||||
|
<path d="M3.36 21V20.16H5.88V21H3.36Z" fill="#AC3232"/>
|
||||||
|
<path d="M29.991 6.64V12.827L35.994 6.295L38.547 8.227L33.694 13.471L40.157 22.28H35.902L31.394 15.932L29.991 17.45V22.28H26.61V6.64H29.991Z" fill="#08110D"/>
|
||||||
|
<path d="M44.6921 7.698C44.6921 8.21933 44.5005 8.664 44.1171 9.032C43.7491 9.38466 43.3045 9.561 42.7831 9.561C42.2618 9.561 41.8248 9.38466 41.4721 9.032C41.1195 8.664 40.9431 8.21933 40.9431 7.698C40.9431 7.17666 41.1195 6.732 41.4721 6.364C41.8248 5.996 42.2618 5.812 42.7831 5.812C43.3045 5.812 43.7491 5.996 44.1171 6.364C44.5005 6.732 44.6921 7.17666 44.6921 7.698ZM44.4851 11.125V22.28H41.1041V11.125H44.4851Z" fill="#08110D"/>
|
||||||
|
<path d="M51.9943 22.671C50.6143 22.671 49.4719 22.3183 48.5673 21.613C47.6779 20.9077 47.2333 19.8727 47.2333 18.508V13.908H45.6463V11.286H47.2333V8.526L50.5913 7.974V11.286H52.8453L53.5813 13.908H50.5913V18.002C50.5913 18.5387 50.7293 18.9833 51.0053 19.336C51.2813 19.6733 51.6799 19.842 52.2013 19.842C52.4006 19.842 52.6153 19.8113 52.8453 19.75C53.0753 19.6887 53.3053 19.6043 53.5353 19.497L54.5243 21.889C54.2636 22.1037 53.8803 22.2877 53.3743 22.441C52.8683 22.5943 52.4083 22.671 51.9943 22.671Z" fill="#08110D"/>
|
||||||
|
<path d="M60.8106 22.671C59.4306 22.671 58.2883 22.3183 57.3836 21.613C56.4943 20.9077 56.0496 19.8727 56.0496 18.508V13.908H54.4626V11.286H56.0496V8.526L59.4076 7.974V11.286H61.6616L62.3976 13.908H59.4076V18.002C59.4076 18.5387 59.5456 18.9833 59.8216 19.336C60.0976 19.6733 60.4963 19.842 61.0176 19.842C61.217 19.842 61.4316 19.8113 61.6616 19.75C61.8916 19.6887 62.1216 19.6043 62.3516 19.497L63.3406 21.889C63.08 22.1037 62.6966 22.2877 62.1906 22.441C61.6846 22.5943 61.2246 22.671 60.8106 22.671Z" fill="#08110D"/>
|
||||||
|
<path d="M66.936 22.648L67.327 21.705L63.279 11.539L66.683 11.125C67.097 12.229 67.511 13.333 67.925 14.437C68.339 15.541 68.7453 16.645 69.144 17.749L71.49 11.125H74.986L70.018 23.683C69.65 24.5877 69.0137 25.362 68.109 26.006C67.2043 26.6653 66.2383 27.1177 65.211 27.363L64.038 24.626C64.59 24.4113 65.165 24.143 65.763 23.821C66.361 23.499 66.752 23.108 66.936 22.648Z" fill="#08110D"/>
|
||||||
|
<path d="M79.3491 22.28L76.8771 19.808V7.952L79.3491 5.48H86.8011L89.2851 7.952V11.816H85.8891V9.608L85.1571 8.876H81.0051L80.2731 9.608V18.152L81.0051 18.884H85.1571L85.8891 18.152V15.944H89.2851V19.808L86.8011 22.28H79.3491Z" fill="#08110D"/>
|
||||||
|
<path d="M91.9268 22.28V7.952L94.3988 5.48H102.151L104.623 7.952V22.28H101.239V17.336H95.3108V22.28H91.9268ZM95.3108 13.952H101.239V9.608L100.507 8.876H96.0428L95.3108 9.608V13.952Z" fill="#08110D"/>
|
||||||
|
<path d="M107.492 22.28V5.48H117.488L119.96 7.952V19.808L117.488 22.28H107.492ZM110.888 18.884H115.844L116.576 18.152V9.608L115.844 8.876H110.888V18.884Z" fill="#08110D"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
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 @@
|
|||||||
|
## KittyCAD 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)
|
||||||
|
|
1220
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,16 +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.3.0", features = [] }
|
tauri-build = { version = "1.5.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
oauth2 = "4.4.1"
|
kittycad = "0.2.41"
|
||||||
|
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 = ["dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||||
tokio = { version = "1.29.1", features = ["time"] }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
toml = "0.6.0"
|
tokio = { version = "1.33.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: 18 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 122 KiB |
@ -6,6 +6,7 @@ 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,7 +68,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Open the system browser with the auth_uri.
|
// Open the system browser with the auth_uri.
|
||||||
// We do this in the browser and not a seperate window because we want 1password and
|
// We do this in the browser and not a separate window because we want 1password and
|
||||||
// other crap to work well.
|
// other crap to work well.
|
||||||
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
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()))?;
|
||||||
@ -85,19 +86,66 @@ 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())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.0.3"
|
"version": "0.11.3"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -23,7 +23,8 @@
|
|||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"scope": [
|
"scope": [
|
||||||
"$HOME/**/*"
|
"$HOME/**/*",
|
||||||
|
"$APPDATA/**/*"
|
||||||
],
|
],
|
||||||
"all": true
|
"all": true
|
||||||
},
|
},
|
||||||
@ -35,8 +36,14 @@
|
|||||||
"https://api.dev.kittycad.io/*"
|
"https://api.dev.kittycad.io/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"os": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@ -54,7 +61,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"identifier": "KittyCAD-modeling-app",
|
"identifier": "io.kittycad.modeling-app",
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": null,
|
"entitlements": null,
|
||||||
@ -65,12 +72,7 @@
|
|||||||
},
|
},
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all",
|
"targets": "all"
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": null,
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
|
6
src-tauri/tauri.macos.conf.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"package": {
|
||||||
|
"productName": "KittyCAD Modeling"
|
||||||
|
}
|
||||||
|
}
|
21
src-tauri/tauri.release.conf.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"tauri": {
|
||||||
|
"updater": {
|
||||||
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"identifier": "io.kittycad.modeling-app",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "http://timestamp.digicert.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
src-tauri/tauri.windows.conf.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"package": {
|
||||||
|
"productName": "KittyCAD Modeling"
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +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 { BrowserRouter } from 'react-router-dom'
|
import { describe, test, vi } from 'vitest'
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
RouterProvider,
|
||||||
|
createMemoryRouter,
|
||||||
|
createRoutesFromElements,
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
|
import { BROWSER_FILE_NAME } from 'Router'
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
let listener: ((rect: any) => void) | undefined = undefined
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
;(global as any).ResizeObserver = class ResizeObserver {
|
||||||
@ -12,12 +22,52 @@ let listener: ((rect: any) => void) | undefined = undefined
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
describe('App tests', () => {
|
||||||
|
test('Renders the modeling app screen, including "Variables" pane.', () => {
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = (await vi.importActual('react-router-dom')) as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useParams: () => ({ id: BROWSER_FILE_NAME }),
|
||||||
|
useLoaderData: () => ({ code: null }),
|
||||||
|
}
|
||||||
|
})
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<TestWrap>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</TestWrap>
|
||||||
)
|
)
|
||||||
const linkElement = screen.getByText(/Variables/i)
|
const linkElement = screen.getByText(/Variables/i)
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
|
// We have to use a memory router in the testing environment,
|
||||||
|
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
|
||||||
|
// 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>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
initialEntries: ['/file/new'],
|
||||||
|
initialIndex: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return <RouterProvider router={router} />
|
||||||
|
}
|
||||||
|
453
src/App.tsx
@ -1,38 +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 { asyncLexer } from './lang/tokeniser'
|
import { PaneType, useStore } from './useStore'
|
||||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
|
||||||
import { _executor } from './lang/executor'
|
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
|
||||||
import { langs } from '@uiw/codemirror-extensions-langs'
|
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
|
||||||
import { ViewUpdate } from '@codemirror/view'
|
|
||||||
import {
|
|
||||||
lineHighlightField,
|
|
||||||
addLineHighlight,
|
|
||||||
} from './editor/highlightextension'
|
|
||||||
import { PaneType, Selections, Themes, useStore } from './useStore'
|
|
||||||
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 {
|
import { EngineCommand } from './lang/std/engineConnection'
|
||||||
EngineCommand,
|
import { throttle } from './lib/utils'
|
||||||
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,
|
||||||
@ -40,94 +18,43 @@ 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 { getSystemTheme } from './lib/getSystemTheme'
|
import { useLoaderData } from 'react-router-dom'
|
||||||
|
import { IndexLoaderData } from './Router'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { onboardingPaths } from 'routes/Onboarding'
|
||||||
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
|
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 streamRef = useRef<HTMLDivElement>(null)
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
|
|
||||||
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,
|
|
||||||
isMouseDownInStream,
|
|
||||||
fileId,
|
|
||||||
cmdId,
|
|
||||||
setCmdId,
|
|
||||||
token,
|
|
||||||
formatCode,
|
|
||||||
debugPanel,
|
|
||||||
theme,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
onboardingStatus,
|
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,
|
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
|
||||||
fileId: s.fileId,
|
|
||||||
cmdId: s.cmdId,
|
|
||||||
setCmdId: s.setCmdId,
|
|
||||||
token: s.token,
|
|
||||||
formatCode: s.formatCode,
|
|
||||||
debugPanel: s.debugPanel,
|
|
||||||
addKCLError: s.addKCLError,
|
|
||||||
theme: s.theme,
|
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
onboardingStatus: s.onboardingStatus,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const { settings } = useGlobalStateContext()
|
||||||
|
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
||||||
|
settings?.context || {}
|
||||||
|
const { state, send } = useModelingContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
// Pane toggling keyboard shortcuts
|
// Pane toggling keyboard shortcuts
|
||||||
@ -143,240 +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 === 'camera'
|
(p) => p === onboardingStatus
|
||||||
|
)
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: isMouseDownInStream
|
: didDragInStream
|
||||||
? 'opacity-40'
|
? 'opacity-40'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
useEngineConnectionSubscriptions()
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
|
||||||
setCode(value)
|
|
||||||
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 pixelDensity = window.devicePixelRatio
|
|
||||||
const streamWidth = streamRef?.current?.offsetWidth
|
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
|
||||||
|
|
||||||
const width = streamWidth ? streamWidth * pixelDensity : 0
|
|
||||||
const quadWidth = Math.round(width / 4) * 4
|
|
||||||
const height = streamHeight ? streamHeight * pixelDensity : 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
|
|
||||||
const asyncWrap = async () => {
|
|
||||||
try {
|
|
||||||
if (!code) {
|
|
||||||
setAst(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tokens = await asyncLexer(code)
|
|
||||||
const _ast = abstractSyntaxTree(tokens)
|
|
||||||
setAst(_ast)
|
|
||||||
resetLogs()
|
|
||||||
resetKCLErrors()
|
|
||||||
if (engineCommandManager) {
|
|
||||||
engineCommandManager.endSession()
|
|
||||||
engineCommandManager.startNewSession()
|
|
||||||
}
|
|
||||||
if (!engineCommandManager) return
|
|
||||||
const programMemory = await _executor(
|
|
||||||
_ast,
|
|
||||||
{
|
|
||||||
root: {
|
|
||||||
log: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: (a: any) => {
|
|
||||||
addLog(a)
|
|
||||||
},
|
|
||||||
__meta: [
|
|
||||||
{
|
|
||||||
pathToNode: [],
|
|
||||||
sourceRange: [0, 0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
_0: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 0,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_90: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 90,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_180: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 180,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_270: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 270,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pendingMemory: {},
|
|
||||||
},
|
|
||||||
engineCommandManager,
|
|
||||||
{ bodyType: 'root' },
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { artifactMap, sourceRangeMap } =
|
|
||||||
await engineCommandManager.waitForAllCommands()
|
|
||||||
|
|
||||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
|
||||||
engineCommandManager.onHover((id) => {
|
|
||||||
if (!id) {
|
|
||||||
setHighlightRange([0, 0])
|
|
||||||
} else {
|
|
||||||
const sourceRange = sourceRangeMap[id]
|
|
||||||
setHighlightRange(sourceRange)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
engineCommandManager.onClick((selections) => {
|
|
||||||
if (!selections) {
|
|
||||||
setCursor2()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { id, type } = selections
|
|
||||||
setCursor2({ range: sourceRangeMap[id], type })
|
|
||||||
})
|
|
||||||
if (programMemory !== undefined) {
|
|
||||||
setProgramMemory(programMemory)
|
|
||||||
}
|
|
||||||
|
|
||||||
setError()
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof KCLError) {
|
|
||||||
addKCLError(e)
|
|
||||||
} else {
|
|
||||||
setError('problem')
|
|
||||||
console.log(e)
|
|
||||||
addLog(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncWrap()
|
|
||||||
}, [code, isStreamReady])
|
|
||||||
|
|
||||||
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,
|
|
||||||
currentTarget,
|
|
||||||
}) => {
|
|
||||||
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 ? 'pan' : 'rotate'
|
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
setCmdId(newCmdId)
|
if (buttonDownInStream === undefined) {
|
||||||
|
if (state.matches('Sketch.Line Tool')) {
|
||||||
if (cmdId && isMouseDownInStream) {
|
|
||||||
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,
|
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
@ -386,49 +114,76 @@ export function App() {
|
|||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
file_id: fileId,
|
})
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction,
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
cmd_id: newCmdId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraExtensions = useMemo(() => {
|
|
||||||
if (TEST) return []
|
|
||||||
return [
|
|
||||||
lintGutter(),
|
|
||||||
linter((_view) => {
|
|
||||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen overflow-hidden relative flex flex-col"
|
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, file }}
|
||||||
|
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 ' +
|
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||||
(isMouseDownInStream || onboardingStatus === 'camera'
|
(buttonDownInStream || onboardingStatus === 'camera'
|
||||||
? ' pointer-events-none '
|
? ' pointer-events-none '
|
||||||
: ' ') +
|
: ' ') +
|
||||||
paneOpacity
|
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={{
|
||||||
@ -436,37 +191,15 @@ export function App() {
|
|||||||
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col justify-between">
|
<div id="code-pane" className="h-full flex flex-col justify-between">
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
title="Code"
|
title="Code"
|
||||||
icon={faCode}
|
icon={faCode}
|
||||||
className="open:!mb-2"
|
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">
|
|
||||||
<CodeMirror
|
|
||||||
className="h-full"
|
|
||||||
value={code}
|
|
||||||
extensions={[
|
|
||||||
langs.javascript({ jsx: true }),
|
|
||||||
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
|
||||||
@ -491,13 +224,13 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<Stream className="absolute inset-0 z-0" />
|
||||||
{debugPanel && (
|
{showDebugPanel && (
|
||||||
<DebugPanel
|
<DebugPanel
|
||||||
title="Debug"
|
title="Debug (AST Explorer)"
|
||||||
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')}
|
||||||
/>
|
/>
|
||||||
|
34
src/Auth.tsx
@ -1,38 +1,12 @@
|
|||||||
import useSWR from 'swr'
|
|
||||||
import fetcher from './lib/fetcher'
|
|
||||||
import withBaseUrl from './lib/withBaseURL'
|
|
||||||
import { User, useStore } from './useStore'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { isTauri } from './lib/isTauri'
|
|
||||||
import Loading from './components/Loading'
|
import Loading from './components/Loading'
|
||||||
import { paths } from './Router'
|
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 { data: user, isLoading } = useSWR<
|
const { auth } = useGlobalStateContext()
|
||||||
User | Partial<{ error_code: string }>
|
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
||||||
>(withBaseUrl('/user'), fetcher)
|
|
||||||
const { token, setUser } = useStore((s) => ({
|
|
||||||
token: s.token,
|
|
||||||
setUser: s.setUser,
|
|
||||||
}))
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
useEffect(() => {
|
return isLoggingIn ? (
|
||||||
if (user && 'id' in user) setUser(user)
|
|
||||||
}, [user, setUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(isTauri() && !token) ||
|
|
||||||
(!isTauri() && !isLoading && !(user && 'id' in user))
|
|
||||||
) {
|
|
||||||
navigate(paths.SIGN_IN)
|
|
||||||
}
|
|
||||||
}, [user, token, navigate, isLoading])
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
253
src/Router.tsx
@ -3,8 +3,15 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
|
useLocation,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
matchRoutes,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
useNavigationType,
|
||||||
|
} from 'react-router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
import Onboarding, {
|
import Onboarding, {
|
||||||
@ -13,6 +20,63 @@ import Onboarding, {
|
|||||||
} from './routes/Onboarding'
|
} from './routes/Onboarding'
|
||||||
import SignIn from './routes/SignIn'
|
import SignIn from './routes/SignIn'
|
||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
|
import { isTauri } from './lib/isTauri'
|
||||||
|
import Home from './routes/Home'
|
||||||
|
import { FileEntry, readDir, readTextFile } from '@tauri-apps/api/fs'
|
||||||
|
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||||
|
import {
|
||||||
|
initializeProjectDirectory,
|
||||||
|
isProjectDirectory,
|
||||||
|
PROJECT_ENTRYPOINT,
|
||||||
|
} from './lib/tauriFS'
|
||||||
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
|
import { WasmErrBanner } from './components/WasmErrBanner'
|
||||||
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsMachine,
|
||||||
|
} from './machines/settingsMachine'
|
||||||
|
import { ContextFrom } from 'xstate'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
|
import * as Sentry from '@sentry/react'
|
||||||
|
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) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: VITE_KC_SENTRY_DSN,
|
||||||
|
// TODO(paultag): pass in the right env here.
|
||||||
|
// environment: "production",
|
||||||
|
integrations: [
|
||||||
|
new Sentry.BrowserTracing({
|
||||||
|
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||||
|
useEffect,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new Sentry.Replay(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for performance monitoring.
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
|
||||||
|
// TODO: Add in kittycad.io endpoints
|
||||||
|
tracePropagationTargets: ['localhost'],
|
||||||
|
|
||||||
|
// Capture Replay for 10% of all sessions,
|
||||||
|
// plus for 100% of sessions with an error
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -26,62 +90,213 @@ const prependRoutes =
|
|||||||
|
|
||||||
export const paths = {
|
export const paths = {
|
||||||
INDEX: '/',
|
INDEX: '/',
|
||||||
|
HOME: '/home',
|
||||||
|
FILE: '/file',
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
SIGN_IN: '/signin',
|
SIGN_IN: '/signin',
|
||||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||||
'/onboarding/'
|
'/onboarding'
|
||||||
) as typeof onboardingPaths,
|
) as typeof onboardingPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
export const BROWSER_FILE_NAME = 'new'
|
||||||
|
|
||||||
|
export type IndexLoaderData = {
|
||||||
|
code: string | null
|
||||||
|
project?: ProjectWithEntryPointMetadata
|
||||||
|
file?: FileEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||||
|
entrypointMetadata: Metadata
|
||||||
|
}
|
||||||
|
export type HomeLoaderData = {
|
||||||
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||||
|
|
||||||
|
const addGlobalContextToElements = (
|
||||||
|
routes: CreateBrowserRouterArg
|
||||||
|
): CreateBrowserRouterArg =>
|
||||||
|
routes.map((route) =>
|
||||||
|
'element' in route
|
||||||
|
? {
|
||||||
|
...route,
|
||||||
|
element: (
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{route.element}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: route
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
addGlobalContextToElements([
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
|
loader: () =>
|
||||||
|
isTauri()
|
||||||
|
? redirect(paths.HOME)
|
||||||
|
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 />}
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
id: paths.FILE,
|
||||||
loader: ({ request }) => {
|
loader: async ({
|
||||||
const store = localStorage.getItem('store')
|
request,
|
||||||
if (store === null) {
|
params,
|
||||||
return redirect(paths.ONBOARDING.INDEX)
|
}): Promise<IndexLoaderData | Response> => {
|
||||||
} else {
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
const status = JSON.parse(store).state.onboardingStatus || ''
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
const notEnRouteToOnboarding =
|
ContextFrom<typeof settingsMachine>
|
||||||
!request.url.includes(paths.ONBOARDING.INDEX) &&
|
>
|
||||||
request.method === 'GET'
|
|
||||||
|
const status = persistedSettings.onboardingStatus || ''
|
||||||
|
const notEnRouteToOnboarding = !request.url.includes(
|
||||||
|
paths.ONBOARDING.INDEX
|
||||||
|
)
|
||||||
// '' is the initial state, 'done' and 'dismissed' are the final states
|
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||||
const hasValidOnboardingStatus =
|
const hasValidOnboardingStatus =
|
||||||
(status !== undefined && status.length === 0) ||
|
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||||
!(status === 'done' || status === 'dismissed')
|
|
||||||
const shouldRedirectToOnboarding =
|
const shouldRedirectToOnboarding =
|
||||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
if (shouldRedirectToOnboarding) {
|
||||||
return redirect(paths.ONBOARDING.INDEX + status)
|
return redirect(
|
||||||
|
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDir = persistedSettings.defaultDirectory || ''
|
||||||
|
|
||||||
|
if (params.id && params.id !== BROWSER_FILE_NAME) {
|
||||||
|
const decodedId = decodeURIComponent(params.id)
|
||||||
|
const projectAndFile = decodedId.replace(defaultDir + sep, '')
|
||||||
|
const firstSlashIndex = projectAndFile.indexOf(sep)
|
||||||
|
const projectName = projectAndFile.slice(0, firstSlashIndex)
|
||||||
|
const projectPath = defaultDir + sep + projectName
|
||||||
|
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
|
||||||
|
|
||||||
|
if (firstSlashIndex === -1 || !currentFileName)
|
||||||
|
return redirect(
|
||||||
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
|
`${params.id}${sep}${PROJECT_ENTRYPOINT}`
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
|
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 {
|
||||||
|
code,
|
||||||
|
project: {
|
||||||
|
name: projectName,
|
||||||
|
path: projectPath,
|
||||||
|
children,
|
||||||
|
entrypointMetadata,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
name: currentFileName,
|
||||||
|
path: params.id,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: paths.SETTINGS,
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.ONBOARDING.INDEX,
|
path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
|
||||||
element: <Onboarding />,
|
element: <Onboarding />,
|
||||||
children: onboardingRoutes,
|
children: onboardingRoutes,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: paths.HOME,
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<Outlet />
|
||||||
|
<Home />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
loader: async () => {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
||||||
|
}
|
||||||
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
|
ContextFrom<typeof settingsMachine>
|
||||||
|
>
|
||||||
|
const projectDir = await initializeProjectDirectory(
|
||||||
|
persistedSettings.defaultDirectory || ''
|
||||||
|
)
|
||||||
|
if (projectDir !== persistedSettings.defaultDirectory) {
|
||||||
|
localStorage.setItem(
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
...persistedSettings,
|
||||||
|
defaultDirectory: projectDir,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const projectsNoMeta = (await readDir(projectDir)).filter(
|
||||||
|
isProjectDirectory
|
||||||
|
)
|
||||||
|
const projects = await Promise.all(
|
||||||
|
projectsNoMeta.map(async (p: FileEntry) => ({
|
||||||
|
entrypointMetadata: await metadata(
|
||||||
|
p.path + sep + PROJECT_ENTRYPOINT
|
||||||
|
),
|
||||||
|
...p,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
|
element: <Settings />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: paths.SIGN_IN,
|
path: paths.SIGN_IN,
|
||||||
element: <SignIn />,
|
element: <SignIn />,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All routes in the app, used in src/index.tsx
|
* All routes in the app, used in src/index.tsx
|
||||||
|
106
src/Toolbar.module.css
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
.toolbarWrapper {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
@apply flex gap-4 items-center rounded-full;
|
||||||
|
@apply border border-cool-20/30 bg-cool-10/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toolbar {
|
||||||
|
@apply border-cool-100/50 bg-cool-120/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sketch) .toolbar {
|
||||||
|
@apply border-fern-20/20 bg-fern-10/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark .sketch) .toolbar {
|
||||||
|
@apply border-fern-120/50 bg-fern-100/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarCap {
|
||||||
|
@apply text-sm font-bold;
|
||||||
|
@apply bg-cool-20/50 text-cool-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toolbarCap {
|
||||||
|
@apply bg-cool-90/50 text-cool-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sketch) .toolbarCap {
|
||||||
|
@apply bg-fern-20/50 text-fern-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark .sketch) .toolbarCap {
|
||||||
|
@apply bg-fern-90/50 text-fern-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
@apply self-stretch flex items-center px-4 py-1;
|
||||||
|
@apply rounded-l-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popoverToggle {
|
||||||
|
@apply self-stretch m-0 flex items-center px-4 py-1;
|
||||||
|
@apply rounded-r-full border-none;
|
||||||
|
@apply hover:bg-cool-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButtons::-webkit-scrollbar {
|
||||||
|
@apply h-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButtons {
|
||||||
|
@apply flex items-center overflow-x-auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarButtons button {
|
||||||
|
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
@apply gap-1.5 p-0.5 pr-1;
|
||||||
|
@apply rounded-sm;
|
||||||
|
}
|
||||||
|
:global(.dark) .toolbarButtons button {
|
||||||
|
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
|
||||||
|
}
|
||||||
|
.toolbarButtons button:hover {
|
||||||
|
@apply text-cool-90 bg-cool-10;
|
||||||
|
}
|
||||||
|
:global(.sketch) .toolbarButtons button:hover {
|
||||||
|
@apply text-fern-90 bg-fern-10;
|
||||||
|
}
|
||||||
|
.toolbarButtons button:disabled {
|
||||||
|
@apply text-chalkboard-70 bg-chalkboard-30;
|
||||||
|
}
|
||||||
|
.toolbarButtons button:disabled:hover {
|
||||||
|
@apply !bg-inherit !text-inherit cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .toolbarButtons button {
|
||||||
|
@apply text-chalkboard-20 border-chalkboard-50;
|
||||||
|
}
|
||||||
|
:global(.dark) .toolbarButtons button:hover {
|
||||||
|
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
|
||||||
|
}
|
||||||
|
:global(.dark .sketch) .toolbarButtons button:hover {
|
||||||
|
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
|
||||||
|
}
|
||||||
|
:global(.dark) .toolbarButtons button:disabled {
|
||||||
|
@apply text-chalkboard-40 bg-chalkboard-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .popoverToggle {
|
||||||
|
@apply hover:bg-cool-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sketch) .popoverToggle {
|
||||||
|
@apply hover:bg-fern-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark .sketch) .popoverToggle {
|
||||||
|
@apply hover:bg-fern-90;
|
||||||
|
}
|
341
src/Toolbar.tsx
@ -1,180 +1,211 @@
|
|||||||
import { useStore, toolTips } from './useStore'
|
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
||||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
import styles from './Toolbar.module.css'
|
||||||
import { EqualLength } from './components/Toolbar/EqualLength'
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
import { EqualAngle } from './components/Toolbar/EqualAngle'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import { Intersect } from './components/Toolbar/Intersect'
|
import { engineCommandManager } from './lang/std/engineConnection'
|
||||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
|
||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
export const sketchButtonClassnames = {
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
background:
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
||||||
import { ExportButton } from './components/ExportButton'
|
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
||||||
|
}
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const { state, send, context } = useModelingContext()
|
||||||
setGuiMode,
|
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
|
||||||
guiMode,
|
const pathId = useMemo(
|
||||||
selectionRanges,
|
() =>
|
||||||
ast,
|
isCursorInSketchCommandRange(
|
||||||
updateAst,
|
engineCommandManager.artifactMap,
|
||||||
programMemory,
|
context.selectionRanges
|
||||||
} = useStore((s) => ({
|
),
|
||||||
guiMode: s.guiMode,
|
[engineCommandManager.artifactMap, context.selectionRanges]
|
||||||
setGuiMode: s.setGuiMode,
|
)
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||||
|
const span = toolbarButtonsRef.current
|
||||||
|
if (!span) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<span
|
||||||
<ExportButton />
|
ref={toolbarButtonsRef}
|
||||||
{guiMode.mode === 'default' && (
|
onWheel={handleToolbarButtonsWheelEvent}
|
||||||
<button
|
className={styles.toolbarButtons + ' ' + className}
|
||||||
onClick={() => {
|
|
||||||
setGuiMode({
|
|
||||||
mode: 'sketch',
|
|
||||||
sketchMode: 'selectFace',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{state.nextEvents.includes('Enter sketch') && (
|
||||||
|
<button
|
||||||
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
Start Sketch
|
Start Sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{guiMode.mode === 'canEditExtrude' && (
|
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
if (!ast) return
|
className="group"
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections[0].range
|
|
||||||
)
|
|
||||||
const { modifiedAst } = sketchOnExtrudedFace(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
programMemory
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
SketchOnFace
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setGuiMode({
|
|
||||||
mode: 'sketch',
|
|
||||||
sketchMode: 'sketchEdit',
|
|
||||||
pathToNode: guiMode.pathToNode,
|
|
||||||
rotation: guiMode.rotation,
|
|
||||||
position: guiMode.position,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
Edit Sketch
|
Edit Sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{guiMode.mode === 'canEditSketch' && (
|
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||||
<>
|
<button onClick={() => send({ type: 'Cancel' })} className="group">
|
||||||
<button
|
<ActionIcon icon="exit" className="!p-0.5" size="md" />
|
||||||
onClick={() => {
|
Exit Sketch
|
||||||
if (!ast) return
|
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections[0].range
|
|
||||||
)
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ExtrudeSketch
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!ast) return
|
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections[0].range
|
|
||||||
)
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ExtrudeSketch (w/o pipe)
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{guiMode.mode === 'sketch' && (
|
|
||||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
|
||||||
Exit sketch
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{toolTips
|
{state.matches('Sketch') && !state.matches('idle') && (
|
||||||
.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
|
<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
|
|
||||||
}
|
}
|
||||||
: {
|
className={
|
||||||
sketchMode: sketchFnName,
|
'group ' +
|
||||||
isTooltip: true,
|
(state.matches('Sketch.Line Tool')
|
||||||
}),
|
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||||
})
|
: '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{sketchFnName}
|
<ActionIcon icon="line" className="!p-0.5" size="md" />
|
||||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
Line
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{state.matches('Sketch') && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
state.matches('Sketch.Move Tool')
|
||||||
|
? send('CancelSketch')
|
||||||
|
: send('Equip move tool')
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
'group ' +
|
||||||
|
(state.matches('Sketch.Move Tool')
|
||||||
|
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon icon="move" className="!p-0.5" size="md" />
|
||||||
|
Move
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{state.matches('Sketch.SketchIdle') &&
|
||||||
|
state.nextEvents
|
||||||
|
.filter(
|
||||||
|
(eventName) =>
|
||||||
|
eventName.includes('Make segment') ||
|
||||||
|
eventName.includes('Constrain')
|
||||||
)
|
)
|
||||||
})}
|
.map((eventName) => (
|
||||||
<br></br>
|
<button
|
||||||
<ConvertToVariable />
|
key={eventName}
|
||||||
<HorzVert horOrVert="horizontal" />
|
onClick={() => send(eventName)}
|
||||||
<HorzVert horOrVert="vertical" />
|
className="group"
|
||||||
<EqualLength />
|
disabled={
|
||||||
<EqualAngle />
|
!state.nextEvents
|
||||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
.filter((event) => state.can(event as any))
|
||||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
.includes(eventName)
|
||||||
<SetAbsDistance buttonType="snapToYAxis" />
|
}
|
||||||
<SetAbsDistance buttonType="xAbs" />
|
title={eventName}
|
||||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
>
|
||||||
<SetAbsDistance buttonType="snapToXAxis" />
|
<ActionIcon
|
||||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
icon={'line'} // TODO
|
||||||
<SetAbsDistance buttonType="yAbs" />
|
bgClassName={sketchButtonClassnames.background}
|
||||||
<SetAngleLength angleOrLength="setAngle" />
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
<SetAngleLength angleOrLength="setLength" />
|
size="md"
|
||||||
<Intersect />
|
/>
|
||||||
<RemoveConstrainingValues />
|
{eventName
|
||||||
<SetAngleBetween />
|
.replace('Make segment ', '')
|
||||||
|
.replace('Constrain ', '')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{state.matches('idle') && (
|
||||||
|
<button
|
||||||
|
onClick={() => send('extrude intent')}
|
||||||
|
disabled={!state.can('extrude intent')}
|
||||||
|
className="group"
|
||||||
|
title={
|
||||||
|
state.can('extrude intent')
|
||||||
|
? 'extrude'
|
||||||
|
: 'sketches need to be closed, or not already extruded'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||||
|
Extrude
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
className={
|
||||||
|
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||||
|
{state.matches('Sketch') ? '2D' : '3D'}
|
||||||
|
</span>
|
||||||
|
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||||
|
<ToolbarButtons />
|
||||||
|
</menu>
|
||||||
|
<Popover.Button
|
||||||
|
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
|
</Popover.Button>
|
||||||
</div>
|
</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 {state.matches('Sketch') ? '2D' : '3D'}
|
||||||
|
</p>
|
||||||
|
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||||
|
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||||
|
</Popover.Button>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<ToolbarButtons className="flex-wrap" />
|
||||||
|
</section>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,94 @@
|
|||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import type { LinkProps } from 'react-router-dom'
|
||||||
|
|
||||||
interface ActionButtonProps extends React.PropsWithChildren {
|
interface BaseActionButtonProps {
|
||||||
icon?: ActionIconProps
|
icon?: ActionIconProps
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
|
||||||
to?: string
|
|
||||||
Element?:
|
|
||||||
| 'button'
|
|
||||||
| 'link'
|
|
||||||
| React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionButton = ({
|
type ActionButtonAsButton = BaseActionButtonProps &
|
||||||
icon,
|
Omit<
|
||||||
className,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
onClick,
|
keyof BaseActionButtonProps
|
||||||
to = paths.INDEX,
|
> & {
|
||||||
Element = 'button',
|
Element: 'button'
|
||||||
children,
|
}
|
||||||
...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 ${
|
|
||||||
icon ? 'pr-2' : 'px-2'
|
|
||||||
} ${className}`
|
|
||||||
|
|
||||||
if (Element === 'button') {
|
type ActionButtonAsLink = BaseActionButtonProps &
|
||||||
|
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
||||||
|
Element: 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonAsExternal = BaseActionButtonProps &
|
||||||
|
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
||||||
|
Element: 'externalLink'
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonAsElement = BaseActionButtonProps &
|
||||||
|
Omit<React.HTMLAttributes<HTMLElement>, keyof BaseActionButtonProps> & {
|
||||||
|
Element: React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonProps =
|
||||||
|
| ActionButtonAsButton
|
||||||
|
| ActionButtonAsLink
|
||||||
|
| ActionButtonAsExternal
|
||||||
|
| ActionButtonAsElement
|
||||||
|
|
||||||
|
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 ${
|
||||||
|
props.icon ? 'pr-2' : 'px-2'
|
||||||
|
} ${props.className || ''}`
|
||||||
|
|
||||||
|
switch (props.Element) {
|
||||||
|
case 'button': {
|
||||||
|
// Note we have to destructure 'className' and 'Element' out of props
|
||||||
|
// because we don't want to pass them to the button element;
|
||||||
|
// the same is true for the other cases below.
|
||||||
|
const { Element, icon, children, className, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className={classNames} {...props}>
|
<button className={classNames} {...rest}>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{props.icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
} else if (Element === 'link') {
|
}
|
||||||
|
case 'link': {
|
||||||
|
const { Element, to, icon, children, className, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<Link to={to} className={classNames} {...props}>
|
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
case 'externalLink': {
|
||||||
|
const { Element, to, icon, children, className, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<Element onClick={onClick} className={classNames} {...props}>
|
<Link
|
||||||
|
to={to || paths.INDEX}
|
||||||
|
className={classNames}
|
||||||
|
{...rest}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const { Element, icon, children, className, ...rest } = props
|
||||||
|
if (!Element) throw new Error('Element is required')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Element className={classNames} {...rest}>
|
||||||
|
{props.icon && <ActionIcon {...props.icon} />}
|
||||||
|
{children}
|
||||||
</Element>
|
</Element>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,67 @@
|
|||||||
import {
|
import {
|
||||||
IconDefinition,
|
IconDefinition as SolidIconDefinition,
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-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,
|
sm: 12,
|
||||||
md: 14.4,
|
md: 14.4,
|
||||||
lg: 18,
|
lg: 20,
|
||||||
|
xl: 28,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionIconProps extends React.PropsWithChildren {
|
export interface ActionIconProps extends React.PropsWithChildren {
|
||||||
icon?: IconDefinition
|
icon?: SolidIconDefinition | BrandIconDefinition | CustomIconName
|
||||||
|
className?: string
|
||||||
bgClassName?: string
|
bgClassName?: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
size?: keyof typeof iconSizes
|
size?: keyof typeof iconSizes
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionIcon = ({
|
export const ActionIcon = ({
|
||||||
icon,
|
icon = faCircleExclamation,
|
||||||
|
className,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
iconClassName,
|
iconClassName,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
children,
|
children,
|
||||||
}: ActionIconProps) => {
|
}: ActionIconProps) => {
|
||||||
|
// By default, we reverse the icon color and background color in dark mode
|
||||||
|
const computedIconClassName =
|
||||||
|
iconClassName ||
|
||||||
|
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
|
||||||
|
|
||||||
|
const computedBgClassName =
|
||||||
|
bgClassName ||
|
||||||
|
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'p-1 w-fit inline-grid place-content-center ' +
|
`p-${
|
||||||
(bgClassName ||
|
size === 'xl' ? '2' : '1'
|
||||||
'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')
|
} w-fit inline-grid place-content-center ${className} ` +
|
||||||
|
computedBgClassName
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children || (
|
{children ? (
|
||||||
<FontAwesomeIcon
|
children
|
||||||
icon={icon || faCircleExclamation}
|
) : typeof icon === 'string' ? (
|
||||||
|
<CustomIcon
|
||||||
|
name={icon}
|
||||||
width={iconSizes[size]}
|
width={iconSizes[size]}
|
||||||
height={iconSizes[size]}
|
height={iconSizes[size]}
|
||||||
className={
|
className={computedIconClassName}
|
||||||
iconClassName ||
|
/>
|
||||||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
) : (
|
||||||
}
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
width={iconSizes[size]}
|
||||||
|
height={iconSizes[size]}
|
||||||
|
className={computedIconClassName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
7
src/components/AppHeader.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
Some CSS cannot be represented
|
||||||
|
in Tailwind, such as complex grid layouts.
|
||||||
|
*/
|
||||||
|
.header {
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
}
|
@ -1,46 +1,55 @@
|
|||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { paths } from '../Router'
|
import { IndexLoaderData } from '../Router'
|
||||||
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import styles from './AppHeader.module.css'
|
||||||
|
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
|
project?: Omit<IndexLoaderData, 'code'>
|
||||||
className?: string
|
className?: string
|
||||||
|
enableMenu?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppHeader = ({
|
export const AppHeader = ({
|
||||||
showToolbar = true,
|
showToolbar = true,
|
||||||
|
project,
|
||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { user } = useStore((s) => ({
|
const { auth } = useGlobalStateContext()
|
||||||
user: s.user,
|
const user = auth?.context?.user
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={
|
className={
|
||||||
'overlaid-panes sticky top-0 z-10 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
|
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
|
||||||
|
styles.header +
|
||||||
|
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||||
className
|
className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link to={paths.INDEX}>
|
<ProjectSidebarMenu
|
||||||
<img
|
renderAsLink={!enableMenu}
|
||||||
src="/kitt-arcade-winking.svg"
|
project={project?.project}
|
||||||
alt="KittyCAD App"
|
file={project?.file}
|
||||||
className="h-9 w-auto"
|
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">KittyCAD App</span>
|
|
||||||
</Link>
|
|
||||||
{/* Toolbar if the context deems it */}
|
{/* Toolbar if the context deems it */}
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || <UserSidebarMenu user={user} />}
|
{children || (
|
||||||
|
<div className="flex items-center gap-1 ml-auto">
|
||||||
|
<NetworkHealthIndicator />
|
||||||
|
<UserSidebarMenu user={user} />
|
||||||
|
</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 { abstractSyntaxTree } 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,8 +7,10 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
} from '../lang/modifyAst'
|
} from '../lang/modifyAst'
|
||||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||||
import { lexer } from '../lang/tokeniser'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { useStore } from '../useStore'
|
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { executeAst } from 'useStore'
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
onVarClick,
|
onVarClick,
|
||||||
@ -93,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>
|
||||||
@ -120,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(() => {
|
||||||
@ -135,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 = abstractSyntaxTree(lexer(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' &&
|
||||||
@ -166,7 +170,7 @@ export function useCalc({
|
|||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
}
|
}
|
||||||
}, [value])
|
}, [value, availableVarInfo])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valueNode,
|
valueNode,
|
||||||
@ -199,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
|
||||||
@ -233,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-liquid-10/50 ui-active:text-liquid-90;
|
||||||
|
@apply transition-colors ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .button {
|
||||||
|
@apply text-chalkboard-30;
|
||||||
|
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button small {
|
||||||
|
@apply text-chalkboard-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .button small {
|
||||||
|
@apply text-chalkboard-40;
|
||||||
|
}
|
82
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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}
|
||||||
|
bgClassName={
|
||||||
|
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||||
|
}
|
||||||
|
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,15 +1,15 @@
|
|||||||
.panel {
|
.panel {
|
||||||
@apply relative overflow-auto z-0;
|
@apply relative z-0;
|
||||||
@apply bg-chalkboard-20/40;
|
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .panel {
|
:global(.dark) .panel {
|
||||||
@apply bg-chalkboard-110/50;
|
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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,7 @@ export interface CollapsiblePanelProps
|
|||||||
title: string
|
title: string
|
||||||
icon?: IconDefinition
|
icon?: IconDefinition
|
||||||
open?: boolean
|
open?: boolean
|
||||||
|
menu?: React.ReactNode
|
||||||
iconClassNames?: {
|
iconClassNames?: {
|
||||||
bg?: string
|
bg?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@ -18,9 +19,11 @@ 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 align-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
bgClassName={
|
bgClassName={
|
||||||
@ -33,6 +36,10 @@ export const PanelHeader = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{title}
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
...props
|
...props
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
className={styles.panel + ' group ' + (className || '')}
|
||||||
>
|
>
|
||||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
<PanelHeader
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
iconClassNames={iconClassNames}
|
||||||
|
menu={menu}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
|
290
src/components/CommandBar.tsx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
Fragment,
|
||||||
|
SetStateAction,
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { Command, SubCommand } from '../lib/commands'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
|
export type SortedCommand = {
|
||||||
|
item: Partial<Command | SubCommand> & { name: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandsContext = createContext(
|
||||||
|
{} as {
|
||||||
|
commands: Command[]
|
||||||
|
addCommands: (commands: Command[]) => void
|
||||||
|
removeCommands: (commands: Command[]) => void
|
||||||
|
commandBarOpen: boolean
|
||||||
|
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CommandBarProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const [commands, internalSetCommands] = useState([] as Command[])
|
||||||
|
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||||
|
|
||||||
|
const addCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||||
|
}
|
||||||
|
const removeCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) =>
|
||||||
|
prevCommands.filter((command) => !newCommands.includes(command))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandsContext.Provider
|
||||||
|
value={{
|
||||||
|
commands,
|
||||||
|
addCommands,
|
||||||
|
removeCommands,
|
||||||
|
commandBarOpen,
|
||||||
|
setCommandBarOpen,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CommandBar />
|
||||||
|
</CommandsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandBar = () => {
|
||||||
|
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||||
|
useHotkeys(['meta+k', 'meta+/'], () => {
|
||||||
|
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 z-40 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 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="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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="w-full bg-transparent focus:outline-none"
|
||||||
|
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="overflow-y-auto max-h-96">
|
||||||
|
{filteredCommands?.map((commandResult) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={commandResult.item.name}
|
||||||
|
value={commandResult}
|
||||||
|
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
|
||||||
|
>
|
||||||
|
<p>{commandResult.item.name}</p>
|
||||||
|
{(commandResult.item as SubCommand).description && (
|
||||||
|
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||||
|
{(commandResult.item as SubCommand).description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</Transition.Child>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarProvider
|
210
src/components/CustomIcon.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
export type CustomIconName =
|
||||||
|
| 'createFile'
|
||||||
|
| 'createFolder'
|
||||||
|
| 'equal'
|
||||||
|
| 'exit'
|
||||||
|
| 'extrude'
|
||||||
|
| 'file'
|
||||||
|
| 'horizontal'
|
||||||
|
| 'line'
|
||||||
|
| 'move'
|
||||||
|
| 'parallel'
|
||||||
|
| 'sketch'
|
||||||
|
| 'vertical'
|
||||||
|
|
||||||
|
export const CustomIcon = ({
|
||||||
|
name,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
name: CustomIconName
|
||||||
|
} & React.SVGProps<SVGSVGElement>) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'createFile':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'createFolder':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'equal':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 8.78V7H14.52V8.78H5ZM5 13.02V11.24H14.52V13.02H5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'exit':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'extrude':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 3L10.3536 3.35355L12.3536 5.35355L11.6465 6.06066L10.5 4.91421V11.5854C11.0826 11.7913 11.5 12.3469 11.5 13C11.5 13.8284 10.8284 14.5 10 14.5C9.17157 14.5 8.5 13.8284 8.5 13C8.5 12.3469 8.91741 11.7913 9.5 11.5854V4.91421L8.35356 6.06066L7.64645 5.35355L9.64645 3.35355L10 3ZM1.95887 12.3282L8 8.63644V9.80838L2.91773 12.9142L10 17.2423L17.0823 12.9142L12 9.80838V8.63644L18.0411 12.3282L19 12.9142L19 14.9683H18V13.5253L10.5 18.1087V19.9683H9.5V18.1087L2 13.5253V14.9683H1L1 12.9142L1.95887 12.3282Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'file':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'horizontal':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 9.5H16V11.5H4V9.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'line':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.5 6C16.3284 6 17 5.32843 17 4.5C17 3.67157 16.3284 3 15.5 3C14.6716 3 14 3.67157 14 4.5C14 4.73107 14.0522 4.94993 14.1456 5.14543L5.14543 14.1456C4.94993 14.0522 4.73107 14 4.5 14C3.67157 14 3 14.6716 3 15.5C3 16.3284 3.67157 17 4.5 17C5.32843 17 6 16.3284 6 15.5C6 15.2679 5.94729 15.0482 5.8532 14.852L14.852 5.8532C15.0482 5.94729 15.2679 6 15.5 6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'move':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 2.29289L10.3536 2.64645L12.3536 4.64645L11.6465 5.35355L10.5 4.20711V8V9.50001H12L15.7929 9.50001L14.6465 8.35356L15.3536 7.64645L17.3536 9.64645L17.7071 10L17.3536 10.3536L15.3536 12.3536L14.6465 11.6465L15.7929 10.5H12H10.5V12V15.7929L11.6465 14.6464L12.3536 15.3536L10.3536 17.3536L10 17.7071L9.64645 17.3536L7.64645 15.3536L8.35356 14.6464L9.50001 15.7929V12V10.5H8.00001H4.20712L5.35357 11.6465L4.64646 12.3536L2.64646 10.3536L2.29291 10L2.64646 9.64645L4.64646 7.64645L5.35357 8.35356L4.20712 9.50001H8.00001H9.50001V8V4.20711L8.35356 5.35355L7.64645 4.64645L9.64645 2.64645L10 2.29289Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'parallel':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8 16V4H6V16H8ZM14 16V4H12V16H14Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'sketch':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M14.8037 13.4035L15.5509 14.1635L16.3682 16.8386L13.5521 16.1346L12.8186 15.3885L14.8037 13.4035ZM14.1025 12.6903L12.1175 14.6754L3.48609 5.89624C2.94588 5.34678 2.94963 4.46456 3.49448 3.91971C4.04591 3.36828 4.94112 3.37208 5.48786 3.92817L14.1025 12.6903ZM6.20094 3.22709L16.4357 13.6371L17.5003 17.1216L17.8412 18.2376L16.7091 17.9546L13.0364 17.0364L2.77301 6.59732C1.84793 5.6564 1.85434 4.14564 2.78737 3.2126C3.73167 2.2683 5.26468 2.27481 6.20094 3.22709Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'vertical':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M11 4V16H9V4H11Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,136 +1,21 @@
|
|||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { useStore } from '../useStore'
|
import { AstExplorer } from './AstExplorer'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { EngineCommand } from '../lang/std/engineConnection'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { ActionButton } from '../components/ActionButton'
|
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
type SketchModeCmd = Extract<
|
|
||||||
EngineCommand['cmd'],
|
|
||||||
{ type: 'default_camera_enable_sketch_mode' }
|
|
||||||
>
|
|
||||||
|
|
||||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||||
const { engineCommandManager } = useStore((s) => ({
|
|
||||||
engineCommandManager: s.engineCommandManager,
|
|
||||||
}))
|
|
||||||
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
|
|
||||||
type: 'default_camera_enable_sketch_mode',
|
|
||||||
origin: { x: 0, y: 0, z: 0 },
|
|
||||||
x_axis: { x: 1, y: 0, z: 0 },
|
|
||||||
y_axis: { x: 0, y: 1, z: 0 },
|
|
||||||
distance_to_plane: 100,
|
|
||||||
ortho: true,
|
|
||||||
})
|
|
||||||
if (!sketchModeCmd) return null
|
|
||||||
return (
|
return (
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
{...props}
|
{...props}
|
||||||
className={'!absolute !h-auto bottom-5 right-5 ' + className}
|
className={
|
||||||
|
'!absolute overflow-hidden !h-auto bottom-5 right-5 ' + className
|
||||||
|
}
|
||||||
|
// header height, top-5, and bottom-5
|
||||||
|
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
||||||
>
|
>
|
||||||
<section className="p-4 flex flex-col gap-4">
|
<section className="p-4 flex flex-col gap-4">
|
||||||
<Xyz
|
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||||
onChange={setSketchModeCmd}
|
<AstExplorer />
|
||||||
pointKey="origin"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<Xyz
|
|
||||||
onChange={setSketchModeCmd}
|
|
||||||
pointKey="x_axis"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<Xyz
|
|
||||||
onChange={setSketchModeCmd}
|
|
||||||
pointKey="y_axis"
|
|
||||||
data={sketchModeCmd}
|
|
||||||
/>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="pr-4">distance_to_plane</div>
|
|
||||||
<input
|
|
||||||
className="w-16 dark:bg-chalkboard-90"
|
|
||||||
type="number"
|
|
||||||
value={sketchModeCmd.distance_to_plane}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
setSketchModeCmd({
|
|
||||||
...sketchModeCmd,
|
|
||||||
distance_to_plane: Number(target.value),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="pr-4">ortho</div>
|
|
||||||
<input
|
|
||||||
className="w-16"
|
|
||||||
type="checkbox"
|
|
||||||
checked={sketchModeCmd.ortho}
|
|
||||||
onChange={(a) => {
|
|
||||||
console.log(a, (a as any).checked)
|
|
||||||
setSketchModeCmd({
|
|
||||||
...sketchModeCmd,
|
|
||||||
ortho: a.target.checked,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ActionButton
|
|
||||||
onClick={() => {
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: sketchModeCmd,
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="hover:border-succeed-50"
|
|
||||||
icon={{
|
|
||||||
icon: faCheck,
|
|
||||||
bgClassName:
|
|
||||||
'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
|
|
||||||
iconClassName:
|
|
||||||
'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Send sketch mode command
|
|
||||||
</ActionButton>
|
|
||||||
</section>
|
</section>
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Xyz = ({
|
|
||||||
pointKey,
|
|
||||||
data,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
pointKey: 'origin' | 'y_axis' | 'x_axis'
|
|
||||||
data: SketchModeCmd
|
|
||||||
onChange: (a: SketchModeCmd) => void
|
|
||||||
}) => {
|
|
||||||
if (!data) return null
|
|
||||||
return (
|
|
||||||
<div className="flex">
|
|
||||||
<div className="pr-4">{pointKey}</div>
|
|
||||||
{Object.entries(data[pointKey]).map(([axis, val]) => {
|
|
||||||
return (
|
|
||||||
<div key={axis} className="flex">
|
|
||||||
<div className="w-4">{axis}</div>
|
|
||||||
<input
|
|
||||||
className="w-16 dark:bg-chalkboard-90"
|
|
||||||
type="number"
|
|
||||||
value={val}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
onChange({
|
|
||||||
...data,
|
|
||||||
[pointKey]: {
|
|
||||||
...data[pointKey],
|
|
||||||
[axis]: Number(target.value),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
57
src/components/DownloadAppBanner.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import { faX } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
const DownloadAppBanner = () => {
|
||||||
|
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
||||||
|
isBannerDismissed: s.isBannerDismissed,
|
||||||
|
setBannerDismissed: s.setBannerDismissed,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
|
||||||
|
open={!isBannerDismissed}
|
||||||
|
onClose={() => ({})}
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="max-w-3xl mx-auto">
|
||||||
|
<div className="flex gap-2 justify-between items-start">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
KittyCAD Modeling App is better as a desktop app!
|
||||||
|
</h2>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => setBannerDismissed(true)}
|
||||||
|
icon={{
|
||||||
|
icon: faX,
|
||||||
|
bgClassName:
|
||||||
|
'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
|
||||||
|
iconClassName:
|
||||||
|
'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent !border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
The browser version of the app only saves your data temporarily in{' '}
|
||||||
|
<code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
|
||||||
|
localStorage
|
||||||
|
</code>
|
||||||
|
, and isn't backed up anywhere! Visit{' '}
|
||||||
|
<a
|
||||||
|
href="https://kittycad.io/modeling-app/download"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
||||||
|
>
|
||||||
|
our website
|
||||||
|
</a>{' '}
|
||||||
|
to download the app for the best experience.
|
||||||
|
</p>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadAppBanner
|
@ -1,8 +1,60 @@
|
|||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import {
|
||||||
|
faBug,
|
||||||
|
faHome,
|
||||||
|
faRefresh,
|
||||||
|
faTrash,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
export const ErrorPage = () => {
|
export const ErrorPage = () => {
|
||||||
|
let error = useRouteError()
|
||||||
|
|
||||||
|
console.error('error', error)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
<h1 className="text-4xl font-bold">404</h1>
|
<section className="max-w-full xl:max-w-4xl mx-auto">
|
||||||
<p className="text-2xl font-bold">Page not found</p>
|
<h1 className="text-4xl mb-8 font-bold">
|
||||||
|
An unexpected error occurred
|
||||||
|
</h1>
|
||||||
|
{isRouteErrorResponse(error) && (
|
||||||
|
<p className="mb-8">
|
||||||
|
{error.status}: {error.data}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between gap-2 mt-6">
|
||||||
|
{isTauri() && (
|
||||||
|
<ActionButton Element="link" to={'/'} icon={{ icon: faHome }}>
|
||||||
|
Go Home
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faRefresh }}
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faTrash }}
|
||||||
|
onClick={() => {
|
||||||
|
window.localStorage.clear()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear storage
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="externalLink"
|
||||||
|
icon={{ icon: faBug }}
|
||||||
|
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
||||||
|
>
|
||||||
|
Report Bug
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,40 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faXmark } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import Modal from 'react-modal'
|
import Modal from 'react-modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
|
type OutputTypeKey = OutputFormat['type']
|
||||||
|
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||||
|
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||||
|
|
||||||
export const ExportButton = () => {
|
interface ExportButtonProps extends React.PropsWithChildren {
|
||||||
const { engineCommandManager } = useStore((s) => ({
|
className?: {
|
||||||
engineCommandManager: s.engineCommandManager,
|
button?: string
|
||||||
}))
|
icon?: string
|
||||||
|
bg?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||||
const [modalIsOpen, setIsOpen] = React.useState(false)
|
const [modalIsOpen, setIsOpen] = React.useState(false)
|
||||||
|
const {
|
||||||
|
settings: {
|
||||||
|
state: {
|
||||||
|
context: { baseUnit },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const defaultType = 'gltf'
|
const defaultType = 'gltf'
|
||||||
const [type, setType] = React.useState(defaultType)
|
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
|
||||||
|
const defaultStorage = 'embedded'
|
||||||
const customModalStyles = {
|
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
|
||||||
content: {
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto',
|
|
||||||
marginRight: '-50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
@ -41,7 +47,8 @@ export const ExportButton = () => {
|
|||||||
// Default to gltf and embedded.
|
// Default to gltf and embedded.
|
||||||
const initialValues: OutputFormat = {
|
const initialValues: OutputFormat = {
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
storage: 'embedded',
|
storage: defaultStorage,
|
||||||
|
presentation: 'pretty',
|
||||||
}
|
}
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues,
|
initialValues,
|
||||||
@ -68,7 +75,18 @@ export const ExportButton = () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
engineCommandManager?.sendSceneCommand({
|
if (values.type === 'obj' || values.type === 'stl') {
|
||||||
|
values.units = baseUnit
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
values.type === 'ply' ||
|
||||||
|
values.type === 'stl' ||
|
||||||
|
values.type === 'gltf'
|
||||||
|
) {
|
||||||
|
// Set the storage type.
|
||||||
|
values.storage = storage
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'export',
|
type: 'export',
|
||||||
@ -77,9 +95,9 @@ export const ExportButton = () => {
|
|||||||
// in the scene to export. In that case, you'd pass the IDs thru here.
|
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||||
entity_ids: [],
|
entity_ids: [],
|
||||||
format: values,
|
format: values,
|
||||||
|
source_unit: baseUnit,
|
||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
closeModal()
|
closeModal()
|
||||||
@ -88,27 +106,48 @@ export const ExportButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={openModal}>Export</button>
|
<ActionButton
|
||||||
|
onClick={openModal}
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: faFileExport,
|
||||||
|
iconClassName: className?.icon,
|
||||||
|
bgClassName: className?.bg,
|
||||||
|
}}
|
||||||
|
className={className?.button}
|
||||||
|
>
|
||||||
|
{children || 'Export'}
|
||||||
|
</ActionButton>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onRequestClose={closeModal}
|
onRequestClose={closeModal}
|
||||||
contentLabel="Export"
|
contentLabel="Export"
|
||||||
style={customModalStyles}
|
overlayClassName="z-40 fixed inset-0 grid place-items-center"
|
||||||
|
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
|
||||||
>
|
>
|
||||||
<div className="text-black">
|
|
||||||
<h1 className="text-2xl font-bold">Export your design</h1>
|
<h1 className="text-2xl font-bold">Export your design</h1>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<p>
|
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
|
||||||
<label htmlFor="type">Type</label>
|
<label htmlFor="type" className="flex-1">
|
||||||
</p>
|
<p className="mb-2">Type</p>
|
||||||
<p>
|
|
||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
name="type"
|
name="type"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setType(e.target.value)
|
setType(e.target.value as OutputTypeKey)
|
||||||
|
if (e.target.value === 'gltf') {
|
||||||
|
// Set default to embedded.
|
||||||
|
setStorage('embedded')
|
||||||
|
} else if (e.target.value === 'ply') {
|
||||||
|
// Set default to ascii.
|
||||||
|
setStorage('ascii')
|
||||||
|
} else if (e.target.value === 'stl') {
|
||||||
|
// Set default to ascii.
|
||||||
|
setStorage('ascii')
|
||||||
|
}
|
||||||
formik.handleChange(e)
|
formik.handleChange(e)
|
||||||
}}
|
}}
|
||||||
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
>
|
>
|
||||||
<option value="gltf">gltf</option>
|
<option value="gltf">gltf</option>
|
||||||
<option value="obj">obj</option>
|
<option value="obj">obj</option>
|
||||||
@ -116,20 +155,18 @@ export const ExportButton = () => {
|
|||||||
<option value="step">step</option>
|
<option value="step">step</option>
|
||||||
<option value="stl">stl</option>
|
<option value="stl">stl</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</label>
|
||||||
|
|
||||||
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
||||||
<>
|
<label htmlFor="storage" className="flex-1">
|
||||||
<p>
|
<p className="mb-2">Storage</p>
|
||||||
{' '}
|
|
||||||
<label htmlFor="storage">Storage</label>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<select
|
<select
|
||||||
id="storage"
|
id="storage"
|
||||||
name="storage"
|
name="storage"
|
||||||
onChange={formik.handleChange}
|
onChange={(e) => {
|
||||||
value={formik.values.storage}
|
setStorage(e.target.value as StorageUnion)
|
||||||
|
formik.handleChange(e)
|
||||||
|
}}
|
||||||
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
>
|
>
|
||||||
{type === 'gltf' && (
|
{type === 'gltf' && (
|
||||||
<>
|
<>
|
||||||
@ -156,16 +193,13 @@ export const ExportButton = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</label>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faXmark,
|
icon: faXmark,
|
||||||
@ -177,8 +211,15 @@ export const ExportButton = () => {
|
|||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
icon={{ icon: faFileExport }}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
158
src/components/FileMachineProvider.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
|
import { IndexLoaderData, paths } from '../Router'
|
||||||
|
import React, { createContext } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
EventFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
Prop,
|
||||||
|
StateFrom,
|
||||||
|
} from 'xstate'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
|
||||||
|
import {
|
||||||
|
createDir,
|
||||||
|
removeDir,
|
||||||
|
removeFile,
|
||||||
|
renameFile,
|
||||||
|
writeFile,
|
||||||
|
} from '@tauri-apps/api/fs'
|
||||||
|
import { FILE_EXT, readProject } from 'lib/tauriFS'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
|
||||||
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
|
state: StateFrom<T>
|
||||||
|
context: ContextFrom<T>
|
||||||
|
send: Prop<InterpreterFrom<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileContext = createContext(
|
||||||
|
{} as MachineContext<typeof fileMachine>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FileMachineProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
|
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
|
|
||||||
|
const [state, send] = useMachine(fileMachine, {
|
||||||
|
context: {
|
||||||
|
project,
|
||||||
|
selectedDirectory: project,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
navigateToFile: (
|
||||||
|
context: ContextFrom<typeof fileMachine>,
|
||||||
|
event: EventFrom<typeof fileMachine>
|
||||||
|
) => {
|
||||||
|
if (event.data && 'name' in event.data) {
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
navigate(
|
||||||
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
|
context.selectedDirectory + sep + event.data.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toastSuccess: (_, event) =>
|
||||||
|
event.data && toast.success((event.data || '') + ''),
|
||||||
|
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||||
|
const newFiles = isTauri()
|
||||||
|
? await readProject(context.project.path)
|
||||||
|
: []
|
||||||
|
return {
|
||||||
|
...context.project,
|
||||||
|
children: newFiles,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createFile: async (
|
||||||
|
context: ContextFrom<typeof fileMachine>,
|
||||||
|
event: EventFrom<typeof fileMachine, 'Create file'>
|
||||||
|
) => {
|
||||||
|
let name = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||||
|
|
||||||
|
if (event.data.makeDir) {
|
||||||
|
await createDir(context.selectedDirectory.path + sep + name)
|
||||||
|
} else {
|
||||||
|
await writeFile(
|
||||||
|
context.selectedDirectory.path +
|
||||||
|
sep +
|
||||||
|
name +
|
||||||
|
(name.endsWith(FILE_EXT) ? '' : FILE_EXT),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Successfully created "${name}"`
|
||||||
|
},
|
||||||
|
renameFile: async (
|
||||||
|
context: ContextFrom<typeof fileMachine>,
|
||||||
|
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||||
|
) => {
|
||||||
|
const { oldName, newName, isDir } = event.data
|
||||||
|
let name = newName ? newName : DEFAULT_FILE_NAME
|
||||||
|
|
||||||
|
await renameFile(
|
||||||
|
context.selectedDirectory.path + sep + oldName,
|
||||||
|
context.selectedDirectory.path +
|
||||||
|
sep +
|
||||||
|
name +
|
||||||
|
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deleteFile: async (
|
||||||
|
context: ContextFrom<typeof fileMachine>,
|
||||||
|
event: EventFrom<typeof fileMachine, 'Delete file'>
|
||||||
|
) => {
|
||||||
|
const isDir = !!event.data.children
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
await removeDir(event.data.path, {
|
||||||
|
recursive: true,
|
||||||
|
}).catch((e) => console.error('Error deleting directory', e))
|
||||||
|
} else {
|
||||||
|
await removeFile(event.data.path).catch((e) =>
|
||||||
|
console.error('Error deleting file', e)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||||
|
event.data.name
|
||||||
|
}"`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
||||||
|
if (event.type !== 'done.invoke.read-files') return false
|
||||||
|
return !!event?.data?.children && event.data.children.length > 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileContext.Provider
|
||||||
|
value={{
|
||||||
|
send,
|
||||||
|
state,
|
||||||
|
context: state.context, // just a convenience, can remove if we need to save on memory
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FileContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileMachineProvider
|
16
src/components/FileTree.module.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.folder {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder::after {
|
||||||
|
content: '';
|
||||||
|
width: 1px;
|
||||||
|
z-index: -1;
|
||||||
|
@apply absolute top-0 bottom-0;
|
||||||
|
left: calc(var(--indent-line-left, 1rem) + 0.25rem);
|
||||||
|
@apply bg-chalkboard-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .folder::after {
|
||||||
|
@apply bg-chalkboard-80;
|
||||||
|
}
|
399
src/components/FileTree.tsx
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
import { IndexLoaderData, paths } from 'Router'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
import { FileEntry } from '@tauri-apps/api/fs'
|
||||||
|
import { Dispatch, useRef, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Dialog, Disclosure } from '@headlessui/react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
import styles from './FileTree.module.css'
|
||||||
|
import { sortProject } from 'lib/tauriFS'
|
||||||
|
|
||||||
|
function getIndentationCSS(level: number) {
|
||||||
|
return `calc(1rem * ${level + 1})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function RenameForm({
|
||||||
|
fileOrDir,
|
||||||
|
setIsRenaming,
|
||||||
|
level = 0,
|
||||||
|
}: {
|
||||||
|
fileOrDir: FileEntry
|
||||||
|
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
level?: number
|
||||||
|
}) {
|
||||||
|
const { send } = useFileContext()
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsRenaming(false)
|
||||||
|
send({
|
||||||
|
type: 'Rename file',
|
||||||
|
data: {
|
||||||
|
oldName: fileOrDir.name || '',
|
||||||
|
newName: inputRef.current?.value || fileOrDir.name || '',
|
||||||
|
isDir: fileOrDir.children !== undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsRenaming(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleRenameSubmit}>
|
||||||
|
<label>
|
||||||
|
<span className="sr-only">Rename file</span>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
placeholder={fileOrDir.name}
|
||||||
|
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={() => setIsRenaming(false)}
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button className="sr-only" type="submit">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteConfirmationDialog({
|
||||||
|
fileOrDir,
|
||||||
|
setIsOpen,
|
||||||
|
}: {
|
||||||
|
fileOrDir: FileEntry
|
||||||
|
setIsOpen: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
}) {
|
||||||
|
const { send } = useFileContext()
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
className="relative z-50"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
|
||||||
|
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
|
||||||
|
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
|
||||||
|
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="my-6">
|
||||||
|
This will permanently delete "{fileOrDir.name || 'this file'}"
|
||||||
|
{fileOrDir.children !== undefined
|
||||||
|
? ' and all of its contents. '
|
||||||
|
: '. '}
|
||||||
|
This action cannot be undone.
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={async () => {
|
||||||
|
send({ type: 'Delete file', data: fileOrDir })
|
||||||
|
setIsOpen(false)
|
||||||
|
}}
|
||||||
|
icon={{
|
||||||
|
icon: faTrashAlt,
|
||||||
|
bgClassName: 'bg-destroy-80',
|
||||||
|
iconClassName:
|
||||||
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||||
|
}}
|
||||||
|
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileTreeItem = ({
|
||||||
|
project,
|
||||||
|
currentFile,
|
||||||
|
fileOrDir,
|
||||||
|
closePanel,
|
||||||
|
level = 0,
|
||||||
|
}: {
|
||||||
|
project?: IndexLoaderData['project']
|
||||||
|
currentFile?: IndexLoaderData['file']
|
||||||
|
fileOrDir: FileEntry
|
||||||
|
closePanel: (
|
||||||
|
focusableElement?:
|
||||||
|
| HTMLElement
|
||||||
|
| React.MutableRefObject<HTMLElement | null>
|
||||||
|
| undefined
|
||||||
|
) => void
|
||||||
|
level?: number
|
||||||
|
}) => {
|
||||||
|
const { send, context } = useFileContext()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false)
|
||||||
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
|
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||||
|
|
||||||
|
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
||||||
|
if (e.metaKey && e.key === 'Backspace') {
|
||||||
|
// Open confirmation dialog
|
||||||
|
setIsConfirmingDelete(true)
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
// Show the renaming form
|
||||||
|
setIsRenaming(true)
|
||||||
|
} else if (e.code === 'Space') {
|
||||||
|
openFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFile() {
|
||||||
|
if (fileOrDir.children !== undefined) return // Don't open directories
|
||||||
|
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||||
|
closePanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fileOrDir.children === undefined ? (
|
||||||
|
<li
|
||||||
|
className={
|
||||||
|
'group m-0 p-0 border-solid border-0 text-energy-100 hover:text-energy-70 hover:bg-energy-10/50 dark:text-energy-30 dark:hover:!text-energy-20 dark:hover:bg-energy-90/50 focus-within:bg-energy-10/80 dark:focus-within:bg-energy-80/50 hover:focus-within:bg-energy-10/80 dark:hover:focus-within:bg-energy-80/50 ' +
|
||||||
|
(isCurrentFile ? 'bg-energy-10/50 dark:bg-energy-90/50' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!isRenaming ? (
|
||||||
|
<button
|
||||||
|
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
|
onDoubleClick={openFile}
|
||||||
|
onClick={(e) => e.currentTarget.focus()}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
<KclIcon
|
||||||
|
className={
|
||||||
|
'inline-block w-3 ' +
|
||||||
|
(isCurrentFile
|
||||||
|
? 'text-energy-90 dark:text-energy-10'
|
||||||
|
: 'text-energy-50 dark:text-energy-50')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{fileOrDir.name}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<RenameForm
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
setIsRenaming={setIsRenaming}
|
||||||
|
level={level}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="group">
|
||||||
|
{!isRenaming ? (
|
||||||
|
<Disclosure.Button
|
||||||
|
className={
|
||||||
|
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 text-chalkboard-70 dark:text-chalkboard-30 hover:bg-energy-10/50 dark:hover:bg-energy-90/50' +
|
||||||
|
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||||
|
? ' group-focus-within:bg-chalkboard-20/50 dark:group-focus-within:bg-chalkboard-80/20 hover:group-focus-within:bg-chalkboard-20 dark:hover:group-focus-within:bg-chalkboard-80/20 group-active:bg-chalkboard-20/50 dark:group-active:bg-chalkboard-80/20 hover:group-active:bg-chalkboard-20/50 dark:hover:group-active:bg-chalkboard-80/20'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
|
onClick={(e) => e.currentTarget.focus()}
|
||||||
|
onClickCapture={(e) =>
|
||||||
|
send({ type: 'Set selected directory', data: fileOrDir })
|
||||||
|
}
|
||||||
|
onFocusCapture={(e) =>
|
||||||
|
send({ type: 'Set selected directory', data: fileOrDir })
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronRight}
|
||||||
|
className={
|
||||||
|
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||||
|
(open ? 'transform rotate-90' : '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{fileOrDir.name}
|
||||||
|
</Disclosure.Button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex items-center"
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronRight}
|
||||||
|
className={
|
||||||
|
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
|
||||||
|
(open ? 'transform rotate-90' : '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RenameForm
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
setIsRenaming={setIsRenaming}
|
||||||
|
level={-1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Disclosure.Panel
|
||||||
|
className={styles.folder}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--indent-line-left': getIndentationCSS(level),
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className="m-0 p-0"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
send({ type: 'Set selected directory', data: fileOrDir })
|
||||||
|
}}
|
||||||
|
onFocusCapture={(e) =>
|
||||||
|
send({ type: 'Set selected directory', data: fileOrDir })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{fileOrDir.children?.map((child) => (
|
||||||
|
<FileTreeItem
|
||||||
|
fileOrDir={child}
|
||||||
|
project={project}
|
||||||
|
currentFile={currentFile}
|
||||||
|
closePanel={closePanel}
|
||||||
|
level={level + 1}
|
||||||
|
key={level + '-' + child.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
)}
|
||||||
|
{isConfirmingDelete && (
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
setIsOpen={setIsConfirmingDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileTreeProps {
|
||||||
|
className?: string
|
||||||
|
file?: IndexLoaderData['file']
|
||||||
|
closePanel: (
|
||||||
|
focusableElement?:
|
||||||
|
| HTMLElement
|
||||||
|
| React.MutableRefObject<HTMLElement | null>
|
||||||
|
| undefined
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileTree = ({
|
||||||
|
className = '',
|
||||||
|
file,
|
||||||
|
closePanel,
|
||||||
|
}: FileTreeProps) => {
|
||||||
|
const { send, context } = useFileContext()
|
||||||
|
useHotkeys('meta + n', createFile)
|
||||||
|
useHotkeys('meta + shift + n', createFolder)
|
||||||
|
|
||||||
|
async function createFile() {
|
||||||
|
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFolder() {
|
||||||
|
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
|
||||||
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'createFile',
|
||||||
|
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||||
|
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 border-none bg-transparent !outline-none"
|
||||||
|
onClick={createFile}
|
||||||
|
>
|
||||||
|
<Tooltip position="inlineStart" delay={750}>
|
||||||
|
Create File
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'createFolder',
|
||||||
|
iconClassName: '!text-energy-80 dark:!text-energy-20',
|
||||||
|
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 border-none bg-transparent !outline-none"
|
||||||
|
onClick={createFolder}
|
||||||
|
>
|
||||||
|
<Tooltip position="inlineStart" delay={750}>
|
||||||
|
Create Folder
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-auto max-h-full pb-12">
|
||||||
|
<ul
|
||||||
|
className="m-0 p-0 text-sm"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
send({ type: 'Set selected directory', data: context.project })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||||
|
<FileTreeItem
|
||||||
|
project={context.project}
|
||||||
|
currentFile={file}
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
closePanel={closePanel}
|
||||||
|
key={fileOrDir.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KclIcon({ className = '' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 40 40"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
162
src/components/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { paths } from '../Router'
|
||||||
|
import {
|
||||||
|
authCommandBarMeta,
|
||||||
|
authMachine,
|
||||||
|
TOKEN_PERSIST_KEY,
|
||||||
|
} from '../machines/authMachine'
|
||||||
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
|
import React, { createContext, useEffect, useRef } from 'react'
|
||||||
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsCommandBarMeta,
|
||||||
|
settingsMachine,
|
||||||
|
} from 'machines/settingsMachine'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { setThemeClass, Themes } from 'lib/theme'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
Prop,
|
||||||
|
StateFrom,
|
||||||
|
} from 'xstate'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
|
||||||
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
|
state: StateFrom<T>
|
||||||
|
context: ContextFrom<T>
|
||||||
|
send: Prop<InterpreterFrom<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalContext = {
|
||||||
|
auth: MachineContext<typeof authMachine>
|
||||||
|
settings: MachineContext<typeof settingsMachine>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalStateContext = createContext({} as GlobalContext)
|
||||||
|
|
||||||
|
export const GlobalStateProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { commands } = useCommandsContext()
|
||||||
|
|
||||||
|
// Settings machine setup
|
||||||
|
const retrievedSettings = useRef(
|
||||||
|
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
||||||
|
)
|
||||||
|
const persistedSettings = Object.assign(
|
||||||
|
settingsMachine.initialState.context,
|
||||||
|
JSON.parse(retrievedSettings.current) as Partial<
|
||||||
|
(typeof settingsMachine)['context']
|
||||||
|
>
|
||||||
|
)
|
||||||
|
|
||||||
|
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
||||||
|
context: persistedSettings,
|
||||||
|
actions: {
|
||||||
|
toastSuccess: (context, event) => {
|
||||||
|
const truncatedNewValue =
|
||||||
|
'data' in event && event.data instanceof Object
|
||||||
|
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||||
|
.toString()
|
||||||
|
.substring(0, 28) as any)
|
||||||
|
: undefined
|
||||||
|
toast.success(
|
||||||
|
event.type +
|
||||||
|
(truncatedNewValue
|
||||||
|
? ` to "${truncatedNewValue}${
|
||||||
|
truncatedNewValue.length === 28 ? '...' : ''
|
||||||
|
}"`
|
||||||
|
: '')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state: settingsState,
|
||||||
|
send: settingsSend,
|
||||||
|
commands,
|
||||||
|
owner: 'settings',
|
||||||
|
commandBarMeta: settingsCommandBarMeta,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for changes to the system theme and update the app theme accordingly
|
||||||
|
// This is only done if the theme setting is set to 'system'.
|
||||||
|
// It can't be done in XState (in an invoked callback, for example)
|
||||||
|
// because there doesn't seem to be a good way to listen to
|
||||||
|
// events outside of the machine that also depend on the machine's context
|
||||||
|
useEffect(() => {
|
||||||
|
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const listener = (e: MediaQueryListEvent) => {
|
||||||
|
if (settingsState.context.theme !== 'system') return
|
||||||
|
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher.addEventListener('change', listener)
|
||||||
|
return () => matcher.removeEventListener('change', listener)
|
||||||
|
}, [settingsState.context])
|
||||||
|
|
||||||
|
// Auth machine setup
|
||||||
|
const [authState, authSend] = useMachine(authMachine, {
|
||||||
|
actions: {
|
||||||
|
goToSignInPage: () => {
|
||||||
|
navigate(paths.SIGN_IN)
|
||||||
|
|
||||||
|
logout()
|
||||||
|
},
|
||||||
|
goToIndexPage: () => {
|
||||||
|
if (window.location.pathname.includes(paths.SIGN_IN)) {
|
||||||
|
navigate(paths.INDEX)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state: authState,
|
||||||
|
send: authSend,
|
||||||
|
commands,
|
||||||
|
commandBarMeta: authCommandBarMeta,
|
||||||
|
owner: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlobalStateContext.Provider
|
||||||
|
value={{
|
||||||
|
auth: {
|
||||||
|
state: authState,
|
||||||
|
context: authState.context,
|
||||||
|
send: authSend,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
state: settingsState,
|
||||||
|
context: settingsState.context,
|
||||||
|
send: settingsSend,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GlobalStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalStateProvider
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
|
return (
|
||||||
|
!isTauri() &&
|
||||||
|
fetch(withBaseUrl('/logout'), {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Themes, useStore } from '../useStore'
|
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
import { useKclContext } from 'lang/KclSinglton'
|
||||||
|
|
||||||
const ReactJsonTypeHack = ReactJson as any
|
const ReactJsonTypeHack = ReactJson as any
|
||||||
|
|
||||||
@ -10,9 +11,7 @@ interface LogPanelProps extends CollapsiblePanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||||
const { logs } = useStore(({ logs }) => ({
|
const { logs } = useKclContext()
|
||||||
logs,
|
|
||||||
}))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
if (element) {
|
if (element) {
|
||||||
@ -46,21 +45,19 @@ export const KCLErrors = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: LogPanelProps) => {
|
}: LogPanelProps) => {
|
||||||
const { kclErrors } = useStore(({ kclErrors }) => ({
|
const { errors } = useKclContext()
|
||||||
kclErrors,
|
|
||||||
}))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||||
}
|
}
|
||||||
}, [kclErrors])
|
}, [errors])
|
||||||
return (
|
return (
|
||||||
<CollapsiblePanel {...props}>
|
<CollapsiblePanel {...props}>
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<div className="absolute inset-0 flex flex-col">
|
<div className="absolute inset-0 flex flex-col">
|
||||||
<ReactJsonTypeHack
|
<ReactJsonTypeHack
|
||||||
src={kclErrors}
|
src={errors}
|
||||||
collapsed={1}
|
collapsed={1}
|
||||||
collapseStringsAfterLength={60}
|
collapseStringsAfterLength={60}
|
||||||
enableClipboard={false}
|
enableClipboard={false}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { processMemory } from './MemoryPanel'
|
import { processMemory } from './MemoryPanel'
|
||||||
import { lexer } from '../lang/tokeniser'
|
|
||||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { initPromise } from '../lang/rust'
|
import { initPromise, parse } from '../lang/wasm'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -11,49 +9,41 @@ describe('processMemory', () => {
|
|||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = `
|
const code = `
|
||||||
const myVar = 5
|
const myVar = 5
|
||||||
const myFn = (a) => {
|
fn myFn = (a) => {
|
||||||
return a - 2
|
return a - 2
|
||||||
}
|
}
|
||||||
const otherVar = myFn(5)
|
const otherVar = myFn(5)
|
||||||
|
|
||||||
const theExtrude = startSketchAt([0, 0])
|
const theExtrude = startSketchOn('XY')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|> lineTo([-2.4, myVar], %)
|
|> lineTo([-2.4, myVar], %)
|
||||||
|> lineTo([-0.76, otherVar], %)
|
|> lineTo([-0.76, otherVar], %)
|
||||||
|> extrude(4, %)
|
|> extrude(4, %)
|
||||||
|
|
||||||
const theSketch = startSketchAt([0, 0])
|
const theSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|> lineTo([-3.35, 0.17], %)
|
|> lineTo([-3.35, 0.17], %)
|
||||||
|> lineTo([0.98, 5.16], %)
|
|> lineTo([0.98, 5.16], %)
|
||||||
|> lineTo([2.15, 4.32], %)
|
|> lineTo([2.15, 4.32], %)
|
||||||
// |> rx(90, %)
|
// |> rx(90, %)
|
||||||
show(theExtrude, theSketch)`
|
show(theExtrude, theSketch)`
|
||||||
const tokens = lexer(code)
|
const ast = parse(code)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
|
||||||
const programMemory = await enginelessExecutor(ast, {
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
root: {
|
root: {},
|
||||||
log: {
|
return: null,
|
||||||
type: 'userVal',
|
|
||||||
value: (a: any) => {
|
|
||||||
console.log('raw log', a)
|
|
||||||
},
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pendingMemory: {},
|
|
||||||
})
|
})
|
||||||
const output = processMemory(programMemory)
|
const output = processMemory(programMemory)
|
||||||
expect(output.myVar).toEqual(5)
|
expect(output.myVar).toEqual(5)
|
||||||
expect(output.myFn).toEqual('__function__')
|
|
||||||
expect(output.otherVar).toEqual(3)
|
expect(output.otherVar).toEqual(3)
|
||||||
expect(output).toEqual({
|
expect(output).toEqual({
|
||||||
myVar: 5,
|
myVar: 5,
|
||||||
myFn: '__function__',
|
myFn: undefined,
|
||||||
otherVar: 3,
|
otherVar: 3,
|
||||||
theExtrude: [],
|
theExtrude: [],
|
||||||
theSketch: [
|
theSketch: [
|
||||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
|
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
|
||||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
|
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
|
||||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
|
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { Themes, useStore } from '../useStore'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory } from '../lang/executor'
|
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
import { useKclContext } from 'lang/KclSinglton'
|
||||||
|
|
||||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
theme?: Exclude<Themes, Themes.System>
|
theme?: Exclude<Themes, Themes.System>
|
||||||
@ -12,9 +13,7 @@ export const MemoryPanel = ({
|
|||||||
theme = Themes.Light,
|
theme = Themes.Light,
|
||||||
...props
|
...props
|
||||||
}: MemoryPanelProps) => {
|
}: MemoryPanelProps) => {
|
||||||
const { programMemory } = useStore((s) => ({
|
const { programMemory } = useKclContext()
|
||||||
programMemory: s.programMemory,
|
|
||||||
}))
|
|
||||||
const ProcessedMemory = useMemo(
|
const ProcessedMemory = useMemo(
|
||||||
() => processMemory(programMemory),
|
() => processMemory(programMemory),
|
||||||
[programMemory]
|
[programMemory]
|
||||||
@ -23,7 +22,11 @@ export const MemoryPanel = ({
|
|||||||
<CollapsiblePanel {...props}>
|
<CollapsiblePanel {...props}>
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<div className="absolute inset-0 flex flex-col items-start">
|
<div className="absolute inset-0 flex flex-col items-start">
|
||||||
<div className=" h-full console-tile w-full">
|
<div
|
||||||
|
className="overflow-y-auto h-full console-tile w-full"
|
||||||
|
style={{ marginBottom: 36 }}
|
||||||
|
>
|
||||||
|
{/* 36px is the height of PanelHeader */}
|
||||||
<ReactJson
|
<ReactJson
|
||||||
src={ProcessedMemory}
|
src={ProcessedMemory}
|
||||||
collapsed={1}
|
collapsed={1}
|
||||||
@ -45,11 +48,15 @@ export const MemoryPanel = ({
|
|||||||
|
|
||||||
export const processMemory = (programMemory: ProgramMemory) => {
|
export const processMemory = (programMemory: ProgramMemory) => {
|
||||||
const processedMemory: any = {}
|
const processedMemory: any = {}
|
||||||
Object.keys(programMemory.root).forEach((key) => {
|
Object.keys(programMemory?.root || {}).forEach((key) => {
|
||||||
const val = programMemory.root[key]
|
const val = programMemory.root[key]
|
||||||
if (typeof val.value !== 'function') {
|
if (typeof val.value !== 'function') {
|
||||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
if (val.type === 'SketchGroup') {
|
||||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
} else if (val.type === 'ExtrudeGroup') {
|
||||||
|
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||||
return rest
|
return rest
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
458
src/components/ModelingMachineProvider.tsx
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import React, { createContext, useEffect, useRef } from 'react'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
Prop,
|
||||||
|
StateFrom,
|
||||||
|
assign,
|
||||||
|
} from 'xstate'
|
||||||
|
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
||||||
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { addStartSketch } from 'lang/modifyAst'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
import {
|
||||||
|
recast,
|
||||||
|
parse,
|
||||||
|
Program,
|
||||||
|
PipeExpression,
|
||||||
|
CallExpression,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import {
|
||||||
|
addCloseToPipe,
|
||||||
|
addNewSketchLn,
|
||||||
|
compareVec2Epsilon,
|
||||||
|
} from 'lang/std/sketch'
|
||||||
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||||
|
import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
||||||
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { pathMapToSelections } from 'lang/util'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
||||||
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
|
|
||||||
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
|
state: StateFrom<T>
|
||||||
|
context: ContextFrom<T>
|
||||||
|
send: Prop<InterpreterFrom<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelingMachineContext = createContext(
|
||||||
|
{} as MachineContext<typeof modelingMachine>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ModelingMachineProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const { auth } = useGlobalStateContext()
|
||||||
|
const token = auth?.context?.token
|
||||||
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
|
useSetupEngineManager(streamRef, token)
|
||||||
|
|
||||||
|
const { isShiftDown, editorView } = useStore((s) => ({
|
||||||
|
isShiftDown: s.isShiftDown,
|
||||||
|
editorView: s.editorView,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// const { commands } = useCommandsContext()
|
||||||
|
|
||||||
|
// Settings machine setup
|
||||||
|
// const retrievedSettings = useRef(
|
||||||
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// What should we persist from modeling state? Nothing?
|
||||||
|
// const persistedSettings = Object.assign(
|
||||||
|
// settingsMachine.initialState.context,
|
||||||
|
// JSON.parse(retrievedSettings.current) as Partial<
|
||||||
|
// (typeof settingsMachine)['context']
|
||||||
|
// >
|
||||||
|
// )
|
||||||
|
|
||||||
|
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
||||||
|
// context: persistedSettings,
|
||||||
|
actions: {
|
||||||
|
'Modify AST': () => {},
|
||||||
|
'Update code selection cursors': () => {},
|
||||||
|
'show default planes': () => {
|
||||||
|
kclManager.showPlanes()
|
||||||
|
},
|
||||||
|
'create path': assign({
|
||||||
|
sketchEnginePathId: () => {
|
||||||
|
const sketchUuid = uuidv4()
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: sketchUuid,
|
||||||
|
cmd: {
|
||||||
|
type: 'start_path',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'edit_mode_enter',
|
||||||
|
target: sketchUuid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return sketchUuid
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'AST start new sketch': assign(
|
||||||
|
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
||||||
|
if (!axis) {
|
||||||
|
// Something really weird must have happened for this to happen.
|
||||||
|
console.error('axis is undefined for starting a new sketch')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
if (!segmentId) {
|
||||||
|
// Something really weird must have happened for this to happen.
|
||||||
|
console.error('segmentId is undefined for starting a new sketch')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _addStartSketch = addStartSketch(
|
||||||
|
kclManager.ast,
|
||||||
|
axis,
|
||||||
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||||
|
[
|
||||||
|
roundOff(coords[1].x - coords[0].x),
|
||||||
|
roundOff(coords[1].y - coords[0].y),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const _modifiedAst = _addStartSketch.modifiedAst
|
||||||
|
const _pathToNode = _addStartSketch.pathToNode
|
||||||
|
const newCode = recast(_modifiedAst)
|
||||||
|
const astWithUpdatedSource = parse(newCode)
|
||||||
|
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
||||||
|
astWithUpdatedSource,
|
||||||
|
_pathToNode
|
||||||
|
).node
|
||||||
|
const startProfileAtCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) =>
|
||||||
|
exp.type === 'CallExpression' &&
|
||||||
|
exp.callee.name === 'startProfileAt'
|
||||||
|
)
|
||||||
|
if (startProfileAtCallExp)
|
||||||
|
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||||
|
commandType: 'start_path',
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
const lineCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
||||||
|
)
|
||||||
|
if (lineCallExp)
|
||||||
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
|
commandType: 'extend_path',
|
||||||
|
parentId: sketchEnginePathId,
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sketchPathToNode: _pathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'AST add line segment': async (
|
||||||
|
{ sketchPathToNode, sketchEnginePathId },
|
||||||
|
{ data: { coords, segmentId } }
|
||||||
|
) => {
|
||||||
|
if (!sketchPathToNode) return
|
||||||
|
const lastCoord = coords[coords.length - 1]
|
||||||
|
|
||||||
|
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'path_get_info',
|
||||||
|
path_id: sketchEnginePathId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const firstSegment = pathInfo?.data?.data?.segments.find(
|
||||||
|
(seg: any) => seg.command === 'line_to'
|
||||||
|
)
|
||||||
|
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: firstSegment.command_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
||||||
|
|
||||||
|
const isClose = compareVec2Epsilon(
|
||||||
|
[startPathCoord.x, startPathCoord.y],
|
||||||
|
[lastCoord.x, lastCoord.y]
|
||||||
|
)
|
||||||
|
|
||||||
|
let _modifiedAst: Program
|
||||||
|
if (!isClose) {
|
||||||
|
const newSketchLn = addNewSketchLn({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
to: [lastCoord.x, lastCoord.y],
|
||||||
|
from: [coords[0].x, coords[0].y],
|
||||||
|
fnName: 'line',
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
const _modifiedAst = newSketchLn.modifiedAst
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
||||||
|
const lineCallExp = getNodeFromPath<CallExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
newSketchLn.pathToNode
|
||||||
|
).node
|
||||||
|
if (segmentId)
|
||||||
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
|
commandType: 'extend_path',
|
||||||
|
parentId: sketchEnginePathId,
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_modifiedAst = addCloseToPipe({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true)
|
||||||
|
// updateAst(_modifiedAst, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'sketch exit execute': () => {
|
||||||
|
kclManager.executeAst()
|
||||||
|
},
|
||||||
|
'set tool': () => {}, // TODO
|
||||||
|
'toast extrude failed': () => {
|
||||||
|
toast.error(
|
||||||
|
'Extrude failed, sketches need to be closed, or not already extruded'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
|
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
|
const setSelections = event.data
|
||||||
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||||
|
return { selectionRanges: setSelections.selection }
|
||||||
|
else if (setSelections.selectionType === 'otherSelection')
|
||||||
|
return {
|
||||||
|
selectionRanges: {
|
||||||
|
...selectionRanges,
|
||||||
|
otherSelections: [setSelections.selection],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else if (!editorView) return {}
|
||||||
|
else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||||
|
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||||
|
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||||
|
// because we want to respect the user manually placing the cursor too.
|
||||||
|
|
||||||
|
// for more details on how selections see `src/lib/selections.ts`.
|
||||||
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionWithShift({
|
||||||
|
codeSelection: setSelections.selection,
|
||||||
|
currestSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
|
}
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// same as comment above
|
||||||
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionBatch({
|
||||||
|
selections: setSelections.selection,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Selection contains axis': () => true,
|
||||||
|
'Selection contains edge': () => true,
|
||||||
|
'Selection contains face': () => true,
|
||||||
|
'Selection contains line': () => true,
|
||||||
|
'Selection contains point': () => true,
|
||||||
|
'Selection is not empty': () => true,
|
||||||
|
'Selection is one face': ({ selectionRanges }) => {
|
||||||
|
return !!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
'Get horizontal info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setHorzDistance',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get vertical info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setVertDistance',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintAngleBetween({
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get length info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
||||||
|
{ selectionRanges }
|
||||||
|
)
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get perpendicular distance info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
devTools: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
||||||
|
if (modelingState.nextEvents.includes('Select default plane')) {
|
||||||
|
modelingSend({
|
||||||
|
type: 'Select default plane',
|
||||||
|
data: { planeId: plane_id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [modelingSend, modelingState.nextEvents])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
kclManager.registerExecuteCallback(() => {
|
||||||
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
})
|
||||||
|
}, [modelingSend])
|
||||||
|
|
||||||
|
// useStateMachineCommands({
|
||||||
|
// state: settingsState,
|
||||||
|
// send: settingsSend,
|
||||||
|
// commands,
|
||||||
|
// owner: 'settings',
|
||||||
|
// commandBarMeta: settingsCommandBarMeta,
|
||||||
|
// })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModelingMachineContext.Provider
|
||||||
|
value={{
|
||||||
|
state: modelingState,
|
||||||
|
context: modelingState.context,
|
||||||
|
send: modelingSend,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
|
||||||
|
since realistically it won't ever have generic children that isn't app.tsx */}
|
||||||
|
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</ModelingMachineContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelingMachineProvider
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
|
import CommandBarProvider from './CommandBar'
|
||||||
|
import {
|
||||||
|
NETWORK_CONTENT,
|
||||||
|
NetworkHealthIndicator,
|
||||||
|
} from './NetworkHealthIndicator'
|
||||||
|
|
||||||
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
|
// wrap in router and xState context
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('NetworkHealthIndicator tests', () => {
|
||||||
|
test('Renders the network indicator', () => {
|
||||||
|
render(
|
||||||
|
<TestWrap>
|
||||||
|
<NetworkHealthIndicator />
|
||||||
|
</TestWrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||||
|
|
||||||
|
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||||
|
NETWORK_CONTENT.good
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Responds to network changes', () => {
|
||||||
|
render(
|
||||||
|
<TestWrap>
|
||||||
|
<NetworkHealthIndicator />
|
||||||
|
</TestWrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.offline(window)
|
||||||
|
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||||
|
|
||||||
|
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||||
|
NETWORK_CONTENT.bad
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
112
src/components/NetworkHealthIndicator.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faExclamation,
|
||||||
|
faWifi,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { Popover } from '@headlessui/react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
|
||||||
|
export const NETWORK_CONTENT = {
|
||||||
|
good: 'Network health is good',
|
||||||
|
bad: 'Network issue',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NETWORK_MESSAGES = {
|
||||||
|
offline: 'You are offline',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkHealthIndicator = () => {
|
||||||
|
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||||
|
const hasIssues = [...networkIssues.values()].length > 0
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const offlineListener = () =>
|
||||||
|
setNetworkIssues((issues) => {
|
||||||
|
return [
|
||||||
|
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||||
|
NETWORK_MESSAGES.offline,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
window.addEventListener('offline', offlineListener)
|
||||||
|
|
||||||
|
const onlineListener = () =>
|
||||||
|
setNetworkIssues((issues) => {
|
||||||
|
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||||
|
})
|
||||||
|
window.addEventListener('online', onlineListener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('offline', offlineListener)
|
||||||
|
window.removeEventListener('online', onlineListener)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="relative">
|
||||||
|
<Popover.Button
|
||||||
|
className={
|
||||||
|
'p-0 border-none relative ' +
|
||||||
|
(hasIssues
|
||||||
|
? 'focus-visible:outline-destroy-80'
|
||||||
|
: 'focus-visible:outline-succeed-80')
|
||||||
|
}
|
||||||
|
data-testid="network-toggle"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Network Health</span>
|
||||||
|
<ActionIcon
|
||||||
|
icon={faWifi}
|
||||||
|
iconClassName={
|
||||||
|
hasIssues
|
||||||
|
? 'text-destroy-80 dark:text-destroy-30'
|
||||||
|
: 'text-succeed-80 dark:text-succeed-30'
|
||||||
|
}
|
||||||
|
bgClassName={
|
||||||
|
hasIssues
|
||||||
|
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||||
|
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Popover.Button>
|
||||||
|
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||||
|
{!hasIssues ? (
|
||||||
|
<span
|
||||||
|
className="flex items-center justify-center gap-1 px-4"
|
||||||
|
data-testid="network-good"
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon={faCheck}
|
||||||
|
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
||||||
|
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||||
|
/>
|
||||||
|
{NETWORK_CONTENT.good}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||||
|
<span
|
||||||
|
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||||
|
data-testid="network-bad"
|
||||||
|
>
|
||||||
|
{NETWORK_CONTENT.bad}
|
||||||
|
{networkIssues.length > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
{networkIssues.map((issue) => (
|
||||||
|
<li
|
||||||
|
key={issue}
|
||||||
|
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon={faExclamation}
|
||||||
|
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||||
|
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||||
|
className="ml-4"
|
||||||
|
/>
|
||||||
|
<p className="flex-1 mr-4">{issue}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Popover.Panel>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri'
|
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
|
|
||||||
export const OpenFileButton = () => {
|
|
||||||
const { setCode } = useStore((s) => ({
|
|
||||||
setCode: s.setCode,
|
|
||||||
}))
|
|
||||||
const handleClick = async () => {
|
|
||||||
const selected = await open({
|
|
||||||
multiple: false,
|
|
||||||
directory: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'CAD',
|
|
||||||
extensions: ['toml'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
if (Array.isArray(selected)) {
|
|
||||||
// User selected multiple files
|
|
||||||
// We should not get here, since multiple is false.
|
|
||||||
} else if (selected === null) {
|
|
||||||
// User cancelled the selection
|
|
||||||
// Do nothing.
|
|
||||||
} else {
|
|
||||||
// User selected a single file
|
|
||||||
// We want to invoke our command to read the file.
|
|
||||||
const json: string = await invoke('read_toml', { path: selected })
|
|
||||||
const packageDetails = JSON.parse(json).package
|
|
||||||
if (packageDetails.main) {
|
|
||||||
const absPath = [
|
|
||||||
...selected.split('/').slice(0, -1),
|
|
||||||
packageDetails.main,
|
|
||||||
].join('/')
|
|
||||||
const file: string = await invoke('read_txt_file', { path: absPath })
|
|
||||||
setCode(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <button onClick={() => handleClick()}>Open File</button>
|
|
||||||
}
|
|
182
src/components/ProjectCard.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { FormEvent, useEffect, useState } from 'react'
|
||||||
|
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faPenAlt,
|
||||||
|
faTrashAlt,
|
||||||
|
faX,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
|
||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
function ProjectCard({
|
||||||
|
project,
|
||||||
|
handleRenameProject,
|
||||||
|
handleDeleteProject,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
project: ProjectWithEntryPointMetadata
|
||||||
|
handleRenameProject: (
|
||||||
|
e: FormEvent<HTMLFormElement>,
|
||||||
|
f: ProjectWithEntryPointMetadata
|
||||||
|
) => Promise<void>
|
||||||
|
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
|
||||||
|
}) {
|
||||||
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
|
const [numberOfParts, setNumberOfParts] = useState(1)
|
||||||
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
|
||||||
|
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayedTime(date: Date) {
|
||||||
|
const startOfToday = new Date()
|
||||||
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
|
return date.getTime() < startOfToday.getTime()
|
||||||
|
? date.toLocaleDateString()
|
||||||
|
: date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getNumberOfParts() {
|
||||||
|
const { kclFileCount, kclDirCount } = getPartsCount(
|
||||||
|
await readProject(project.path)
|
||||||
|
)
|
||||||
|
setNumberOfParts(kclFileCount)
|
||||||
|
setNumberOfFolders(kclDirCount)
|
||||||
|
}
|
||||||
|
getNumberOfParts()
|
||||||
|
}, [project.path])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...props}
|
||||||
|
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-90 hover:border-chalkboard-30 dark:hover:border-chalkboard-80"
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<form onSubmit={handleSave} className="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
className="dark:bg-chalkboard-80 dark:border-chalkboard-40 min-w-0 p-1"
|
||||||
|
type="text"
|
||||||
|
id="newProjectName"
|
||||||
|
name="newProjectName"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
defaultValue={project.name}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
icon={{ icon: faCheck, size: 'sm' }}
|
||||||
|
className="!p-0"
|
||||||
|
></ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faX, size: 'sm' }}
|
||||||
|
className="!p-0"
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="p-1 flex flex-col h-full gap-2">
|
||||||
|
<Link
|
||||||
|
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
|
||||||
|
className="flex-1 text-liquid-100"
|
||||||
|
>
|
||||||
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
|
</Link>
|
||||||
|
<span className="text-chalkboard-60 text-xs">
|
||||||
|
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
||||||
|
{numberOfFolders > 0 &&
|
||||||
|
`/ ${numberOfFolders} folder${
|
||||||
|
numberOfFolders === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
|
</span>
|
||||||
|
<span className="text-chalkboard-60 text-xs">
|
||||||
|
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
|
||||||
|
</span>
|
||||||
|
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faPenAlt, size: 'sm' }}
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className="!p-0"
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: faTrashAlt,
|
||||||
|
size: 'sm',
|
||||||
|
bgClassName: 'bg-destroy-80 hover:bg-destroy-70',
|
||||||
|
iconClassName:
|
||||||
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||||
|
}}
|
||||||
|
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
|
onClick={() => setIsConfirmingDelete(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={isConfirmingDelete}
|
||||||
|
onClose={() => setIsConfirmingDelete(false)}
|
||||||
|
className="relative z-50"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
|
||||||
|
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
|
||||||
|
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
|
||||||
|
Delete File
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
This will permanently delete "{project.name || 'this file'}".
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<p className="my-4">
|
||||||
|
Are you sure you want to delete "{project.name || 'this file'}
|
||||||
|
"? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteProject(project)
|
||||||
|
setIsConfirmingDelete(false)
|
||||||
|
}}
|
||||||
|
icon={{
|
||||||
|
icon: faTrashAlt,
|
||||||
|
bgClassName: 'bg-destroy-80',
|
||||||
|
iconClassName:
|
||||||
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||||
|
}}
|
||||||
|
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => setIsConfirmingDelete(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCard
|
98
src/components/ProjectSidebarMenu.test.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
|
import CommandBarProvider from './CommandBar'
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const projectWellFormed = {
|
||||||
|
name: 'Simple Box',
|
||||||
|
path: '/some/path/Simple Box',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'main.kcl',
|
||||||
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entrypointMetadata: {
|
||||||
|
accessedAt: now,
|
||||||
|
blksize: 32,
|
||||||
|
blocks: 32,
|
||||||
|
createdAt: now,
|
||||||
|
dev: 1,
|
||||||
|
gid: 1,
|
||||||
|
ino: 1,
|
||||||
|
isDir: false,
|
||||||
|
isFile: true,
|
||||||
|
isSymlink: false,
|
||||||
|
mode: 1,
|
||||||
|
modifiedAt: now,
|
||||||
|
nlink: 1,
|
||||||
|
permissions: { readonly: false, mode: 1 },
|
||||||
|
rdev: 1,
|
||||||
|
size: 32,
|
||||||
|
uid: 1,
|
||||||
|
},
|
||||||
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
|
|
||||||
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
|
test('Renders the project name', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
|
||||||
|
|
||||||
|
expect(screen.getByTestId('projectName')).toHaveTextContent(
|
||||||
|
projectWellFormed.name
|
||||||
|
)
|
||||||
|
expect(screen.getByTestId('createdAt')).toHaveTextContent(
|
||||||
|
`Created ${now.toLocaleDateString()}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Renders app name if given no project', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu />
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
|
||||||
|
|
||||||
|
expect(screen.getByTestId('projectName')).toHaveTextContent(
|
||||||
|
'KittyCAD Modeling App'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Renders as a link if set to do so', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<ProjectSidebarMenu
|
||||||
|
project={projectWellFormed}
|
||||||
|
renderAsLink={true}
|
||||||
|
/>
|
||||||
|
</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('project-sidebar-link')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('project-sidebar-link-name')).toHaveTextContent(
|
||||||
|
projectWellFormed.name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|