Compare commits
281 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
826ad267b4 | |||
2476c12480 | |||
ae460ed02f | |||
3a93839a2d | |||
dbb94d7e95 | |||
968a67e654 | |||
8ebb8b8b94 | |||
f9259aa869 | |||
a6f92e358b | |||
35e4727856 | |||
a986f76e70 | |||
7a537eea8e | |||
ca985dd1a8 | |||
1cba48f513 | |||
e7a70a9735 | |||
6e14dbaf77 | |||
62dc07e117 | |||
391f4ba206 | |||
4c5178ea5c | |||
8c5d7bf648 | |||
231371fb16 | |||
cd4672c98d | |||
c80dd44c59 | |||
c190122240 | |||
36ea343860 | |||
1962af760c | |||
8be7805ac2 | |||
64772f5c98 | |||
5419039fae | |||
aabb88ee45 | |||
7408ba50dd | |||
7181ff0c33 | |||
7ae1b66855 | |||
c31f1ad98b | |||
894bddb369 | |||
94918ccb2e | |||
be7605cdef | |||
aca9b9226c | |||
7312035818 | |||
956e4c46c1 | |||
0d010b60e5 | |||
6838e96723 | |||
c2210835ea | |||
d1e7cb23a1 | |||
9cd3845975 | |||
ca2634d523 | |||
48f1d5e623 | |||
87aecf7f50 | |||
b89faa4a28 | |||
1666e17ca5 | |||
5d90c0488f | |||
11cc85a9e8 | |||
3383becc0f | |||
4c65d5b2ef | |||
59fa51d75a | |||
3fc4d71a1e | |||
317dc6d0b2 | |||
4f8fe2d155 | |||
cda301997e | |||
a70399bacf | |||
3510abfcb9 | |||
fb3c34d5f3 | |||
7289965916 | |||
2d3c73d46a | |||
dd3117cf03 | |||
efa3bc7ac6 | |||
0858d32c1e | |||
e7c1554129 | |||
6eaa0e0852 |
6
.env.development
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
||||||
|
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
||||||
|
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||||
|
VITE_KC_SKIP_AUTH=false
|
||||||
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
|
VITE_KC_SENTRY_DSN=
|
6
.env.production
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||||
|
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||||
|
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||||
|
VITE_KC_SKIP_AUTH=false
|
||||||
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
src/wasm-lib/*
|
16
.eslintrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"css-modules"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest",
|
||||||
|
"plugin:css-modules/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
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'
|
73
.github/workflows/build.yml
vendored
@ -1,73 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: install ubuntu system dependencies
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev libwebkit2gtk-4.0-dev
|
|
||||||
# libgdk-pixbuf2.0-dev libsoup2.4-dev libjavascriptcoregtk-4.0-dev
|
|
||||||
- name: Sync node version and setup cache
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18.x'
|
|
||||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
|
||||||
- run: yarn install
|
|
||||||
- name: Rust setup
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
- 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: add missing import
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
yarn add-missing-import
|
|
||||||
pwd
|
|
||||||
ls -la
|
|
||||||
yarn fmt
|
|
||||||
# - name: tauri build
|
|
||||||
# shell: bash
|
|
||||||
# run: yarn tauri build
|
|
||||||
# - uses: actions/upload-artifact@v2
|
|
||||||
# with:
|
|
||||||
# name: tauri-app
|
|
||||||
# path: src-tauri/target/release/bundle
|
|
||||||
- name: Build the app
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CI: false
|
|
||||||
with:
|
|
||||||
tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
|
|
||||||
releaseName: 'App Name v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
|
|
||||||
releaseBody: 'See the assets to download and install this version.'
|
|
||||||
releaseDraft: true
|
|
||||||
prerelease: false
|
|
47
.github/workflows/cargo-build.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
|
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
|
57
.github/workflows/cargo-clippy.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
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
|
37
.github/workflows/cargo-criterion.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
|
|
45
.github/workflows/cargo-fmt.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
|
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
|
61
.github/workflows/cargo-test.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
|
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}}
|
||||||
|
|
277
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-format:
|
||||||
|
runs-on: 'ubuntu-20.04'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn fmt-check
|
||||||
|
|
||||||
|
check-types:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
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
|
||||||
|
|
||||||
|
build-test-web:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.export_version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
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
|
||||||
|
|
||||||
|
- id: export_version
|
||||||
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-apps:
|
||||||
|
needs: [check-format, build-test-web, check-types]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, ubuntu-20.04, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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'
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
|
- 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: Fix format
|
||||||
|
run: yarn fmt
|
||||||
|
|
||||||
|
- name: install apple silicon target mac
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Prepare Windows certificate and variables
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
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 Windows certicate with SSM KSP
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
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 and sign the app for the current platform
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
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 }}
|
||||||
|
with:
|
||||||
|
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
|
||||||
|
|
||||||
|
publish-apps-release:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
needs: [build-test-web, build-apps]
|
||||||
|
env:
|
||||||
|
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||||
|
PUB_DATE: ${{ github.event.release.created_at }}
|
||||||
|
NOTES: ${{ github.event.release.body }}
|
||||||
|
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://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||||
|
jq --null-input \
|
||||||
|
--arg version "v${VERSION_NO_V}" \
|
||||||
|
--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://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||||
|
jq --null-input \
|
||||||
|
--arg version "v${VERSION_NO_V}" \
|
||||||
|
--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: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||||
|
|
||||||
|
- name: Upload update endpoint to public bucket
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
|
with:
|
||||||
|
path: last_update.json
|
||||||
|
destination: dl.kittycad.io/releases/modeling-app
|
||||||
|
|
||||||
|
- name: Upload download endpoint to public bucket
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
|
with:
|
||||||
|
path: last_download.json
|
||||||
|
destination: dl.kittycad.io/releases/modeling-app
|
||||||
|
|
||||||
|
- name: Upload release files to Github
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: artifact/*/*itty*
|
20
.github/workflows/test.yml
vendored
@ -1,20 +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@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '16.x'
|
|
||||||
- run: yarn install
|
|
||||||
- run: yarn build:wasm:ci
|
|
||||||
- run: yarn simpleserver:ci
|
|
||||||
- run: yarn test:nowatch
|
|
||||||
- run: yarn test:cov
|
|
||||||
- run: yarn test:rust
|
|
29
.github/workflows/update-dev-branch.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: update-dev-branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/update-dev-branch.yml
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-branch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
# checkout our branch
|
||||||
|
git checkout dev || git checkout -b dev
|
||||||
|
# fetch origin
|
||||||
|
git fetch origin
|
||||||
|
# reset to main
|
||||||
|
git reset --hard origin/main
|
||||||
|
# force push it
|
||||||
|
git push -f origin dev
|
5
.gitignore
vendored
@ -22,6 +22,11 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
src/wasm-lib/.idea
|
||||||
|
|
||||||
# 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
|
||||||
|
4
.husky/pre-push
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn fmt-check
|
9
.prettierignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
build
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Ignore Rust projects:
|
||||||
|
*.rs
|
||||||
|
target
|
||||||
|
src/wasm-lib/pkg
|
||||||
|
src/wasm-lib/kcl/bindings
|
4
.vscode/settings.json
vendored
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"geos"
|
"geos"
|
||||||
]
|
],
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
}
|
}
|
149
README.md
@ -1,48 +1,159 @@
|
|||||||
## 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/)
|
||||||
|
- 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. Then, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
then
|
|
||||||
|
followed by:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn build:wasm
|
yarn build:wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||||
|
|
||||||
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.
|
## Tauri
|
||||||
|
|
||||||
|
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.)
|
||||||
|
|
||||||
|
To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access to the devtools.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION=x.y.z yarn run bump-jsons
|
||||||
|
```
|
||||||
|
|
||||||
|
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||||
|
|
||||||
|
2. Merge the PR
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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
Normal file
After Width: | Height: | Size: 207 KiB |
3
babel.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ["@babel/preset-env"],
|
||||||
|
}
|
19955
docs/kcl/std.json
Normal file
3871
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).
|
22
index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="An open-source CAD modeling tool from the future by KittyCAD."
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
|
<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>
|
||||||
|
</head>
|
||||||
|
<body class="body-bg">
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root" class="h-screen overflow-y-auto"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
122
package.json
@ -1,61 +1,82 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.1.0",
|
"version": "0.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/autocomplete": "^6.9.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",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@react-three/drei": "^9.42.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@react-three/fiber": "^8.9.1",
|
"@kittycad/lib": "^0.0.38",
|
||||||
|
"@lezer/javascript": "^1.4.7",
|
||||||
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
|
"@sentry/react": "^7.65.0",
|
||||||
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"@types/jest": "^27.0.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/react-codemirror": "^4.15.1",
|
"@uiw/react-codemirror": "^4.21.13",
|
||||||
"allotment": "^1.17.0",
|
"@xstate/react": "^3.2.2",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"formik": "^2.4.3",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
|
"json-rpc-2.0": "^1.6.0",
|
||||||
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
"react-modal-promise": "^1.0.2",
|
"react-modal-promise": "^1.0.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-router-dom": "^6.14.2",
|
||||||
"sketch-helpers": "^0.0.2",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.0.4",
|
"swr": "^2.0.4",
|
||||||
"three": "^0.146.0",
|
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
||||||
|
"toml": "^3.0.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"util": "^0.12.5",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"wasm-pack": "^0.11.1",
|
"vitest": "^0.34.1",
|
||||||
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.3",
|
||||||
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
|
"ws": "^8.13.0",
|
||||||
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.1.4"
|
"zustand": "^4.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "vite",
|
||||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm:ci && react-scripts build",
|
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||||
"build:local": "react-scripts build",
|
"build:local": "vite build",
|
||||||
"build:both": "react-scripts build",
|
"build:both": "vite build",
|
||||||
"build:both:local": "yarn build:wasm && react-scripts build",
|
"build:both:local": "yarn build:wasm && vite build",
|
||||||
"test": "react-scripts test",
|
"pretest": "yarn remove-importmeta",
|
||||||
"test:nowatch": "react-scripts test --watchAll=false",
|
"test": "vitest --mode development",
|
||||||
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
|
"test:nowatch": "vitest run --mode development",
|
||||||
"test:cov": "react-scripts test --watchAll=false --coverage=true",
|
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
||||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
"test:cov": "vitest run --coverage --mode development",
|
||||||
"simpleserver": "http-server ./public --cors -p 3000",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"eject": "react-scripts eject",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src/**/*.{ts,tsx,js}",
|
"fmt": "prettier --write ./src",
|
||||||
"remove-importmeta": "sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"",
|
"fmt-check": "prettier --check ./src",
|
||||||
"remove-importmeta:ci": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"",
|
"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",
|
||||||
"add-missing-import": "echo \"import util from 'util'; if (typeof window !== 'undefined' && !window.TextEncoder) { window.TextEncoder = util.TextEncoder; window.TextDecoder = util.TextDecoder}\" | cat - ./src/wasm-lib/pkg/wasm_lib.js > temp && mv temp ./src/wasm-lib/pkg/wasm_lib.js",
|
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||||
"build:wasm:ci": "mkdir src/wasm-lib/pkg; cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cd ../../ && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn remove-importmeta:ci && yarn add-missing-import && yarn fmt",
|
"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\"",
|
||||||
"build:wasm": "mkdir src/wasm-lib/pkg; cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cd ../../ && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn remove-importmeta && yarn add-missing-import && yarn fmt"
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
},
|
"lint": "eslint --fix src",
|
||||||
"jest": {
|
"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"
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"node_modules/(?!(three|allotment)/)"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
@ -63,18 +84,6 @@
|
|||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"never"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@ -88,14 +97,31 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@babel/preset-env": "^7.22.9",
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/three": "^0.146.0",
|
"@types/debounce": "^1.2.1",
|
||||||
|
"@types/isomorphic-fetch": "^0.0.36",
|
||||||
|
"@types/react-modal": "^3.16.0",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
|
"@types/wicg-file-system-access": "^2020.9.6",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"@vitest/coverage-istanbul": "^0.34.1",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
"eslint": "^8.44.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-css-modules": "^2.11.0",
|
||||||
|
"happy-dom": "^10.8.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.4",
|
||||||
|
"vite": "^4.4.3",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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: 3.8 KiB After Width: | Height: | Size: 15 KiB |
@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
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 |
37
public/kitt-8bit-winking.svg
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 31V30H1V29H0V8H1V7H2V6H3V5H4V4H21V5H22V6H23V7H24V8H25V29H24V30H22V31H19V32H20V34H14V32H15V31H10V32H11V34H5V32H6V31H3Z" fill="#101412"/>
|
||||||
|
<path d="M6 31V29.5H10V31H9V32H10V33H6V32H7V31H6Z" fill="#4B4862"/>
|
||||||
|
<path d="M15 31V29.5H19V31H18V32H19V33H15V32H16V31H15Z" fill="#4B4862"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 24.5V29H3V30H22V29H24V24.5H1ZM9 29V27H16V29H9Z" fill="#9BADB7"/>
|
||||||
|
<path d="M1 25V23H24V25H1Z" fill="#BECAD0"/>
|
||||||
|
<path d="M2 27V26H7V27H2Z" fill="#2B3E48"/>
|
||||||
|
<path d="M4 29V28H7V29H4Z" fill="#2B3E48"/>
|
||||||
|
<path d="M18 27V26H19V27H18Z" fill="#2B3E48"/>
|
||||||
|
<path d="M20 27V26H21V27H20Z" fill="#2B3E48"/>
|
||||||
|
<path d="M22 27V26H23V27H22Z" fill="#2B3E48"/>
|
||||||
|
<path d="M18 29V28H21V29H18Z" fill="#2B3E48"/>
|
||||||
|
<path d="M22 7V6H21V5H4V6H3V7H2V8H1V10H24V8H23V7H22Z" fill="#FBF580"/>
|
||||||
|
<path d="M1 24V22H24V24H1Z" fill="#AEAA4C"/>
|
||||||
|
<path d="M24 9H1V23H24V9Z" fill="#E5E3A1"/>
|
||||||
|
<path d="M4 12V11H21V12H22V20H21V21H4V20H3V12H4Z" fill="#1F2320"/>
|
||||||
|
<rect x="10" y="5" width="5" height="2" fill="#AEAA4C"/>
|
||||||
|
<path d="M16 13V12H18V16H17L16 13Z" fill="#DBFF3C"/>
|
||||||
|
<path d="M11 16H14V17H13V19H16V18H17V19H16V20H9V19H8V18H9V19H12V17H11V16Z" fill="#DBFF3C"/>
|
||||||
|
<path d="M9 15V14H6V15H5V14H6V13H9V14H10V15H9Z" fill="#DBFF3C"/>
|
||||||
|
<path d="M4 7V6H5V4H6V2H7V1H8V2H9V4H10V6H11V7H4Z" fill="#DBFF3C"/>
|
||||||
|
<path d="M21 6V7H14V6H15V4H16V2H17V1H18V2H19V4H20V6H21Z" fill="#DBFF3C"/>
|
||||||
|
<path d="M16 2V0H19V2H20V4H21V5.5H20V4H19V2H18V1H17V2H16V4H15V5.5H14V4H15V2H16Z" fill="#92C51B"/>
|
||||||
|
<path d="M6 2V0H9V2H10V4H11V5.5H10V4H9V2H8V1H7V2H6V4H5V5.5H4V4H5V2H6Z" fill="#92C51B"/>
|
||||||
|
<rect x="11" y="6" width="3" height="1" fill="#D0CC6A"/>
|
||||||
|
<path d="M16 7V6H17V5H18V6H19V7H16Z" fill="#76AA1D"/>
|
||||||
|
<path d="M7 6V5H8V6H9V7H6V6H7Z" fill="#76AA1D"/>
|
||||||
|
<path d="M21 7V6H20V5H21V6H22V7H21Z" fill="#76AA1D"/>
|
||||||
|
<path d="M4 6V7H3V6H4V5H5V6H4Z" fill="#76AA1D"/>
|
||||||
|
<path d="M10 5H11V6H12V7H11V6H10V5Z" fill="#76AA1D"/>
|
||||||
|
<path d="M14 5H15V6H14V7H13V6H14V5Z" fill="#76AA1D"/>
|
||||||
|
<path d="M17 13H16V16H17V13Z" fill="#92C51B"/>
|
||||||
|
<path d="M2 25V23H1V25H2Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M23 25V23H24V25H23Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M4 24V23H7V24H4Z" fill="#D56161"/>
|
||||||
|
<path d="M4 25V24H7V25H4Z" fill="#AC3232"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
18
public/kitt-arcade-winking.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 31V30H1V29H0V8H1V7H2V6H3V5H4V3H5V2H6V1H7V0H8V1H9V2H10V3H11V4H14V3H15V2H16V1H17V0H18V1H19V2H20V3H21V5H22V6H23V7H24V8H25V29H24V30H22V31H19V32H20V34H14V32H15V31H10V32H11V34H5V32H6V31H3Z" fill="#101412"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 5H14V4H15V3H16V2H17V1H18V2H19V3H20V5H21V6H22V7H23V8H24V9V10V23V24V25V29H22V30H18V31V32H19V33H15V32H16V31V30H9V31V32H10V33H6V32H7V31V30H3V29H1V25V24V23V10V9V8H2V7H3V6H4V5H5V3H6V2H7V1H8V2H9V3H10V4H11V5ZM2 26V27H7V26H2ZM4 28V29H7V28H4ZM18 27V26H19V27H18ZM20 26V27H21V26H20ZM22 27V26H23V27H22ZM18 28V29H21V28H18ZM9 27H16V29H9V27Z" fill="#D0FF00"/>
|
||||||
|
<path d="M1 24V23H24V24H1Z" fill="#B1E515"/>
|
||||||
|
<path d="M4 12V11H21V12H22V20H21V21H4V20H3V12H4Z" fill="#1F2320"/>
|
||||||
|
<path d="M16 16V12H18V16H16Z" fill="#D0FF00"/>
|
||||||
|
<path d="M11 16H14V17H13V19H16V18H17V19H16V20H9V19H8V18H9V19H12V17H11V16Z" fill="#D0FF00"/>
|
||||||
|
<path d="M9 15V14H6V15H5V14H6V13H9V14H10V15H9Z" fill="#D0FF00"/>
|
||||||
|
<path d="M22 8V7H23V8H22Z" fill="#B1E515"/>
|
||||||
|
<path d="M3 8V7H2V8H3Z" fill="#B1E515"/>
|
||||||
|
<path d="M21 7V6H22V7H21Z" fill="#92C51B"/>
|
||||||
|
<path d="M4 7V6H3V7H4Z" fill="#92C51B"/>
|
||||||
|
<rect x="12" y="5" width="1" height="2" fill="#B1E515"/>
|
||||||
|
<path d="M16 7V6H17V5H18V6H19V7H16Z" fill="#101412"/>
|
||||||
|
<path d="M7 6V5H8V6H9V7H6V6H7Z" fill="#101412"/>
|
||||||
|
<path d="M4 24V23H7V24H4Z" fill="#92C51B"/>
|
||||||
|
<rect x="11" y="5" width="3" height="1" fill="#92C51B"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
45
public/kittycad-logomark-light.svg
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<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 d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
|
<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="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
|
<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="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
|
<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="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
|
<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="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
|
<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="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"/>
|
||||||
|
<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>
|
After Width: | Height: | Size: 5.9 KiB |
45
public/kittycad-logomark.svg
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<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 d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
|
<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="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
|
<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="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
|
<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="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
|
<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="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
|
<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="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"/>
|
||||||
|
<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>
|
After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 36 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "KCMA",
|
||||||
"name": "Create React App Sample",
|
"name": "KittyCAD Modeling App",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
BIN
public/onboarding-bracket-dark.png
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
public/onboarding-bracket.png
Normal file
After Width: | Height: | Size: 142 KiB |
26
public/roadmap.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
## 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)
|
||||||
|
|
1605
src-tauri/Cargo.lock
generated
@ -12,15 +12,21 @@ 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.4.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
anyhow = "1"
|
||||||
|
kittycad = "0.2.25"
|
||||||
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.3.0", features = [] }
|
serde_json = "1.0"
|
||||||
|
tauri = { version = "1.4.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||||
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
tokio = { version = "1.32.0", features = ["time"] }
|
||||||
|
toml = "0.8.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||||
# DO NOT REMOVE!!
|
# DO NOT REMOVE!!
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 122 KiB |
@ -1,8 +1,151 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
use std::io::Read;
|
||||||
tauri::Builder::default()
|
|
||||||
.run(tauri::generate_context!())
|
use anyhow::Result;
|
||||||
.expect("error while running tauri application");
|
use oauth2::TokenResponse;
|
||||||
|
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.
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_toml(path: &str) -> Result<String, InvokeError> {
|
||||||
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let value =
|
||||||
|
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This command returns a string that is the contents of a file at the path.
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
||||||
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This command instantiates a new window with auth.
|
||||||
|
/// The string returned from this method is the access token.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
||||||
|
println!("Logging in...");
|
||||||
|
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
||||||
|
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
// We can hardcode the client ID.
|
||||||
|
// This value is safe to be embedded in version control.
|
||||||
|
// This is the client ID of the KittyCAD app.
|
||||||
|
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
|
||||||
|
let auth_client = oauth2::basic::BasicClient::new(
|
||||||
|
oauth2::ClientId::new(client_id),
|
||||||
|
None,
|
||||||
|
oauth2::AuthUrl::new(format!("{host}/authorize"))
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
|
Some(
|
||||||
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.set_auth_type(oauth2::AuthType::RequestBody)
|
||||||
|
.set_device_authorization_url(device_auth_url);
|
||||||
|
|
||||||
|
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
|
||||||
|
.exchange_device_code()
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
||||||
|
.request_async(oauth2::reqwest::async_http_client)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
|
||||||
|
let Some(auth_uri) = details.verification_uri_complete() else {
|
||||||
|
return Err(InvokeError::from("getting the verification uri failed"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the system browser with the auth_uri.
|
||||||
|
// We do this in the browser and not a seperate window because we want 1password and
|
||||||
|
// other crap to work well.
|
||||||
|
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
|
||||||
|
// Wait for the user to login.
|
||||||
|
let token = auth_client
|
||||||
|
.exchange_device_access_token(&details)
|
||||||
|
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
||||||
|
.access_token()
|
||||||
|
.secret()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
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() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
#[cfg(debug_assertions)] // only include this code on debug builds
|
||||||
|
{
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
// comment out the below if you don't devtools to open everytime.
|
||||||
|
// it's useful because otherwise devtools shuts everytime rust code changes.
|
||||||
|
window.open_devtools();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_user,
|
||||||
|
login,
|
||||||
|
read_toml,
|
||||||
|
read_txt_file
|
||||||
|
])
|
||||||
|
.plugin(tauri_plugin_fs_extra::init())
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,44 @@
|
|||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "untitled-app",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.1.0"
|
"version": "0.9.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"all": false
|
"all": false,
|
||||||
|
"dialog": {
|
||||||
|
"all": true,
|
||||||
|
"ask": true,
|
||||||
|
"confirm": true,
|
||||||
|
"message": true,
|
||||||
|
"open": true,
|
||||||
|
"save": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"scope": [
|
||||||
|
"$HOME/**/*",
|
||||||
|
"$APPDATA/**/*"
|
||||||
|
],
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"request": true,
|
||||||
|
"scope": [
|
||||||
|
"https://dev.kittycad.io/*",
|
||||||
|
"https://kittycad.io/*",
|
||||||
|
"https://api.dev.kittycad.io/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"shell": {
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -29,7 +61,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"identifier": "untitled-app",
|
"identifier": "io.kittycad.modeling-app",
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": null,
|
"entitlements": null,
|
||||||
@ -42,24 +74,29 @@
|
|||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": ""
|
"timestampUrl": "http://timestamp.digicert.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": false
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 600,
|
"height": 1200,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "untitled-app",
|
"title": "KittyCAD Modeling",
|
||||||
"width": 800
|
"width": 1800
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
7
src-tauri/tauri.macos.conf.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"package": {
|
||||||
|
"productName": "KittyCAD Modeling"
|
||||||
|
}
|
||||||
|
}
|
7
src-tauri/tauri.windows.conf.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"package": {
|
||||||
|
"productName": "KittyCAD Modeling"
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import React from 'react'
|
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import App from './App'
|
import { App } from './App'
|
||||||
|
import { describe, test, vi } from 'vitest'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
|
||||||
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,8 +15,38 @@ let listener: ((rect: any) => void) | undefined = undefined
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
describe('App tests', () => {
|
||||||
render(<App />)
|
test('Renders the modeling app screen, including "Variables" pane.', () => {
|
||||||
const linkElement = screen.getByText(/Variables/i)
|
vi.mock('react-router-dom', async () => {
|
||||||
expect(linkElement).toBeInTheDocument()
|
const actual = (await vi.importActual('react-router-dom')) as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useParams: () => ({ id: 'new' }),
|
||||||
|
useLoaderData: () => ({ code: null }),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
render(
|
||||||
|
<TestWrap>
|
||||||
|
<App />
|
||||||
|
</TestWrap>
|
||||||
|
)
|
||||||
|
const linkElement = screen.getByText(/Variables/i)
|
||||||
|
expect(linkElement).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
|
// wrap in router and xState context
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
534
src/App.tsx
@ -1,266 +1,318 @@
|
|||||||
import { useRef, useState, useEffect } from 'react'
|
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
||||||
import { Canvas } from '@react-three/fiber'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { Allotment } from 'allotment'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { OrbitControls, OrthographicCamera } from '@react-three/drei'
|
import { PaneType, useStore } from './useStore'
|
||||||
import { asyncLexer } from './lang/tokeniser'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { executor, ExtrudeGroup, SketchGroup } from './lang/executor'
|
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
|
||||||
import { ViewUpdate } from '@codemirror/view'
|
|
||||||
import {
|
|
||||||
lineHighlightField,
|
|
||||||
addLineHighlight,
|
|
||||||
} from './editor/highlightextension'
|
|
||||||
import { useStore } from './useStore'
|
|
||||||
import { Toolbar } from './Toolbar'
|
|
||||||
import { BasePlanes } from './components/BasePlanes'
|
|
||||||
import { SketchPlane } from './components/SketchPlane'
|
|
||||||
import { Logs } from './components/Logs'
|
|
||||||
import { AxisIndicator } from './components/AxisIndicator'
|
|
||||||
import { RenderViewerArtifacts } from './components/RenderViewerArtifacts'
|
|
||||||
import { PanelHeader } from './components/PanelHeader'
|
|
||||||
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 { EngineCommand } from './lang/std/engineConnection'
|
||||||
|
import { throttle } from './lib/utils'
|
||||||
|
import { AppHeader } from './components/AppHeader'
|
||||||
|
import { Resizable } from 're-resizable'
|
||||||
|
import {
|
||||||
|
faCode,
|
||||||
|
faCodeCommit,
|
||||||
|
faSquareRootVariable,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
|
import { isTauri } from './lib/isTauri'
|
||||||
|
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 { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
|
|
||||||
const OrrthographicCamera = OrthographicCamera as any
|
export function App() {
|
||||||
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
|
|
||||||
function App() {
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const cam = useRef()
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
editorView,
|
|
||||||
setEditorView,
|
|
||||||
setSelectionRanges,
|
|
||||||
selectionRanges: selectionRange,
|
|
||||||
guiMode,
|
|
||||||
lastGuiMode,
|
|
||||||
addLog,
|
|
||||||
code,
|
|
||||||
setCode,
|
setCode,
|
||||||
setAst,
|
engineCommandManager,
|
||||||
setError,
|
buttonDownInStream,
|
||||||
errorState,
|
openPanes,
|
||||||
setProgramMemory,
|
setOpenPanes,
|
||||||
resetLogs,
|
didDragInStream,
|
||||||
selectionRangeTypeMap,
|
streamDimensions,
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
executeAst,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
|
||||||
setEditorView: s.setEditorView,
|
|
||||||
setSelectionRanges: s.setSelectionRanges,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
guiMode: s.guiMode,
|
guiMode: s.guiMode,
|
||||||
setGuiMode: s.setGuiMode,
|
setGuiMode: s.setGuiMode,
|
||||||
addLog: s.addLog,
|
|
||||||
code: s.code,
|
|
||||||
setCode: s.setCode,
|
setCode: s.setCode,
|
||||||
setAst: s.setAst,
|
engineCommandManager: s.engineCommandManager,
|
||||||
lastGuiMode: s.lastGuiMode,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
setError: s.setError,
|
openPanes: s.openPanes,
|
||||||
errorState: s.errorState,
|
setOpenPanes: s.setOpenPanes,
|
||||||
setProgramMemory: s.setProgramMemory,
|
didDragInStream: s.didDragInStream,
|
||||||
resetLogs: s.resetLogs,
|
streamDimensions: s.streamDimensions,
|
||||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
executeAst: s.executeAst,
|
||||||
}))
|
}))
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
|
||||||
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 =
|
const {
|
||||||
ranges.length !== selectionRange.codeBasedSelections.length ||
|
auth: {
|
||||||
ranges.some(({ from, to }, i) => {
|
context: { token },
|
||||||
return (
|
},
|
||||||
from !== selectionRange.codeBasedSelections[i].range[0] ||
|
settings: {
|
||||||
to !== selectionRange.codeBasedSelections[i].range[1]
|
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||||
)
|
},
|
||||||
})
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
if (!isChange) return
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
setSelectionRanges({
|
|
||||||
otherSelections: [],
|
// Pane toggling keyboard shortcuts
|
||||||
codeBasedSelections: ranges.map(({ from, to }, i) => {
|
const togglePane = useCallback(
|
||||||
if (selectionRangeTypeMap[to]) {
|
(newPane: PaneType) =>
|
||||||
return {
|
openPanes.includes(newPane)
|
||||||
type: selectionRangeTypeMap[to],
|
? setOpenPanes(openPanes.filter((p) => p !== newPane))
|
||||||
range: [from, to],
|
: setOpenPanes([...openPanes, newPane]),
|
||||||
}
|
[openPanes, setOpenPanes]
|
||||||
}
|
)
|
||||||
return {
|
useHotkeys('shift + c', () => togglePane('code'))
|
||||||
type: 'default',
|
useHotkeys('shift + v', () => togglePane('variables'))
|
||||||
range: [from, to],
|
useHotkeys('shift + l', () => togglePane('logs'))
|
||||||
}
|
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||||
}),
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
})
|
useHotkeys('esc', () => {
|
||||||
}
|
if (guiMode.mode === 'sketch') {
|
||||||
const [geoArray, setGeoArray] = useState<(ExtrudeGroup | SketchGroup)[]>([])
|
if (guiMode.sketchMode === 'selectFace') return
|
||||||
useEffect(() => {
|
if (guiMode.sketchMode === 'sketchEdit') {
|
||||||
const asyncWrap = async () => {
|
// TODO: share this with Toolbar's "Exit sketch" button
|
||||||
try {
|
// exiting sketch should be done consistently across all exits
|
||||||
if (!code) {
|
engineCommandManager?.sendSceneCommand({
|
||||||
setGeoArray([])
|
type: 'modeling_cmd_req',
|
||||||
setAst(null)
|
cmd_id: uuidv4(),
|
||||||
return
|
cmd: { type: 'edit_mode_exit' },
|
||||||
}
|
|
||||||
const tokens = await asyncLexer(code)
|
|
||||||
const _ast = abstractSyntaxTree(tokens)
|
|
||||||
setAst(_ast)
|
|
||||||
resetLogs()
|
|
||||||
const programMemory = 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: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_sketch: [],
|
|
||||||
})
|
})
|
||||||
setProgramMemory(programMemory)
|
engineCommandManager?.sendSceneCommand({
|
||||||
const geos = programMemory?.return
|
type: 'modeling_cmd_req',
|
||||||
?.map(({ name }: { name: string }) => {
|
cmd_id: uuidv4(),
|
||||||
const artifact = programMemory?.root?.[name]
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
if (
|
})
|
||||||
artifact.type === 'extrudeGroup' ||
|
setGuiMode({ mode: 'default' })
|
||||||
artifact.type === 'sketchGroup'
|
// this is necessary to get the UI back into a consistent
|
||||||
) {
|
// state right now, hopefully won't need to rerender
|
||||||
return artifact
|
// when exiting sketch mode in the future
|
||||||
}
|
executeAst()
|
||||||
return null
|
} else {
|
||||||
})
|
engineCommandManager?.sendSceneCommand({
|
||||||
.filter((a) => a) as (ExtrudeGroup | SketchGroup)[]
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'set_tool',
|
||||||
|
tool: 'select',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
rotation: guiMode.rotation,
|
||||||
|
position: guiMode.position,
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
pathId: guiMode.pathId,
|
||||||
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setGeoArray(geos)
|
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||||
console.log(programMemory)
|
(p) => p === onboardingStatus
|
||||||
setError()
|
)
|
||||||
} catch (e: any) {
|
? 'opacity-20'
|
||||||
setError('problem')
|
: didDragInStream
|
||||||
console.log(e)
|
? 'opacity-40'
|
||||||
addLog(e)
|
: ''
|
||||||
|
|
||||||
|
// Use file code loaded from disk
|
||||||
|
// on mount, and overwrite any locally-stored code
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTauri() && loadedCode !== null) {
|
||||||
|
setCode(loadedCode)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
// Clear code on unmount if in desktop app
|
||||||
|
if (isTauri()) {
|
||||||
|
setCode('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
asyncWrap()
|
}, [loadedCode, setCode])
|
||||||
}, [code])
|
|
||||||
|
useSetupEngineManager(streamRef, token)
|
||||||
|
useEngineConnectionSubscriptions()
|
||||||
|
|
||||||
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
|
engineCommandManager?.sendSceneCommand(message)
|
||||||
|
}, 16)
|
||||||
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
e.nativeEvent.preventDefault()
|
||||||
|
|
||||||
|
const { x, y } = getNormalisedCoordinates({
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
el: e.currentTarget,
|
||||||
|
...streamDimensions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const newCmdId = uuidv4()
|
||||||
|
if (buttonDownInStream === undefined) {
|
||||||
|
if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any)
|
||||||
|
) {
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: newCmdId,
|
||||||
|
cmd: {
|
||||||
|
type: 'mouse_move',
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'highlight_set_entity',
|
||||||
|
selected_at_window: { x, y },
|
||||||
|
},
|
||||||
|
cmd_id: newCmdId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||||
|
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 {
|
||||||
|
console.log('none')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction,
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
cmd_id: newCmdId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div
|
||||||
|
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
ref={streamRef}
|
||||||
|
>
|
||||||
|
<AppHeader
|
||||||
|
className={
|
||||||
|
'transition-opacity transition-duration-75 ' +
|
||||||
|
paneOpacity +
|
||||||
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
|
}
|
||||||
|
project={project}
|
||||||
|
enableMenu={true}
|
||||||
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Allotment snap={true}>
|
<Resizable
|
||||||
<Allotment vertical defaultSizes={[400, 1, 1]} minSize={20}>
|
className={
|
||||||
<div className="h-full flex flex-col items-start">
|
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||||
<PanelHeader title="Editor" />
|
(buttonDownInStream || onboardingStatus === 'camera'
|
||||||
{/* <button
|
? ' pointer-events-none '
|
||||||
disabled={!shouldFormat}
|
: ' ') +
|
||||||
onClick={formatCode}
|
paneOpacity
|
||||||
className={`${!shouldFormat && 'text-gray-300'}`}
|
}
|
||||||
>
|
defaultSize={{
|
||||||
format
|
width: '550px',
|
||||||
</button> */}
|
height: 'auto',
|
||||||
<div
|
}}
|
||||||
className="bg-red h-full w-full overflow-auto"
|
minWidth={200}
|
||||||
id="code-mirror-override"
|
maxWidth={800}
|
||||||
>
|
minHeight={'auto'}
|
||||||
<CodeMirror
|
maxHeight={'auto'}
|
||||||
className="h-full"
|
handleClasses={{
|
||||||
value={code}
|
right:
|
||||||
extensions={[javascript({ jsx: true }), lineHighlightField]}
|
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
||||||
onChange={onChange}
|
}}
|
||||||
onUpdate={onUpdate}
|
>
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
<div id="code-pane" className="h-full flex flex-col justify-between">
|
||||||
/>
|
<CollapsiblePanel
|
||||||
</div>
|
title="Code"
|
||||||
</div>
|
icon={faCode}
|
||||||
<MemoryPanel />
|
className="open:!mb-2"
|
||||||
<Logs />
|
open={openPanes.includes('code')}
|
||||||
</Allotment>
|
menu={<CodeMenu />}
|
||||||
<Allotment vertical defaultSizes={[400, 1]} minSize={20}>
|
>
|
||||||
<div className="h-full">
|
<TextEditor theme={editorTheme} />
|
||||||
<PanelHeader title="Drafting Board" />
|
</CollapsiblePanel>
|
||||||
<Toolbar />
|
<section className="flex flex-col">
|
||||||
<div className="border h-full border-gray-300 relative">
|
<MemoryPanel
|
||||||
<div className="absolute inset-0">
|
theme={editorTheme}
|
||||||
<Canvas>
|
open={openPanes.includes('variables')}
|
||||||
<OrbitControls
|
title="Variables"
|
||||||
enableDamping={false}
|
icon={faSquareRootVariable}
|
||||||
enablePan
|
/>
|
||||||
enableRotate={
|
<Logs
|
||||||
!(
|
theme={editorTheme}
|
||||||
guiMode.mode === 'canEditSketch' ||
|
open={openPanes.includes('logs')}
|
||||||
guiMode.mode === 'sketch'
|
title="Logs"
|
||||||
)
|
icon={faCodeCommit}
|
||||||
}
|
/>
|
||||||
enableZoom
|
<KCLErrors
|
||||||
reverseOrbit={false}
|
theme={editorTheme}
|
||||||
/>
|
open={openPanes.includes('kclErrors')}
|
||||||
<OrrthographicCamera
|
title="KCL Errors"
|
||||||
ref={cam}
|
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
|
||||||
makeDefault
|
/>
|
||||||
position={[0, 0, 1000]}
|
</section>
|
||||||
zoom={100}
|
</div>
|
||||||
rotation={[0, 0, 0]}
|
</Resizable>
|
||||||
far={2000}
|
<Stream className="absolute inset-0 z-0" />
|
||||||
/>
|
{showDebugPanel && (
|
||||||
<ambientLight />
|
<DebugPanel
|
||||||
<pointLight position={[10, 10, 10]} />
|
title="Debug"
|
||||||
<RenderViewerArtifacts artifacts={geoArray} />
|
className={
|
||||||
<BasePlanes />
|
'transition-opacity transition-duration-75 ' +
|
||||||
<SketchPlane />
|
paneOpacity +
|
||||||
<AxisIndicator />
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
</Canvas>
|
}
|
||||||
</div>
|
open={openPanes.includes('debug')}
|
||||||
{errorState.isError && (
|
/>
|
||||||
<div className="absolute inset-0 bg-gray-700/20">
|
)}
|
||||||
<pre>
|
|
||||||
{'last first: \n\n' +
|
|
||||||
JSON.stringify(lastGuiMode, null, 2) +
|
|
||||||
'\n\n' +
|
|
||||||
JSON.stringify(guiMode)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Stream />
|
|
||||||
</Allotment>
|
|
||||||
</Allotment>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
|
||||||
|
41
src/Auth.tsx
@ -1,31 +1,16 @@
|
|||||||
import useSWR from 'swr'
|
import Loading from './components/Loading'
|
||||||
import fetcher from './lib/fetcher'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import withBaseUrl from './lib/withBaseURL'
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
export const Auth = () => {
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
const { data: user } = useSWR(withBaseUrl('/user'), fetcher) as any
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const isLocalHost =
|
const {
|
||||||
typeof window !== 'undefined' && window.location.hostname === 'localhost'
|
auth: { state },
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
const isLoggingIn = state.matches('checkIfLoggedIn')
|
||||||
|
|
||||||
if (!user && !isLocalHost) {
|
return isLoggingIn ? (
|
||||||
return (
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
<>
|
) : (
|
||||||
<div className=" bg-gray-800 p-1 px-4 rounded-r-lg pointer-events-auto flex items-center">
|
<>{children}</>
|
||||||
<a
|
)
|
||||||
className="font-bold mr-2 text-purple-400"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target={'_self'}
|
|
||||||
href={`https://dev.kittycad.io/signin?callbackUrl=${encodeURIComponent(
|
|
||||||
typeof window !== 'undefined' && window.location.href
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <App />
|
|
||||||
}
|
}
|
||||||
|
269
src/Router.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import { App } from './App'
|
||||||
|
import {
|
||||||
|
createBrowserRouter,
|
||||||
|
Outlet,
|
||||||
|
redirect,
|
||||||
|
useLocation,
|
||||||
|
RouterProvider,
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
matchRoutes,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
useNavigationType,
|
||||||
|
} from 'react-router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
|
import { Settings } from './routes/Settings'
|
||||||
|
import Onboarding, {
|
||||||
|
onboardingRoutes,
|
||||||
|
onboardingPaths,
|
||||||
|
} from './routes/Onboarding'
|
||||||
|
import SignIn from './routes/SignIn'
|
||||||
|
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 { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsMachine,
|
||||||
|
} from './machines/settingsMachine'
|
||||||
|
import { ContextFrom } from 'xstate'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
|
import * as Sentry from '@sentry/react'
|
||||||
|
|
||||||
|
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: VITE_KC_SENTRY_DSN,
|
||||||
|
// TODO(paultag): pass in the right env here.
|
||||||
|
// environment: "production",
|
||||||
|
integrations: [
|
||||||
|
new Sentry.BrowserTracing({
|
||||||
|
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||||
|
useEffect,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new Sentry.Replay(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for performance monitoring.
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
|
||||||
|
// TODO: Add in kittycad.io endpoints
|
||||||
|
tracePropagationTargets: ['localhost'],
|
||||||
|
|
||||||
|
// Capture Replay for 10% of all sessions,
|
||||||
|
// plus for 100% of sessions with an error
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const prependRoutes =
|
||||||
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(routesObject).map(([constName, path]) => [
|
||||||
|
constName,
|
||||||
|
prepend + path,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paths = {
|
||||||
|
INDEX: '/',
|
||||||
|
HOME: '/home',
|
||||||
|
FILE: '/file',
|
||||||
|
SETTINGS: '/settings',
|
||||||
|
SIGN_IN: '/signin',
|
||||||
|
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||||
|
'/onboarding'
|
||||||
|
) as typeof onboardingPaths,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndexLoaderData = {
|
||||||
|
code: string | null
|
||||||
|
project?: ProjectWithEntryPointMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||||
|
entrypoint_metadata: 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,
|
||||||
|
loader: () =>
|
||||||
|
isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'),
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.FILE + '/:id',
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<Outlet />
|
||||||
|
<App />
|
||||||
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
id: paths.FILE,
|
||||||
|
loader: async ({
|
||||||
|
request,
|
||||||
|
params,
|
||||||
|
}): Promise<IndexLoaderData | Response> => {
|
||||||
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
|
ContextFrom<typeof settingsMachine>
|
||||||
|
>
|
||||||
|
|
||||||
|
const status = persistedSettings.onboardingStatus || ''
|
||||||
|
const notEnRouteToOnboarding = !request.url.includes(
|
||||||
|
paths.ONBOARDING.INDEX
|
||||||
|
)
|
||||||
|
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||||
|
const hasValidOnboardingStatus =
|
||||||
|
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||||
|
const shouldRedirectToOnboarding =
|
||||||
|
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||||
|
|
||||||
|
if (shouldRedirectToOnboarding) {
|
||||||
|
return redirect(
|
||||||
|
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.id && params.id !== 'new') {
|
||||||
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
|
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT)
|
||||||
|
const entrypoint_metadata = await metadata(
|
||||||
|
params.id + '/' + PROJECT_ENTRYPOINT
|
||||||
|
)
|
||||||
|
const children = await readDir(params.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
project: {
|
||||||
|
name: params.id.slice(params.id.lastIndexOf('/') + 1),
|
||||||
|
path: params.id,
|
||||||
|
children,
|
||||||
|
entrypoint_metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
|
element: <Settings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
|
||||||
|
element: <Onboarding />,
|
||||||
|
children: onboardingRoutes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.HOME,
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<Outlet />
|
||||||
|
<Home />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
loader: async () => {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return redirect(paths.FILE + '/new')
|
||||||
|
}
|
||||||
|
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) => ({
|
||||||
|
entrypoint_metadata: await metadata(
|
||||||
|
p.path + '/' + PROJECT_ENTRYPOINT
|
||||||
|
),
|
||||||
|
...p,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
|
element: <Settings />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.SIGN_IN,
|
||||||
|
element: <SignIn />,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All routes in the app, used in src/index.tsx
|
||||||
|
* @returns RouterProvider
|
||||||
|
*/
|
||||||
|
export const Router = () => {
|
||||||
|
return <RouterProvider router={router} />
|
||||||
|
}
|
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;
|
||||||
|
}
|
418
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
|||||||
import { useStore, toolTips } from './useStore'
|
import { useStore, toolTips, ToolTip } from './useStore'
|
||||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||||
@ -8,9 +8,39 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
|||||||
import { Intersect } from './components/Toolbar/Intersect'
|
import { Intersect } from './components/Toolbar/Intersect'
|
||||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||||
|
import { Fragment, useEffect } from 'react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
|
import styles from './Toolbar.module.css'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useAppMode } from 'hooks/useAppMode'
|
||||||
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
|
||||||
|
export const sketchButtonClassnames = {
|
||||||
|
background:
|
||||||
|
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
||||||
|
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const sketchFnLabels: Record<ToolTip | 'sketch_line' | 'move', string> = {
|
||||||
|
sketch_line: 'Line',
|
||||||
|
line: 'Line',
|
||||||
|
move: 'Move',
|
||||||
|
angledLine: 'Angled Line',
|
||||||
|
angledLineThatIntersects: 'Angled Line That Intersects',
|
||||||
|
angledLineOfXLength: 'Angled Line Of X Length',
|
||||||
|
angledLineOfYLength: 'Angled Line Of Y Length',
|
||||||
|
angledLineToX: 'Angled Line To X',
|
||||||
|
angledLineToY: 'Angled Line To Y',
|
||||||
|
lineTo: 'Line to Point',
|
||||||
|
xLine: 'Horizontal Line',
|
||||||
|
yLine: 'Vertical Line',
|
||||||
|
xLineTo: 'Horizontal Line to Point',
|
||||||
|
yLineTo: 'Vertical Line to Point',
|
||||||
|
}
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const {
|
||||||
@ -20,6 +50,8 @@ export const Toolbar = () => {
|
|||||||
ast,
|
ast,
|
||||||
updateAst,
|
updateAst,
|
||||||
programMemory,
|
programMemory,
|
||||||
|
engineCommandManager,
|
||||||
|
executeAst,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
guiMode: s.guiMode,
|
guiMode: s.guiMode,
|
||||||
setGuiMode: s.setGuiMode,
|
setGuiMode: s.setGuiMode,
|
||||||
@ -27,78 +59,33 @@ export const Toolbar = () => {
|
|||||||
ast: s.ast,
|
ast: s.ast,
|
||||||
updateAst: s.updateAst,
|
updateAst: s.updateAst,
|
||||||
programMemory: s.programMemory,
|
programMemory: s.programMemory,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
executeAst: s.executeAst,
|
||||||
}))
|
}))
|
||||||
|
useAppMode()
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div>
|
console.log('guiMode', guiMode)
|
||||||
{guiMode.mode === 'default' && (
|
}, [guiMode])
|
||||||
<button
|
|
||||||
onClick={() => {
|
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||||
setGuiMode({
|
return (
|
||||||
mode: 'sketch',
|
<span className={styles.toolbarButtons + ' ' + className}>
|
||||||
sketchMode: 'selectFace',
|
{guiMode.mode === 'default' && (
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="border m-1 px-1 rounded text-xs"
|
|
||||||
>
|
|
||||||
Start Sketch
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{guiMode.mode === 'canEditExtrude' && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!ast) return
|
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections[0].range
|
|
||||||
)
|
|
||||||
const { modifiedAst } = sketchOnExtrudedFace(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
programMemory
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst)
|
|
||||||
}}
|
|
||||||
className="border m-1 px-1 rounded text-xs"
|
|
||||||
>
|
|
||||||
SketchOnFace
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setGuiMode({
|
|
||||||
mode: 'sketch',
|
|
||||||
sketchMode: 'sketchEdit',
|
|
||||||
pathToNode: guiMode.pathToNode,
|
|
||||||
rotation: guiMode.rotation,
|
|
||||||
position: guiMode.position,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="border m-1 px-1 rounded text-xs"
|
|
||||||
>
|
|
||||||
Edit Sketch
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{guiMode.mode === 'canEditSketch' && (
|
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ast) return
|
setGuiMode({
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
mode: 'sketch',
|
||||||
ast,
|
sketchMode: 'selectFace',
|
||||||
selectionRanges.codeBasedSelections[0].range
|
})
|
||||||
)
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
}}
|
||||||
className="border m-1 px-1 rounded text-xs"
|
className="group"
|
||||||
>
|
>
|
||||||
ExtrudeSketch
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
|
Start Sketch
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{guiMode.mode === 'canEditExtrude' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ast) return
|
if (!ast) return
|
||||||
@ -106,84 +93,243 @@ export const Toolbar = () => {
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges.codeBasedSelections[0].range
|
selectionRanges.codeBasedSelections[0].range
|
||||||
)
|
)
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
const { modifiedAst } = sketchOnExtrudedFace(
|
||||||
ast,
|
ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
false
|
programMemory
|
||||||
)
|
)
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
updateAst(modifiedAst, true)
|
||||||
}}
|
}}
|
||||||
className="border m-1 px-1 rounded text-xs"
|
className="group"
|
||||||
>
|
>
|
||||||
ExtrudeSketch (w/o pipe)
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
|
Sketch on Face
|
||||||
</button>
|
</button>
|
||||||
</>
|
)}
|
||||||
)}
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
|
<button
|
||||||
{guiMode.mode === 'sketch' && (
|
onClick={() => {
|
||||||
<button
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
onClick={() => setGuiMode({ mode: 'default' })}
|
ast,
|
||||||
className="border m-1 px-1 rounded text-xs"
|
selectionRanges.codeBasedSelections[0].range
|
||||||
>
|
)
|
||||||
Exit sketch
|
setGuiMode({
|
||||||
</button>
|
mode: 'sketch',
|
||||||
)}
|
sketchMode: 'enterSketchEdit',
|
||||||
{toolTips
|
pathToNode: pathToNode,
|
||||||
.filter(
|
rotation: [0, 0, 0, 1],
|
||||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
position: [0, 0, 0],
|
||||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
pathId: guiMode.pathId,
|
||||||
)
|
})
|
||||||
.map((sketchFnName) => {
|
}}
|
||||||
if (
|
className="group"
|
||||||
guiMode.mode !== 'sketch' ||
|
>
|
||||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||||
)
|
Edit Sketch
|
||||||
return null
|
</button>
|
||||||
return (
|
)}
|
||||||
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
key={sketchFnName}
|
onClick={() => {
|
||||||
className={`border m-0.5 px-0.5 rounded text-xs ${
|
if (!ast) return
|
||||||
guiMode.sketchMode === sketchFnName && 'bg-gray-400'
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
}`}
|
ast,
|
||||||
onClick={() =>
|
selectionRanges.codeBasedSelections[0].range
|
||||||
setGuiMode({
|
)
|
||||||
...guiMode,
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
...(guiMode.sketchMode === sketchFnName
|
ast,
|
||||||
? {
|
pathToNode
|
||||||
sketchMode: 'sketchEdit',
|
)
|
||||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
}
|
}}
|
||||||
: {
|
className="group"
|
||||||
sketchMode: sketchFnName,
|
|
||||||
isTooltip: true,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{sketchFnName}
|
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
Extrude
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!ast) return
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||||
|
Extrude as new
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{guiMode.mode === 'sketch' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
executeAst()
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon="exit"
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
Exit sketch
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{toolTips
|
||||||
|
.filter(
|
||||||
|
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||||
|
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||||
)
|
)
|
||||||
})}
|
.map((sketchFnName) => {
|
||||||
<br></br>
|
if (
|
||||||
<ConvertToVariable />
|
guiMode.mode !== 'sketch' ||
|
||||||
<HorzVert horOrVert="horizontal" />
|
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||||
<HorzVert horOrVert="vertical" />
|
)
|
||||||
<EqualLength />
|
return null
|
||||||
<EqualAngle />
|
return (
|
||||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
<button
|
||||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
key={sketchFnName}
|
||||||
<SetAbsDistance buttonType="snapToYAxis" />
|
onClick={() => {
|
||||||
<SetAbsDistance buttonType="xAbs" />
|
engineCommandManager?.sendSceneCommand({
|
||||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
type: 'modeling_cmd_req',
|
||||||
<SetAbsDistance buttonType="snapToXAxis" />
|
cmd_id: uuidv4(),
|
||||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
cmd: {
|
||||||
<SetAbsDistance buttonType="yAbs" />
|
type: 'set_tool',
|
||||||
<SetAngleLength angleOrLength="setAngle" />
|
tool:
|
||||||
<SetAngleLength angleOrLength="setLength" />
|
guiMode.sketchMode === sketchFnName
|
||||||
<Intersect />
|
? 'select'
|
||||||
<RemoveConstrainingValues />
|
: (sketchFnName as any),
|
||||||
<SetAngleBetween />
|
},
|
||||||
</div>
|
})
|
||||||
|
setGuiMode({
|
||||||
|
...guiMode,
|
||||||
|
...(guiMode.sketchMode === sketchFnName
|
||||||
|
? {
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
sketchMode: sketchFnName,
|
||||||
|
waitingFirstClick: true,
|
||||||
|
isTooltip: true,
|
||||||
|
pathId: guiMode.pathId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'group ' +
|
||||||
|
(guiMode.sketchMode === sketchFnName
|
||||||
|
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon={sketchFnName.includes('line') ? 'line' : 'move'}
|
||||||
|
className="!p-0.5"
|
||||||
|
bgClassName={sketchButtonClassnames.background}
|
||||||
|
iconClassName={sketchButtonClassnames.icon}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
{sketchFnLabels[sketchFnName]}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<HorzVert horOrVert="horizontal" />
|
||||||
|
<HorzVert horOrVert="vertical" />
|
||||||
|
<EqualLength />
|
||||||
|
<EqualAngle />
|
||||||
|
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||||
|
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||||
|
<SetAbsDistance buttonType="snapToYAxis" />
|
||||||
|
<SetAbsDistance buttonType="xAbs" />
|
||||||
|
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||||
|
<SetAbsDistance buttonType="snapToXAxis" />
|
||||||
|
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||||
|
<SetAbsDistance buttonType="yAbs" />
|
||||||
|
<SetAngleLength angleOrLength="setAngle" />
|
||||||
|
<SetAngleLength angleOrLength="setLength" />
|
||||||
|
<Intersect />
|
||||||
|
<RemoveConstrainingValues />
|
||||||
|
<SetAngleBetween />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||||
|
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||||
|
</span>
|
||||||
|
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||||
|
<ToolbarButtons />
|
||||||
|
</menu>
|
||||||
|
<Popover.Button
|
||||||
|
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
|
</Popover.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition ease-out duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
||||||
|
</Transition>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="opacity-0 translate-y-1 scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 scale-100"
|
||||||
|
leave="transition ease-out duration-75"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-2"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
||||||
|
<section className="flex justify-between items-center">
|
||||||
|
<p
|
||||||
|
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||||
|
>
|
||||||
|
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||||
|
</p>
|
||||||
|
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||||
|
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||||
|
</Popover.Button>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<ToolbarButtons className="flex-wrap" />
|
||||||
|
</section>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
263
src/colors.css
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
:root {
|
||||||
|
/*
|
||||||
|
Generated using Catmosphere Theme Builder
|
||||||
|
by KittyCAD
|
||||||
|
https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D
|
||||||
|
*/
|
||||||
|
/* Chalkboard */
|
||||||
|
--chalkboard-10: oklch(99.7% 0.008766 102.8deg);
|
||||||
|
--chalkboard-20: oklch(91.34% 0.009353 109deg);
|
||||||
|
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
|
||||||
|
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
|
||||||
|
--chalkboard-50: oklch(66.27% 0.01111 127.6deg);
|
||||||
|
--chalkboard-60: oklch(57.92% 0.0117 133.9deg);
|
||||||
|
--chalkboard-70: oklch(49.56% 0.01229 140.1deg);
|
||||||
|
--chalkboard-80: oklch(41.21% 0.01288 146.3deg);
|
||||||
|
--chalkboard-90: oklch(32.85% 0.01346 152.5deg);
|
||||||
|
--chalkboard-100: oklch(24.49% 0.01405 158.7deg);
|
||||||
|
--chalkboard-110: oklch(16.14% 0.01464 164.9deg);
|
||||||
|
--chalkboard-120: oklch(7.783% 0.01522 171.1deg);
|
||||||
|
|
||||||
|
/* Energy */
|
||||||
|
--energy-10: oklch(93.31% 0.227 122.3deg);
|
||||||
|
--energy-20: oklch(86.01% 0.2092 123.6deg);
|
||||||
|
--energy-30: oklch(78.71% 0.1914 125deg);
|
||||||
|
--energy-40: oklch(71.41% 0.1736 126.3deg);
|
||||||
|
--energy-50: oklch(64.1% 0.1557 127.7deg);
|
||||||
|
--energy-60: oklch(56.8% 0.1379 129.1deg);
|
||||||
|
--energy-70: oklch(49.5% 0.1201 130.4deg);
|
||||||
|
--energy-80: oklch(42.2% 0.1023 131.8deg);
|
||||||
|
--energy-90: oklch(34.9% 0.08446 133.1deg);
|
||||||
|
--energy-100: oklch(27.6% 0.06664 134.5deg);
|
||||||
|
--energy-110: oklch(20.3% 0.04882 135.8deg);
|
||||||
|
--energy-120: oklch(13% 0.031 137.2deg);
|
||||||
|
|
||||||
|
/* Liquid */
|
||||||
|
--liquid-10: oklch(93.45% 0.1002 193.1deg);
|
||||||
|
--liquid-20: oklch(86.21% 0.09511 198.7deg);
|
||||||
|
--liquid-30: oklch(78.97% 0.09003 204.2deg);
|
||||||
|
--liquid-40: oklch(71.74% 0.08495 209.8deg);
|
||||||
|
--liquid-50: oklch(64.5% 0.07988 215.3deg);
|
||||||
|
--liquid-60: oklch(57.26% 0.0748 220.9deg);
|
||||||
|
--liquid-70: oklch(50.03% 0.06972 226.4deg);
|
||||||
|
--liquid-80: oklch(42.79% 0.06465 232deg);
|
||||||
|
--liquid-90: oklch(35.56% 0.05957 237.5deg);
|
||||||
|
--liquid-100: oklch(28.32% 0.0545 243.1deg);
|
||||||
|
--liquid-110: oklch(21.08% 0.04942 248.6deg);
|
||||||
|
--liquid-120: oklch(13.85% 0.04434 254.2deg);
|
||||||
|
|
||||||
|
/* Fern */
|
||||||
|
--fern-10: oklch(93.22% 0.1243 144.8deg);
|
||||||
|
--fern-20: oklch(86.59% 0.1193 144.6deg);
|
||||||
|
--fern-30: oklch(79.97% 0.1143 144.4deg);
|
||||||
|
--fern-40: oklch(73.34% 0.1093 144.2deg);
|
||||||
|
--fern-50: oklch(66.71% 0.1043 144deg);
|
||||||
|
--fern-60: oklch(60.09% 0.09927 143.8deg);
|
||||||
|
--fern-70: oklch(53.46% 0.09425 143.6deg);
|
||||||
|
--fern-80: oklch(46.83% 0.08924 143.3deg);
|
||||||
|
--fern-90: oklch(40.21% 0.08422 143.1deg);
|
||||||
|
--fern-100: oklch(33.58% 0.0792 142.9deg);
|
||||||
|
--fern-110: oklch(26.95% 0.07419 142.7deg);
|
||||||
|
--fern-120: oklch(20.33% 0.06917 142.5deg);
|
||||||
|
|
||||||
|
/* Cool */
|
||||||
|
--cool-10: oklch(97.71% 0.03321 196.6deg);
|
||||||
|
--cool-20: oklch(90.82% 0.03783 203.8deg);
|
||||||
|
--cool-30: oklch(83.94% 0.04245 211deg);
|
||||||
|
--cool-40: oklch(77.06% 0.04706 218.1deg);
|
||||||
|
--cool-50: oklch(70.18% 0.05168 225.3deg);
|
||||||
|
--cool-60: oklch(63.29% 0.0563 232.5deg);
|
||||||
|
--cool-70: oklch(56.41% 0.06091 239.6deg);
|
||||||
|
--cool-80: oklch(49.53% 0.06553 246.8deg);
|
||||||
|
--cool-90: oklch(42.65% 0.07015 254deg);
|
||||||
|
--cool-100: oklch(35.76% 0.07477 261.2deg);
|
||||||
|
--cool-110: oklch(28.88% 0.07938 268.3deg);
|
||||||
|
--cool-120: oklch(22% 0.084 275.5deg);
|
||||||
|
|
||||||
|
/* River */
|
||||||
|
--river-10: oklch(93.35% 0.03169 273.4deg);
|
||||||
|
--river-20: oklch(86.91% 0.04221 273.1deg);
|
||||||
|
--river-30: oklch(80.46% 0.05274 272.7deg);
|
||||||
|
--river-40: oklch(74.01% 0.06326 272.4deg);
|
||||||
|
--river-50: oklch(67.57% 0.07378 272deg);
|
||||||
|
--river-60: oklch(61.12% 0.0843 271.7deg);
|
||||||
|
--river-70: oklch(54.67% 0.09483 271.4deg);
|
||||||
|
--river-80: oklch(48.22% 0.1053 271deg);
|
||||||
|
--river-90: oklch(41.78% 0.1159 270.7deg);
|
||||||
|
--river-100: oklch(35.33% 0.1264 270.4deg);
|
||||||
|
--river-110: oklch(28.88% 0.1369 270deg);
|
||||||
|
--river-120: oklch(22.44% 0.1474 269.7deg);
|
||||||
|
|
||||||
|
/* Berry */
|
||||||
|
--berry-10: oklch(93.77% 0.05212 329deg);
|
||||||
|
--berry-20: oklch(87.3% 0.05912 325.3deg);
|
||||||
|
--berry-30: oklch(80.82% 0.06612 321.6deg);
|
||||||
|
--berry-40: oklch(74.34% 0.07313 317.8deg);
|
||||||
|
--berry-50: oklch(67.86% 0.08013 314.1deg);
|
||||||
|
--berry-60: oklch(61.39% 0.08713 310.3deg);
|
||||||
|
--berry-70: oklch(54.91% 0.09413 306.6deg);
|
||||||
|
--berry-80: oklch(48.43% 0.1011 302.8deg);
|
||||||
|
--berry-90: oklch(41.95% 0.1081 299.1deg);
|
||||||
|
--berry-100: oklch(35.47% 0.1151 295.4deg);
|
||||||
|
--berry-110: oklch(29% 0.1221 291.6deg);
|
||||||
|
--berry-120: oklch(22.52% 0.1291 287.9deg);
|
||||||
|
|
||||||
|
/* Destroy */
|
||||||
|
--destroy-10: oklch(88.21% 0.06281 14.85deg);
|
||||||
|
--destroy-20: oklch(83.23% 0.08511 16.91deg);
|
||||||
|
--destroy-30: oklch(78.25% 0.1074 18.96deg);
|
||||||
|
--destroy-40: oklch(73.27% 0.1297 21.01deg);
|
||||||
|
--destroy-50: oklch(68.29% 0.152 23.07deg);
|
||||||
|
--destroy-60: oklch(63.31% 0.1743 25.12deg);
|
||||||
|
--destroy-70: oklch(58.33% 0.1966 27.18deg);
|
||||||
|
--destroy-80: oklch(53.35% 0.2189 29.23deg);
|
||||||
|
|
||||||
|
/* Warn */
|
||||||
|
--warn-10: oklch(90.19% 0.1361 92deg);
|
||||||
|
--warn-20: oklch(84.6% 0.1388 84.84deg);
|
||||||
|
--warn-30: oklch(79.01% 0.1414 77.68deg);
|
||||||
|
--warn-40: oklch(73.42% 0.144 70.52deg);
|
||||||
|
--warn-50: oklch(67.83% 0.1466 63.36deg);
|
||||||
|
--warn-60: oklch(62.24% 0.1492 56.2deg);
|
||||||
|
--warn-70: oklch(56.65% 0.1518 49.04deg);
|
||||||
|
--warn-80: oklch(51.06% 0.1544 41.88deg);
|
||||||
|
|
||||||
|
/* Succeed */
|
||||||
|
--succeed-10: oklch(89% 0.16 143.4deg);
|
||||||
|
--succeed-20: oklch(83.23% 0.1608 143.3deg);
|
||||||
|
--succeed-30: oklch(77.46% 0.1616 143.1deg);
|
||||||
|
--succeed-40: oklch(71.69% 0.1623 143deg);
|
||||||
|
--succeed-50: oklch(65.92% 0.1631 142.9deg);
|
||||||
|
--succeed-60: oklch(60.16% 0.1639 142.8deg);
|
||||||
|
--succeed-70: oklch(54.39% 0.1647 142.6deg);
|
||||||
|
--succeed-80: oklch(48.62% 0.1654 142.5deg);
|
||||||
|
|
||||||
|
/* Base values for use with Tailwind. */
|
||||||
|
/* Chalkboard */
|
||||||
|
--_chalkboard-10: 99.7% 0.008766 102.8deg;
|
||||||
|
--_chalkboard-20: 91.34% 0.009353 109deg;
|
||||||
|
--_chalkboard-30: 82.99% 0.00994 115.2deg;
|
||||||
|
--_chalkboard-40: 74.63% 0.01053 121.4deg;
|
||||||
|
--_chalkboard-50: 66.27% 0.01111 127.6deg;
|
||||||
|
--_chalkboard-60: 57.92% 0.0117 133.9deg;
|
||||||
|
--_chalkboard-70: 49.56% 0.01229 140.1deg;
|
||||||
|
--_chalkboard-80: 41.21% 0.01288 146.3deg;
|
||||||
|
--_chalkboard-90: 32.85% 0.01346 152.5deg;
|
||||||
|
--_chalkboard-100: 24.49% 0.01405 158.7deg;
|
||||||
|
--_chalkboard-110: 16.14% 0.01464 164.9deg;
|
||||||
|
--_chalkboard-120: 7.783% 0.01522 171.1deg;
|
||||||
|
|
||||||
|
/* Energy */
|
||||||
|
--_energy-10: 93.31% 0.227 122.3deg;
|
||||||
|
--_energy-20: 86.01% 0.2092 123.6deg;
|
||||||
|
--_energy-30: 78.71% 0.1914 125deg;
|
||||||
|
--_energy-40: 71.41% 0.1736 126.3deg;
|
||||||
|
--_energy-50: 64.1% 0.1557 127.7deg;
|
||||||
|
--_energy-60: 56.8% 0.1379 129.1deg;
|
||||||
|
--_energy-70: 49.5% 0.1201 130.4deg;
|
||||||
|
--_energy-80: 42.2% 0.1023 131.8deg;
|
||||||
|
--_energy-90: 34.9% 0.08446 133.1deg;
|
||||||
|
--_energy-100: 27.6% 0.06664 134.5deg;
|
||||||
|
--_energy-110: 20.3% 0.04882 135.8deg;
|
||||||
|
--_energy-120: 13% 0.031 137.2deg;
|
||||||
|
|
||||||
|
/* Liquid */
|
||||||
|
--_liquid-10: 93.45% 0.1002 193.1deg;
|
||||||
|
--_liquid-20: 86.21% 0.09511 198.7deg;
|
||||||
|
--_liquid-30: 78.97% 0.09003 204.2deg;
|
||||||
|
--_liquid-40: 71.74% 0.08495 209.8deg;
|
||||||
|
--_liquid-50: 64.5% 0.07988 215.3deg;
|
||||||
|
--_liquid-60: 57.26% 0.0748 220.9deg;
|
||||||
|
--_liquid-70: 50.03% 0.06972 226.4deg;
|
||||||
|
--_liquid-80: 42.79% 0.06465 232deg;
|
||||||
|
--_liquid-90: 35.56% 0.05957 237.5deg;
|
||||||
|
--_liquid-100: 28.32% 0.0545 243.1deg;
|
||||||
|
--_liquid-110: 21.08% 0.04942 248.6deg;
|
||||||
|
--_liquid-120: 13.85% 0.04434 254.2deg;
|
||||||
|
|
||||||
|
/* Fern */
|
||||||
|
--_fern-10: 93.22% 0.1243 144.8deg;
|
||||||
|
--_fern-20: 86.59% 0.1193 144.6deg;
|
||||||
|
--_fern-30: 79.97% 0.1143 144.4deg;
|
||||||
|
--_fern-40: 73.34% 0.1093 144.2deg;
|
||||||
|
--_fern-50: 66.71% 0.1043 144deg;
|
||||||
|
--_fern-60: 60.09% 0.09927 143.8deg;
|
||||||
|
--_fern-70: 53.46% 0.09425 143.6deg;
|
||||||
|
--_fern-80: 46.83% 0.08924 143.3deg;
|
||||||
|
--_fern-90: 40.21% 0.08422 143.1deg;
|
||||||
|
--_fern-100: 33.58% 0.0792 142.9deg;
|
||||||
|
--_fern-110: 26.95% 0.07419 142.7deg;
|
||||||
|
--_fern-120: 20.33% 0.06917 142.5deg;
|
||||||
|
|
||||||
|
/* Cool */
|
||||||
|
--_cool-10: 97.71% 0.03321 196.6deg;
|
||||||
|
--_cool-20: 90.82% 0.03783 203.8deg;
|
||||||
|
--_cool-30: 83.94% 0.04245 211deg;
|
||||||
|
--_cool-40: 77.06% 0.04706 218.1deg;
|
||||||
|
--_cool-50: 70.18% 0.05168 225.3deg;
|
||||||
|
--_cool-60: 63.29% 0.0563 232.5deg;
|
||||||
|
--_cool-70: 56.41% 0.06091 239.6deg;
|
||||||
|
--_cool-80: 49.53% 0.06553 246.8deg;
|
||||||
|
--_cool-90: 42.65% 0.07015 254deg;
|
||||||
|
--_cool-100: 35.76% 0.07477 261.2deg;
|
||||||
|
--_cool-110: 28.88% 0.07938 268.3deg;
|
||||||
|
--_cool-120: 22% 0.084 275.5deg;
|
||||||
|
|
||||||
|
/* River */
|
||||||
|
--_river-10: 93.35% 0.03169 273.4deg;
|
||||||
|
--_river-20: 86.91% 0.04221 273.1deg;
|
||||||
|
--_river-30: 80.46% 0.05274 272.7deg;
|
||||||
|
--_river-40: 74.01% 0.06326 272.4deg;
|
||||||
|
--_river-50: 67.57% 0.07378 272deg;
|
||||||
|
--_river-60: 61.12% 0.0843 271.7deg;
|
||||||
|
--_river-70: 54.67% 0.09483 271.4deg;
|
||||||
|
--_river-80: 48.22% 0.1053 271deg;
|
||||||
|
--_river-90: 41.78% 0.1159 270.7deg;
|
||||||
|
--_river-100: 35.33% 0.1264 270.4deg;
|
||||||
|
--_river-110: 28.88% 0.1369 270deg;
|
||||||
|
--_river-120: 22.44% 0.1474 269.7deg;
|
||||||
|
|
||||||
|
/* Berry */
|
||||||
|
--_berry-10: 93.77% 0.05212 329deg;
|
||||||
|
--_berry-20: 87.3% 0.05912 325.3deg;
|
||||||
|
--_berry-30: 80.82% 0.06612 321.6deg;
|
||||||
|
--_berry-40: 74.34% 0.07313 317.8deg;
|
||||||
|
--_berry-50: 67.86% 0.08013 314.1deg;
|
||||||
|
--_berry-60: 61.39% 0.08713 310.3deg;
|
||||||
|
--_berry-70: 54.91% 0.09413 306.6deg;
|
||||||
|
--_berry-80: 48.43% 0.1011 302.8deg;
|
||||||
|
--_berry-90: 41.95% 0.1081 299.1deg;
|
||||||
|
--_berry-100: 35.47% 0.1151 295.4deg;
|
||||||
|
--_berry-110: 29% 0.1221 291.6deg;
|
||||||
|
--_berry-120: 22.52% 0.1291 287.9deg;
|
||||||
|
|
||||||
|
/* Destroy */
|
||||||
|
--_destroy-10: 88.21% 0.06281 14.85deg;
|
||||||
|
--_destroy-20: 83.23% 0.08511 16.91deg;
|
||||||
|
--_destroy-30: 78.25% 0.1074 18.96deg;
|
||||||
|
--_destroy-40: 73.27% 0.1297 21.01deg;
|
||||||
|
--_destroy-50: 68.29% 0.152 23.07deg;
|
||||||
|
--_destroy-60: 63.31% 0.1743 25.12deg;
|
||||||
|
--_destroy-70: 58.33% 0.1966 27.18deg;
|
||||||
|
--_destroy-80: 53.35% 0.2189 29.23deg;
|
||||||
|
|
||||||
|
/* Warn */
|
||||||
|
--_warn-10: 90.19% 0.1361 92deg;
|
||||||
|
--_warn-20: 84.6% 0.1388 84.84deg;
|
||||||
|
--_warn-30: 79.01% 0.1414 77.68deg;
|
||||||
|
--_warn-40: 73.42% 0.144 70.52deg;
|
||||||
|
--_warn-50: 67.83% 0.1466 63.36deg;
|
||||||
|
--_warn-60: 62.24% 0.1492 56.2deg;
|
||||||
|
--_warn-70: 56.65% 0.1518 49.04deg;
|
||||||
|
--_warn-80: 51.06% 0.1544 41.88deg;
|
||||||
|
|
||||||
|
/* Succeed */
|
||||||
|
--_succeed-10: 89% 0.16 143.4deg;
|
||||||
|
--_succeed-20: 83.23% 0.1608 143.3deg;
|
||||||
|
--_succeed-30: 77.46% 0.1616 143.1deg;
|
||||||
|
--_succeed-40: 71.69% 0.1623 143deg;
|
||||||
|
--_succeed-50: 65.92% 0.1631 142.9deg;
|
||||||
|
--_succeed-60: 60.16% 0.1639 142.8deg;
|
||||||
|
--_succeed-70: 54.39% 0.1647 142.6deg;
|
||||||
|
--_succeed-80: 48.62% 0.1654 142.5deg;
|
||||||
|
}
|
92
src/components/ActionButton.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
|
import React from 'react'
|
||||||
|
import { paths } from '../Router'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import type { LinkProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface BaseActionButtonProps {
|
||||||
|
icon?: ActionIconProps
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonAsButton = BaseActionButtonProps &
|
||||||
|
Omit<
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
keyof BaseActionButtonProps
|
||||||
|
> & {
|
||||||
|
Element: 'button'
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonAsLink = BaseActionButtonProps &
|
||||||
|
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
||||||
|
Element: 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonAsExternal = BaseActionButtonProps &
|
||||||
|
Omit<
|
||||||
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
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 (
|
||||||
|
<button className={classNames} {...rest}>
|
||||||
|
{props.icon && <ActionIcon {...icon} />}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'link': {
|
||||||
|
const { Element, to, icon, children, className, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Link to={to || paths.INDEX} className={classNames} {...rest}>
|
||||||
|
{icon && <ActionIcon {...icon} />}
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'externalLink': {
|
||||||
|
const { Element, icon, children, className, ...rest } = props
|
||||||
|
return (
|
||||||
|
<a className={classNames} {...rest}>
|
||||||
|
{icon && <ActionIcon {...icon} />}
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/components/ActionIcon.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
IconDefinition as SolidIconDefinition,
|
||||||
|
faCircleExclamation,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { CustomIcon, CustomIconName } from './CustomIcon'
|
||||||
|
|
||||||
|
const iconSizes = {
|
||||||
|
sm: 12,
|
||||||
|
md: 14.4,
|
||||||
|
lg: 20,
|
||||||
|
xl: 28,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionIconProps extends React.PropsWithChildren {
|
||||||
|
icon?: SolidIconDefinition | BrandIconDefinition | CustomIconName
|
||||||
|
className?: string
|
||||||
|
bgClassName?: string
|
||||||
|
iconClassName?: string
|
||||||
|
size?: keyof typeof iconSizes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionIcon = ({
|
||||||
|
icon = faCircleExclamation,
|
||||||
|
className,
|
||||||
|
bgClassName,
|
||||||
|
iconClassName,
|
||||||
|
size = 'md',
|
||||||
|
children,
|
||||||
|
}: 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 (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
`p-${
|
||||||
|
size === 'xl' ? '2' : '1'
|
||||||
|
} w-fit inline-grid place-content-center ${className} ` +
|
||||||
|
computedBgClassName
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
children
|
||||||
|
) : typeof icon === 'string' ? (
|
||||||
|
<CustomIcon
|
||||||
|
name={icon}
|
||||||
|
width={iconSizes[size]}
|
||||||
|
height={iconSizes[size]}
|
||||||
|
className={computedIconClassName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
width={iconSizes[size]}
|
||||||
|
height={iconSizes[size]}
|
||||||
|
className={computedIconClassName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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;
|
||||||
|
}
|
54
src/components/AppHeader.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Toolbar } from '../Toolbar'
|
||||||
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
|
import { ProjectWithEntryPointMetadata } 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 {
|
||||||
|
showToolbar?: boolean
|
||||||
|
project?: ProjectWithEntryPointMetadata
|
||||||
|
className?: string
|
||||||
|
enableMenu?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppHeader = ({
|
||||||
|
showToolbar = true,
|
||||||
|
project,
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
enableMenu = false,
|
||||||
|
}: AppHeaderProps) => {
|
||||||
|
const {
|
||||||
|
auth: {
|
||||||
|
context: { user },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={
|
||||||
|
(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
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||||
|
{/* Toolbar if the context deems it */}
|
||||||
|
{showToolbar && (
|
||||||
|
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||||
|
<Toolbar />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
|
{children || (
|
||||||
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<NetworkHealthIndicator />
|
||||||
|
<UserSidebarMenu user={user} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
182
src/components/AstExplorer.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
|
export function AstExplorer() {
|
||||||
|
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
||||||
|
ast: s.ast,
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
}))
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections?.[0]?.range
|
||||||
|
)
|
||||||
|
const node = getNodeFromPath(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={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, setCursor2 } = useStore((s) => ({
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
setCursor2: s.setCursor2,
|
||||||
|
}))
|
||||||
|
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) => {
|
||||||
|
setCursor2({ 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
@ -1,9 +1,6 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import {
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
abstractSyntaxTree,
|
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
||||||
BinaryPart,
|
|
||||||
Value,
|
|
||||||
} from '../lang/abstractSyntaxTree'
|
|
||||||
import { executor } from '../lang/executor'
|
import { executor } from '../lang/executor'
|
||||||
import {
|
import {
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -12,7 +9,6 @@ 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 { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
@ -96,11 +92,14 @@ export function useCalc({
|
|||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
} {
|
} {
|
||||||
const { ast, programMemory, selectionRange } = useStore((s) => ({
|
const { ast, programMemory, selectionRange, engineCommandManager } = useStore(
|
||||||
ast: s.ast,
|
(s) => ({
|
||||||
programMemory: s.programMemory,
|
ast: s.ast,
|
||||||
selectionRange: s.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>
|
||||||
@ -141,25 +140,27 @@ export function useCalc({
|
|||||||
}, [ast, programMemory, selectionRange])
|
}, [ast, programMemory, selectionRange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!engineCommandManager) return
|
||||||
try {
|
try {
|
||||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(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: [] }
|
||||||
})
|
})
|
||||||
const programMemory = executor(ast, _programMem)
|
executor(ast, _programMem, engineCommandManager).then((programMemory) => {
|
||||||
const resultDeclaration = ast.body.find(
|
const resultDeclaration = ast.body.find(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.type === 'VariableDeclaration' &&
|
a.type === 'VariableDeclaration' &&
|
||||||
a.declarations?.[0]?.id?.name === '__result__'
|
a.declarations?.[0]?.id?.name === '__result__'
|
||||||
)
|
)
|
||||||
const init =
|
const init =
|
||||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||||
resultDeclaration?.declarations?.[0]?.init
|
resultDeclaration?.declarations?.[0]?.init
|
||||||
const result = programMemory?.root?.__result__?.value
|
const result = programMemory?.root?.__result__?.value
|
||||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||||
init && setValueNode(init)
|
init && setValueNode(init)
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
@ -197,29 +198,25 @@ 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" className="block mt-3 font-mono">
|
||||||
htmlFor="create-new-variable"
|
|
||||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
|
||||||
>
|
|
||||||
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)
|
||||||
@ -231,7 +228,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}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
export const AxisIndicator = () => (
|
|
||||||
<>
|
|
||||||
<mesh position={[0.5, 0, 0]}>
|
|
||||||
<boxBufferGeometry args={[1, 0.05, 0.05]} />
|
|
||||||
<meshStandardMaterial color="red" />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 0.5, 0]}>
|
|
||||||
<boxBufferGeometry args={[0.05, 1, 0.05]} />
|
|
||||||
<meshStandardMaterial color="blue" />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 0, 0.5]}>
|
|
||||||
<boxBufferGeometry args={[0.05, 0.05, 1]} />
|
|
||||||
<meshStandardMaterial color="green" />
|
|
||||||
</mesh>
|
|
||||||
</>
|
|
||||||
)
|
|
@ -1,121 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { DoubleSide, Vector3 } from 'three'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { Intersection } from '@react-three/fiber'
|
|
||||||
import { Text } from '@react-three/drei'
|
|
||||||
import { addSketchTo } from '../lang/modifyAst'
|
|
||||||
import { Program } from '../lang/abstractSyntaxTree'
|
|
||||||
import { Quaternion } from 'three'
|
|
||||||
|
|
||||||
const opacity = 0.1
|
|
||||||
|
|
||||||
export const BasePlanes = () => {
|
|
||||||
const [axisIndex, setAxisIndex] = useState<null | number>(null)
|
|
||||||
const { setGuiMode, guiMode, ast, updateAst } = useStore(
|
|
||||||
({ guiMode, setGuiMode, ast, updateAst }) => ({
|
|
||||||
guiMode,
|
|
||||||
setGuiMode,
|
|
||||||
ast,
|
|
||||||
updateAst,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPointerEvent = ({
|
|
||||||
intersections,
|
|
||||||
}: {
|
|
||||||
intersections: Intersection[]
|
|
||||||
}) => {
|
|
||||||
if (!intersections.length) {
|
|
||||||
setAxisIndex(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let closestIntersection = intersections[0]
|
|
||||||
intersections.forEach((intersection) => {
|
|
||||||
if (intersection.distance < closestIntersection.distance)
|
|
||||||
closestIntersection = intersection
|
|
||||||
})
|
|
||||||
const smallestIndex = Number(closestIntersection.eventObject.name)
|
|
||||||
setAxisIndex(smallestIndex)
|
|
||||||
}
|
|
||||||
const onClick = () => {
|
|
||||||
if (guiMode.mode !== 'sketch') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (guiMode.sketchMode !== 'selectFace') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ast: Program = ast
|
|
||||||
? ast
|
|
||||||
: {
|
|
||||||
type: 'Program',
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
body: [],
|
|
||||||
nonCodeMeta: {},
|
|
||||||
}
|
|
||||||
const axis = axisIndex === 0 ? 'xy' : axisIndex === 1 ? 'xz' : 'yz'
|
|
||||||
const quaternion = new Quaternion()
|
|
||||||
if (axisIndex === 1) {
|
|
||||||
quaternion.setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2)
|
|
||||||
} else if (axisIndex === 2) {
|
|
||||||
quaternion.setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2)
|
|
||||||
}
|
|
||||||
const { modifiedAst, id, pathToNode } = addSketchTo(_ast, axis)
|
|
||||||
|
|
||||||
setGuiMode({
|
|
||||||
mode: 'sketch',
|
|
||||||
sketchMode: 'sketchEdit',
|
|
||||||
rotation: quaternion.toArray() as [number, number, number, number],
|
|
||||||
position: [0, 0, 0],
|
|
||||||
pathToNode,
|
|
||||||
})
|
|
||||||
|
|
||||||
updateAst(modifiedAst)
|
|
||||||
}
|
|
||||||
if (guiMode.mode !== 'sketch') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (guiMode.sketchMode !== 'selectFace') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
|
||||||
<mesh
|
|
||||||
key={index}
|
|
||||||
rotation-x={index === 1 ? -Math.PI / 2 : 0}
|
|
||||||
rotation-y={index === 2 ? -Math.PI / 2 : 0}
|
|
||||||
onPointerMove={onPointerEvent}
|
|
||||||
onPointerOut={onPointerEvent}
|
|
||||||
onClick={onClick}
|
|
||||||
name={`${index}`}
|
|
||||||
>
|
|
||||||
<planeGeometry args={[5, 5]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
color="blue"
|
|
||||||
side={DoubleSide}
|
|
||||||
transparent
|
|
||||||
opacity={opacity + (axisIndex === index ? 0.3 : 0)}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
fontSize={1}
|
|
||||||
color="#555"
|
|
||||||
position={[1, 1, 0.01]}
|
|
||||||
font={'/roboto.woff'}
|
|
||||||
>
|
|
||||||
{index === 0 ? 'xy' : index === 1 ? 'xz' : 'yz'}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize={1}
|
|
||||||
color="#555"
|
|
||||||
position={[1, 1, -0.01]}
|
|
||||||
font={'/roboto.woff'}
|
|
||||||
>
|
|
||||||
{index === 0 ? 'xy' : index === 1 ? 'xz' : 'yz'}
|
|
||||||
</Text>
|
|
||||||
</mesh>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
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 { useStore } from 'useStore'
|
||||||
|
import styles from './CodeMenu.module.css'
|
||||||
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
|
import { editorShortcutMeta } from './TextEditor'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
const { formatCode } = useStore((s) => ({
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
}))
|
||||||
|
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={() => formatCode()} 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>
|
||||||
|
)
|
||||||
|
}
|
52
src/components/CollapsiblePanel.module.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.panel {
|
||||||
|
@apply relative z-0;
|
||||||
|
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .panel {
|
||||||
|
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply sticky top-0 z-10 cursor-pointer;
|
||||||
|
@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 bg-chalkboard-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header:not(:last-of-type) {
|
||||||
|
@apply border-b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .header {
|
||||||
|
@apply bg-chalkboard-110 border-b-chalkboard-90 text-chalkboard-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .header:not(:last-of-type) {
|
||||||
|
@apply border-b-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel:first-of-type .header {
|
||||||
|
@apply rounded-t;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel:last-of-type .header {
|
||||||
|
@apply rounded-b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel[open] .header {
|
||||||
|
@apply rounded-t rounded-b-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel[open] {
|
||||||
|
@apply flex-grow max-h-full h-48 my-1 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel[open] + .panel[open],
|
||||||
|
.panel[open]:first-of-type {
|
||||||
|
@apply mt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel[open]:last-of-type {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
70
src/components/CollapsiblePanel.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import styles from './CollapsiblePanel.module.css'
|
||||||
|
|
||||||
|
export interface CollapsiblePanelProps
|
||||||
|
extends React.PropsWithChildren,
|
||||||
|
React.HTMLAttributes<HTMLDetailsElement> {
|
||||||
|
title: string
|
||||||
|
icon?: IconDefinition
|
||||||
|
open?: boolean
|
||||||
|
menu?: React.ReactNode
|
||||||
|
iconClassNames?: {
|
||||||
|
bg?: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelHeader = ({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
iconClassNames,
|
||||||
|
menu,
|
||||||
|
}: CollapsiblePanelProps) => {
|
||||||
|
return (
|
||||||
|
<summary className={styles.header}>
|
||||||
|
<div className="flex gap-2 align-center flex-1">
|
||||||
|
<ActionIcon
|
||||||
|
icon={icon}
|
||||||
|
bgClassName={
|
||||||
|
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||||
|
(iconClassNames?.bg || '')
|
||||||
|
}
|
||||||
|
iconClassName={
|
||||||
|
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||||
|
(iconClassNames?.icon || '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CollapsiblePanel = ({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
iconClassNames,
|
||||||
|
menu,
|
||||||
|
...props
|
||||||
|
}: CollapsiblePanelProps) => {
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
{...props}
|
||||||
|
className={styles.panel + ' group ' + (className || '')}
|
||||||
|
>
|
||||||
|
<PanelHeader
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
iconClassNames={iconClassNames}
|
||||||
|
menu={menu}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</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
|
161
src/components/CustomIcon.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
export type CustomIconName =
|
||||||
|
| 'equal'
|
||||||
|
| 'exit'
|
||||||
|
| 'extrude'
|
||||||
|
| 'horizontal'
|
||||||
|
| 'line'
|
||||||
|
| 'move'
|
||||||
|
| 'parallel'
|
||||||
|
| 'sketch'
|
||||||
|
| 'vertical'
|
||||||
|
|
||||||
|
export const CustomIcon = ({
|
||||||
|
name,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
name: CustomIconName
|
||||||
|
} & React.SVGProps<SVGSVGElement>) => {
|
||||||
|
switch (name) {
|
||||||
|
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 '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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
146
src/components/DebugPanel.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { EngineCommand } from '../lang/std/engineConnection'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ActionButton } from '../components/ActionButton'
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { isReducedMotion } from 'lang/util'
|
||||||
|
import { AstExplorer } from './AstExplorer'
|
||||||
|
|
||||||
|
type SketchModeCmd = Extract<
|
||||||
|
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||||
|
{ type: 'default_camera_enable_sketch_mode' }
|
||||||
|
>
|
||||||
|
|
||||||
|
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||||
|
const { engineCommandManager } = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
}))
|
||||||
|
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
|
||||||
|
type: 'default_camera_enable_sketch_mode',
|
||||||
|
origin: { x: 0, y: 0, z: 0 },
|
||||||
|
x_axis: { x: 1, y: 0, z: 0 },
|
||||||
|
y_axis: { x: 0, y: 1, z: 0 },
|
||||||
|
distance_to_plane: 100,
|
||||||
|
ortho: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
|
})
|
||||||
|
if (!sketchModeCmd) return null
|
||||||
|
return (
|
||||||
|
<CollapsiblePanel
|
||||||
|
{...props}
|
||||||
|
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">
|
||||||
|
<Xyz
|
||||||
|
onChange={setSketchModeCmd}
|
||||||
|
pointKey="origin"
|
||||||
|
data={sketchModeCmd}
|
||||||
|
/>
|
||||||
|
<Xyz
|
||||||
|
onChange={setSketchModeCmd}
|
||||||
|
pointKey="x_axis"
|
||||||
|
data={sketchModeCmd}
|
||||||
|
/>
|
||||||
|
<Xyz
|
||||||
|
onChange={setSketchModeCmd}
|
||||||
|
pointKey="y_axis"
|
||||||
|
data={sketchModeCmd}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="pr-4">distance_to_plane</div>
|
||||||
|
<input
|
||||||
|
className="w-16 dark:bg-chalkboard-90"
|
||||||
|
type="number"
|
||||||
|
value={sketchModeCmd.distance_to_plane}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setSketchModeCmd({
|
||||||
|
...sketchModeCmd,
|
||||||
|
distance_to_plane: Number(target.value),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="pr-4">ortho</div>
|
||||||
|
<input
|
||||||
|
className="w-16"
|
||||||
|
type="checkbox"
|
||||||
|
checked={sketchModeCmd.ortho}
|
||||||
|
onChange={(a) => {
|
||||||
|
console.log(a, (a as any).checked)
|
||||||
|
setSketchModeCmd({
|
||||||
|
...sketchModeCmd,
|
||||||
|
ortho: a.target.checked,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: sketchModeCmd,
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="hover:border-succeed-50"
|
||||||
|
icon={{
|
||||||
|
icon: faCheck,
|
||||||
|
bgClassName:
|
||||||
|
'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
|
||||||
|
iconClassName:
|
||||||
|
'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send sketch mode command
|
||||||
|
</ActionButton>
|
||||||
|
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||||
|
<AstExplorer />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</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
|
62
src/components/ErrorPage.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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 = () => {
|
||||||
|
let error = useRouteError()
|
||||||
|
|
||||||
|
console.error('error', error)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
|
<section className="max-w-full xl:max-w-4xl mx-auto">
|
||||||
|
<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="link"
|
||||||
|
icon={{ icon: faBug }}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
to="https://discord.com/channels/915388055236509727/1138967922614743060"
|
||||||
|
>
|
||||||
|
Report Bug
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
191
src/components/ExportButton.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import Modal from 'react-modal'
|
||||||
|
import React from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
|
||||||
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
|
|
||||||
|
interface ExportButtonProps extends React.PropsWithChildren {
|
||||||
|
className?: {
|
||||||
|
button?: string
|
||||||
|
// If we wanted more classname configuration of sub-elements,
|
||||||
|
// put them here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||||
|
const { engineCommandManager } = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const [modalIsOpen, setIsOpen] = React.useState(false)
|
||||||
|
|
||||||
|
const defaultType = 'gltf'
|
||||||
|
const [type, setType] = React.useState(defaultType)
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to gltf and embedded.
|
||||||
|
const initialValues: OutputFormat = {
|
||||||
|
type: defaultType,
|
||||||
|
storage: 'embedded',
|
||||||
|
presentation: 'pretty',
|
||||||
|
}
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
onSubmit: (values: OutputFormat) => {
|
||||||
|
// Set the default coords.
|
||||||
|
if (
|
||||||
|
values.type === 'obj' ||
|
||||||
|
values.type === 'ply' ||
|
||||||
|
values.type === 'step' ||
|
||||||
|
values.type === 'stl'
|
||||||
|
) {
|
||||||
|
// Set the default coords.
|
||||||
|
// In the future we can make this configurable.
|
||||||
|
// But for now, its probably best to keep it consistent with the
|
||||||
|
// UI.
|
||||||
|
values.coords = {
|
||||||
|
forward: {
|
||||||
|
axis: 'y',
|
||||||
|
direction: 'negative',
|
||||||
|
},
|
||||||
|
up: {
|
||||||
|
axis: 'z',
|
||||||
|
direction: 'positive',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'export',
|
||||||
|
// By default let's leave this blank to export the whole scene.
|
||||||
|
// In the future we might want to let the user choose which entities
|
||||||
|
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||||
|
entity_ids: [],
|
||||||
|
format: values,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
onClick={openModal}
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faFileExport }}
|
||||||
|
className={className?.button}
|
||||||
|
>
|
||||||
|
{children || 'Export'}
|
||||||
|
</ActionButton>
|
||||||
|
<Modal
|
||||||
|
isOpen={modalIsOpen}
|
||||||
|
onRequestClose={closeModal}
|
||||||
|
contentLabel="Export"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<h1 className="text-2xl font-bold">Export your design</h1>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
|
||||||
|
<label htmlFor="type" className="flex-1">
|
||||||
|
<p className="mb-2">Type</p>
|
||||||
|
<select
|
||||||
|
id="type"
|
||||||
|
name="type"
|
||||||
|
onChange={(e) => {
|
||||||
|
setType(e.target.value)
|
||||||
|
formik.handleChange(e)
|
||||||
|
}}
|
||||||
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
|
>
|
||||||
|
<option value="gltf">gltf</option>
|
||||||
|
<option value="obj">obj</option>
|
||||||
|
<option value="ply">ply</option>
|
||||||
|
<option value="step">step</option>
|
||||||
|
<option value="stl">stl</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
||||||
|
<label htmlFor="storage" className="flex-1">
|
||||||
|
<p className="mb-2">Storage</p>
|
||||||
|
<select
|
||||||
|
id="storage"
|
||||||
|
name="storage"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={
|
||||||
|
'storage' in formik.values ? formik.values.storage : ''
|
||||||
|
}
|
||||||
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
|
>
|
||||||
|
{type === 'gltf' && (
|
||||||
|
<>
|
||||||
|
<option value="embedded">embedded</option>
|
||||||
|
<option value="binary">binary</option>
|
||||||
|
<option value="standard">standard</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === 'ply' && (
|
||||||
|
<>
|
||||||
|
<option value="ascii">ascii</option>
|
||||||
|
<option value="binary">binary</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === 'stl' && (
|
||||||
|
<>
|
||||||
|
<option value="ascii">ascii</option>
|
||||||
|
<option value="binary_little_endian">
|
||||||
|
binary_little_endian
|
||||||
|
</option>
|
||||||
|
<option value="binary_big_endian">
|
||||||
|
binary_big_endian
|
||||||
|
</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between mt-6">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={closeModal}
|
||||||
|
icon={{
|
||||||
|
icon: faXmark,
|
||||||
|
bgClassName: 'bg-destroy-80',
|
||||||
|
iconClassName:
|
||||||
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||||
|
}}
|
||||||
|
className="hover:border-destroy-40"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
icon={{ icon: faFileExport }}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
164
src/components/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
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 { invoke } from '@tauri-apps/api'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
41
src/components/Loading.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setHasLongLoadTime(true)
|
||||||
|
}, 4000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [setHasLongLoadTime])
|
||||||
|
return (
|
||||||
|
<div className="body-bg flex flex-col items-center justify-center h-screen">
|
||||||
|
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||||
|
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
||||||
|
<circle
|
||||||
|
cx="5"
|
||||||
|
cy="5"
|
||||||
|
r="4"
|
||||||
|
stroke="var(--liquid-10)"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray="4, 4"
|
||||||
|
className="animate-spin origin-center"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
|
||||||
|
{children || 'Loading'}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
|
||||||
|
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Loading is taking longer than expected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
@ -1,14 +1,18 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { PanelHeader } from './PanelHeader'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
const ReactJsonTypeHack = ReactJson as any
|
const ReactJsonTypeHack = ReactJson as any
|
||||||
|
|
||||||
export const Logs = () => {
|
interface LogPanelProps extends CollapsiblePanelProps {
|
||||||
const { logs, resetLogs } = useStore(({ logs, resetLogs }) => ({
|
theme?: Exclude<Themes, Themes.System>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||||
|
const { logs } = useStore(({ logs }) => ({
|
||||||
logs,
|
logs,
|
||||||
resetLogs,
|
|
||||||
}))
|
}))
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector('.console-tile')
|
const element = document.querySelector('.console-tile')
|
||||||
@ -17,10 +21,9 @@ export const Logs = () => {
|
|||||||
}
|
}
|
||||||
}, [logs])
|
}, [logs])
|
||||||
return (
|
return (
|
||||||
<div>
|
<CollapsiblePanel {...props}>
|
||||||
<PanelHeader title="Logs" />
|
<div className="relative w-full">
|
||||||
<div className="h-full relative">
|
<div className="absolute inset-0 flex flex-col">
|
||||||
<div className="absolute inset-0 flex flex-col items-start">
|
|
||||||
<ReactJsonTypeHack
|
<ReactJsonTypeHack
|
||||||
src={logs}
|
src={logs}
|
||||||
collapsed={1}
|
collapsed={1}
|
||||||
@ -32,9 +35,46 @@ export const Logs = () => {
|
|||||||
indentWidth={2}
|
indentWidth={2}
|
||||||
quotesOnKeys={false}
|
quotesOnKeys={false}
|
||||||
name={false}
|
name={false}
|
||||||
|
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsiblePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KCLErrors = ({
|
||||||
|
theme = Themes.Light,
|
||||||
|
...props
|
||||||
|
}: LogPanelProps) => {
|
||||||
|
const { kclErrors } = useStore(({ kclErrors }) => ({
|
||||||
|
kclErrors,
|
||||||
|
}))
|
||||||
|
useEffect(() => {
|
||||||
|
const element = document.querySelector('.console-tile')
|
||||||
|
if (element) {
|
||||||
|
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||||
|
}
|
||||||
|
}, [kclErrors])
|
||||||
|
return (
|
||||||
|
<CollapsiblePanel {...props}>
|
||||||
|
<div className="h-full relative">
|
||||||
|
<div className="absolute inset-0 flex flex-col">
|
||||||
|
<ReactJsonTypeHack
|
||||||
|
src={kclErrors}
|
||||||
|
collapsed={1}
|
||||||
|
collapseStringsAfterLength={60}
|
||||||
|
enableClipboard={false}
|
||||||
|
displayArrayKey={false}
|
||||||
|
displayDataTypes={false}
|
||||||
|
displayObjectSize={true}
|
||||||
|
indentWidth={2}
|
||||||
|
quotesOnKeys={false}
|
||||||
|
name={false}
|
||||||
|
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsiblePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,75 +1,48 @@
|
|||||||
import { processMemory } from './MemoryPanel'
|
import { processMemory } from './MemoryPanel'
|
||||||
import { lexer } from '../lang/tokeniser'
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { executor } from '../lang/executor'
|
|
||||||
import { initPromise } from '../lang/rust'
|
import { initPromise } from '../lang/rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
describe('processMemory', () => {
|
describe('processMemory', () => {
|
||||||
it('should grab the values and remove and geo data', () => {
|
it('should grab the values and remove and geo data', async () => {
|
||||||
|
// 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 = startSketchAt([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 = startSketchAt([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 = parser_wasm(code)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
const programMemory = executor(ast, {
|
root: {},
|
||||||
root: {
|
return: null,
|
||||||
log: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: (a: any) => {
|
|
||||||
console.log('raw log', a)
|
|
||||||
},
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_sketch: [],
|
|
||||||
})
|
})
|
||||||
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: [],
|
||||||
{
|
|
||||||
type: 'extrudePlane',
|
|
||||||
position: [-1.2, 2.5, 0],
|
|
||||||
rotation: [
|
|
||||||
0.5984837231672995, -0.3765862890544571, 0.3765862890544572,
|
|
||||||
0.5984837231672996,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'extrudePlane',
|
|
||||||
position: [-1.58, 4, 0],
|
|
||||||
rotation: [
|
|
||||||
0.3024567786448806, 0.6391556125481195, -0.6391556125481194,
|
|
||||||
0.30245677864488063,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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,10 +1,18 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { PanelHeader } from './PanelHeader'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory } from '../lang/executor'
|
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
export const MemoryPanel = () => {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
|
theme?: Exclude<Themes, Themes.System>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MemoryPanel = ({
|
||||||
|
theme = Themes.Light,
|
||||||
|
...props
|
||||||
|
}: MemoryPanelProps) => {
|
||||||
const { programMemory } = useStore((s) => ({
|
const { programMemory } = useStore((s) => ({
|
||||||
programMemory: s.programMemory,
|
programMemory: s.programMemory,
|
||||||
}))
|
}))
|
||||||
@ -13,11 +21,14 @@ export const MemoryPanel = () => {
|
|||||||
[programMemory]
|
[programMemory]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<CollapsiblePanel {...props}>
|
||||||
<PanelHeader title="Variables" />
|
|
||||||
<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=" overflow-auto 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}
|
||||||
@ -28,11 +39,12 @@ export const MemoryPanel = () => {
|
|||||||
indentWidth={2}
|
indentWidth={2}
|
||||||
quotesOnKeys={false}
|
quotesOnKeys={false}
|
||||||
name={false}
|
name={false}
|
||||||
|
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsiblePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +53,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
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 {
|
||||||
|
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,7 +0,0 @@
|
|||||||
export const PanelHeader = ({ title }: { title: string }) => {
|
|
||||||
return (
|
|
||||||
<div className="font-mono text-[11px] bg-stone-100 w-full pl-4 h-[20px] text-stone-700 flex items-center">
|
|
||||||
<span className="pt-1">{title}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
162
src/components/ProjectCard.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { FormEvent, 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 } 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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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">
|
||||||
|
Edited {getDisplayedTime(project.entrypoint_metadata.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
|