Compare commits
172 Commits
v0.0.3
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
977e566ae4 | |||
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 | |||
7aaf923529 | |||
bcb05d02b4 | |||
ef451b70b6 | |||
c33107aa28 | |||
990605bbea | |||
d075c4ad13 | |||
a3f41f5519 | |||
cb173e2850 | |||
87cd3b67f4 | |||
fe3ee3806e | |||
26737e055a | |||
c9ed6c724c | |||
a5fa259d55 | |||
c01590b49b | |||
1d656d68c6 | |||
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
e180b73c9d | |||
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 | |||
738b1a7c21 | |||
62aebaf523 | |||
00ee913e3f | |||
2095375b37 | |||
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 | |||
7a9a33c656 | |||
a4a393fc45 | |||
10884fd0b0 | |||
bcf83dc7ee | |||
32f79c98f8 | |||
c11149e909 | |||
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 |
@ -1,3 +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_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=
|
||||
|
@ -1,3 +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_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||
|
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/wasm-lib/*
|
23
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src-tauri/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
81
.github/workflows/build.yml
vendored
@ -1,81 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-20.04, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Rust setup
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
||||
- name: wasm prep
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||
npx wasm-pack build --target web --out-dir pkg
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
|
||||
- name: macos sed
|
||||
if: matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
||||
|
||||
- name: ubuntu and windows sed
|
||||
if: matrix.os != 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
||||
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
- name: Build the app for the current platform (no upload)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
path: src-tauri/target/release/bundle
|
||||
name: modeling-app_macos_linux_windows
|
||||
|
||||
- name: Build the app for the current platform and upload to release
|
||||
if: github.event_name == 'release'
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
releaseId: ${{ github.event.release.id }}
|
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@v3
|
||||
|
||||
- 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@v3
|
||||
- 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 -- -D warnings
|
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@v3
|
||||
- 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@v3
|
||||
- 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 test --all
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
316
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,316 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
|
||||
check-format:
|
||||
runs-on: 'ubuntu-20.04'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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@v3
|
||||
- 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@v3
|
||||
|
||||
- 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@v3
|
||||
|
||||
- name: install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Rust setup
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
||||
- 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: macos sed
|
||||
if: matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
||||
|
||||
- name: ubuntu and windows sed
|
||||
if: matrix.os != 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
|
||||
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
- name: install apple silicon target mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
- name: Build the app for the current platform (no upload)
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_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/*/*' }}
|
||||
|
||||
|
||||
sign-windows-msi:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'release'
|
||||
needs: [build-test-web, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Setup Certificate
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
id: variables
|
||||
run: |
|
||||
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 SSM KSP on 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: Signing using Signtool
|
||||
run: |
|
||||
signtool.exe sign /sha1 ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "artifact\msi\*.msi"
|
||||
signtool.exe verify /v /pa "artifact\msi\*.msi"
|
||||
|
||||
# TODO: for the updater, investigate if we need to also replace what's in the .zip, and what to do about the .sig file
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: artifact/*
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event_name == 'release'
|
||||
needs: [build-test-web, build-apps, sign-windows-msi]
|
||||
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*
|
16
.github/workflows/format.yml
vendored
@ -1,16 +0,0 @@
|
||||
# on pull requests, setup node, run `yarn prettier --check`
|
||||
|
||||
name: Check formatting
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: yarn install
|
||||
- run: yarn fmt-check
|
21
.github/workflows/test.yml
vendored
@ -1,21 +0,0 @@
|
||||
# on pull requests, setup node, run `yarn install` and `yarn test:nowatch`
|
||||
|
||||
name: Test
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: yarn install
|
||||
- run: yarn build:wasm
|
||||
- run: yarn tsc
|
||||
- run: yarn simpleserver:ci
|
||||
- run: yarn test:nowatch
|
||||
- run: yarn test:cov
|
||||
- run: yarn test:rust
|
3
.gitignore
vendored
@ -24,4 +24,7 @@ yarn-error.log*
|
||||
|
||||
# rust
|
||||
src/wasm-lib/target
|
||||
src/wasm-lib/bindings
|
||||
src/wasm-lib/kcl/bindings
|
||||
public/wasm_lib_bg.wasm
|
||||
src/wasm-lib/lcov.info
|
||||
|
@ -5,3 +5,5 @@ coverage
|
||||
# Ignore Rust projects:
|
||||
*.rs
|
||||
target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
|
122
README.md
@ -1,54 +1,101 @@
|
||||
## 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
|
||||
```
|
||||
then
|
||||
|
||||
followed by:
|
||||
|
||||
```
|
||||
yarn build:wasm
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
|
||||
```
|
||||
yarn simpleserver
|
||||
```
|
||||
in one terminal
|
||||
and
|
||||
## Developing in Chrome
|
||||
|
||||
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/).
|
||||
If you're having trouble logging into the `modeling-app`, you may need to
|
||||
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
|
||||
```
|
||||
in another.
|
||||
|
||||
If you want to edit the rust files, you can cd into `src/wasm-lib` and then use the usual rust commands, `cargo build`, `cargo test`, when you want to bring the changes back to the web-app, a fresh `yarn build:wasm` in the root will be needed.
|
||||
|
||||
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
|
||||
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.
|
||||
|
||||
## 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.)
|
||||
@ -58,11 +105,22 @@ To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access t
|
||||
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
|
||||

|
||||
|
||||
|
||||
<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">
|
||||
|
||||
## 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
|
||||
@ -70,6 +128,7 @@ Note that these became separate apps on Macos, so make sure you open the right o
|
||||
```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
|
||||
@ -77,3 +136,24 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA
|
||||
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
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 207 KiB |
19534
docs/kcl/std.json
Normal file
3556
docs/kcl/std.md
Normal file
75
docs/kcl/types.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Types
|
||||
|
||||
`KCL` defines the following types and keywords the language.
|
||||
|
||||
All these types can be nested in various forms where nesting applies. Like
|
||||
arrays can hold objects and vice versa.
|
||||
|
||||
## Boolean
|
||||
|
||||
`true` or `false` work when defining values.
|
||||
|
||||
## Variable declaration
|
||||
|
||||
Variables are defined with the `let` keyword like so:
|
||||
|
||||
```
|
||||
let myBool = false
|
||||
```
|
||||
|
||||
## Array
|
||||
|
||||
An array is defined with `[]` braces. What is inside the brackets can
|
||||
be of any type. For example, the following is completely valid:
|
||||
|
||||
```
|
||||
let myArray = ["thing", 2, false]
|
||||
```
|
||||
|
||||
If you want to get a value from an array you can use the index like so:
|
||||
`myArray[0]`.
|
||||
|
||||
|
||||
## Object
|
||||
|
||||
An object is defined with `{}` braces. Here is an example object:
|
||||
|
||||
```
|
||||
let myObj = {a: 0, b: "thing"}
|
||||
```
|
||||
|
||||
We support two different ways of getting properties from objects, you can call
|
||||
`myObj.a` or `myObj["a"]` both work.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
We also have support for defining your own functions. Functions can take in any
|
||||
type of argument. Below is an example of the syntax:
|
||||
|
||||
```
|
||||
fn myFn = (x) => {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
|
||||
## Binary expressions
|
||||
|
||||
You can also do math! Let's show an example below:
|
||||
|
||||
```
|
||||
let myMathExpression = 3 + 1 * 2 / 3 - 7
|
||||
```
|
||||
|
||||
You can nest expressions in parenthesis as well:
|
||||
|
||||
```
|
||||
let myMathExpression = 3 + (1 * 2 / (3 - 7))
|
||||
```
|
||||
|
||||
Please if you find any issues using any of the above expressions or syntax
|
||||
please file an issue with the `ast` label on the [modeling-app
|
||||
repo](https://github.com/KittyCAD/modeling-app/issues/new).
|
@ -11,6 +11,7 @@
|
||||
/>
|
||||
<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">
|
||||
|
35
package.json
@ -1,26 +1,36 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.0.3",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@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",
|
||||
"@kittycad/lib": "^0.0.27",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.37",
|
||||
"@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/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.15.1",
|
||||
"@uiw/react-codemirror": "^4.21.13",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"formik": "^2.4.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"http-server": "^14.1.1",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -32,14 +42,18 @@
|
||||
"react-router-dom": "^6.14.2",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"swr": "^2.0.4",
|
||||
"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",
|
||||
"uuid": "^9.0.0",
|
||||
"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",
|
||||
"ws": "^8.13.0",
|
||||
"xstate": "^4.38.2",
|
||||
"zustand": "^4.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
@ -50,15 +64,15 @@
|
||||
"build:both:local": "yarn build:wasm && vite build",
|
||||
"test": "vitest --mode development",
|
||||
"test:nowatch": "vitest run --mode development",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
|
||||
"test:cov": "vitest run --coverage --mode development",
|
||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src",
|
||||
"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"
|
||||
},
|
||||
@ -85,6 +99,7 @@
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/kcma-logomark.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
@ -1,26 +1,45 @@
|
||||
<svg width="316" height="75" viewBox="0 0 316 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.33449 67.7274V65.5747H3.02906V63.4219H0.876343V18.2149H3.02906V16.0622H5.18177V13.9095H7.33449V11.7568H9.4872V7.45137H11.6399V5.29866H13.7926V3.14594H15.9453V0.993229H18.0981V3.14594H20.2508V5.29866H22.4035V7.45137H24.5562V9.60409H31.0143V7.45137H33.1671V5.29866H35.3198V3.14594H37.4725V0.993229H39.6252V3.14594H41.7779V5.29866H43.9306V7.45137H46.0833V11.7568H48.2361V13.9095H50.3888V16.0622H52.5415V18.2149H54.6942V63.4219H52.5415V65.5747H48.2361V67.7274H41.7779V69.8801H43.9306V74.1855H31.0143V69.8801H33.1671V67.7274H22.4035V69.8801H24.5562V74.1855H11.6399V69.8801H13.7926V67.7274H7.33449Z" fill="#101412"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5563 11.7568H31.0145V9.60409H33.1672V7.45137H35.3199V5.29866H37.4726V3.14594H39.6253V5.29866H41.778V7.45137H43.9307V11.7568H46.0835V13.9095H48.2361V16.0622H50.3888V18.2149H52.5415V20.3677V22.5204V50.5057V52.6584V54.8111V63.4219H48.2361V65.5747H39.6253V67.7274V69.8801H41.778V72.0328H33.1671V69.8801H35.3199V67.7274V65.5747H20.2509V67.7274V69.8801H22.4036V72.0328H13.7927V69.8801H15.9454V67.7274V65.5747H7.33454V63.4219H3.02911V54.8111V52.6584V50.5057V22.5204V20.3677V18.2149H5.18183V16.0622H7.33454V13.9095H9.48726V11.7568L9.48731 11.7568H11.64V7.45137H13.7927V5.29866H15.9455V3.14594H18.0982V5.29866H20.2509V7.45137H22.4036V9.60409H24.5563V11.7568ZM5.18191 56.9638V59.1165H15.9455V56.9638H5.18191ZM9.48734 61.2692V63.4219H15.9455V61.2692H9.48734ZM39.6253 59.1165V56.9638H41.7781V59.1165H39.6253ZM43.9308 56.9638V59.1165H46.0835V56.9638H43.9308ZM48.2362 59.1165V56.9638H50.3889V59.1165H48.2362ZM39.6253 61.2692V63.4219H46.0835V61.2692H39.6253ZM20.2509 59.1165H35.3199V63.4219H20.2509V59.1165Z" fill="#D0FF00"/>
|
||||
<path d="M3.02911 52.6584V50.5057H52.5415V52.6584H3.02911Z" fill="#B1E515"/>
|
||||
<path d="M9.48725 26.8258V24.6731H46.0834V26.8258H48.2361V44.0475H46.0834V46.2002H9.48725V44.0475H7.33453V26.8258H9.48725Z" fill="#1F2320"/>
|
||||
<path d="M35.3198 35.4367V26.8258H39.6252V35.4367H35.3198Z" fill="#D0FF00"/>
|
||||
<path d="M24.5562 35.4367H31.0144V37.5894H28.8617V41.8948H35.3198V39.7421H37.4725V41.8948H35.3198V44.0475H20.2508V41.8948H18.0981V39.7421H20.2508V41.8948H26.709V37.5894H24.5562V35.4367Z" fill="#D0FF00"/>
|
||||
<path d="M20.2508 33.2839V31.1312H13.7927V33.2839H11.64V31.1312H13.7927V28.9785H20.2508V31.1312H22.4035V33.2839H20.2508Z" fill="#D0FF00"/>
|
||||
<path d="M48.2361 18.2149V16.0622H50.3888V18.2149H48.2361Z" fill="#92C51B"/>
|
||||
<path d="M7.33448 18.2149V16.0622H5.18176V18.2149H7.33448Z" fill="#92C51B"/>
|
||||
<path d="M46.0834 16.0622V13.9095H48.2361V16.0622H46.0834Z" fill="#92C51B"/>
|
||||
<path d="M9.48725 16.0622V13.9095H7.33453V16.0622H9.48725Z" fill="#92C51B"/>
|
||||
<rect x="26.709" y="11.7568" width="2.15271" height="4.30543" fill="#B1E515"/>
|
||||
<path d="M35.3197 16.0622V13.9095H37.4725V11.7568H39.6252V13.9095H41.7779V16.0622H35.3197Z" fill="#101412"/>
|
||||
<path d="M15.9453 13.9095V11.7568H18.098V13.9095H20.2507V16.0622H13.7926V13.9095H15.9453Z" fill="#101412"/>
|
||||
<path d="M9.48718 52.6584V50.5057H15.9453V52.6584H9.48718Z" fill="#92C51B"/>
|
||||
<rect x="24.5562" y="11.7568" width="6.45814" height="2.15271" fill="#92C51B"/>
|
||||
<path d="M77.4822 18.0099V33.8657L92.8664 17.1258L99.4091 22.077L86.9721 35.5161L103.535 58.0914H92.6306L81.0777 41.8231L77.4822 45.7133V58.0914H68.8175V18.0099H77.4822Z" fill="#FFFFFA"/>
|
||||
<path d="M115.158 20.7213C115.158 22.0574 114.666 23.1969 113.684 24.14C112.741 25.0438 111.601 25.4957 110.265 25.4957C108.929 25.4957 107.809 25.0438 106.906 24.14C106.002 23.1969 105.55 22.0574 105.55 20.7213C105.55 19.3853 106.002 18.2457 106.906 17.3026C107.809 16.3595 108.929 15.888 110.265 15.888C111.601 15.888 112.741 16.3595 113.684 17.3026C114.666 18.2457 115.158 19.3853 115.158 20.7213ZM114.627 29.5039V58.0914H105.962V29.5039H114.627Z" fill="#FFFFFA"/>
|
||||
<path d="M133.871 59.0935C130.335 59.0935 127.407 58.1897 125.089 56.3821C122.809 54.5745 121.67 51.922 121.67 48.4247V36.636H117.603V29.9165H121.67V22.8433L130.276 21.4286V29.9165H136.052L137.938 36.636H130.276V47.128C130.276 48.5033 130.629 49.6429 131.337 50.5467C132.044 51.4112 133.066 51.8434 134.402 51.8434C134.913 51.8434 135.463 51.7648 136.052 51.6077C136.642 51.4505 137.231 51.2343 137.82 50.9593L140.355 57.0894C139.687 57.6395 138.705 58.1111 137.408 58.504C136.111 58.897 134.932 59.0935 133.871 59.0935Z" fill="#FFFFFA"/>
|
||||
<path d="M156.465 59.0935C152.929 59.0935 150.001 58.1897 147.683 56.3821C145.404 54.5745 144.264 51.922 144.264 48.4247V36.636H140.197V29.9165H144.264V22.8433L152.87 21.4286V29.9165H158.646L160.532 36.636H152.87V47.128C152.87 48.5033 153.223 49.6429 153.931 50.5467C154.638 51.4112 155.66 51.8434 156.996 51.8434C157.507 51.8434 158.057 51.7648 158.646 51.6077C159.236 51.4505 159.825 51.2343 160.415 50.9593L162.949 57.0894C162.281 57.6395 161.299 58.1111 160.002 58.504C158.705 58.897 157.526 59.0935 156.465 59.0935Z" fill="#FFFFFA"/>
|
||||
<path d="M172.163 59.0345L173.165 56.6178L162.791 30.5649L171.515 29.5039C172.576 32.3332 173.637 35.1625 174.698 37.9917C175.759 40.821 176.8 43.6503 177.822 46.4796L183.834 29.5039H192.793L180.062 61.687C179.119 64.0054 177.488 65.9898 175.169 67.6403C172.851 69.33 170.375 70.4892 167.742 71.1179L164.736 64.1036C166.151 63.5535 167.625 62.8658 169.157 62.0406C170.69 61.2154 171.692 60.2134 172.163 59.0345Z" fill="#FFFFFA"/>
|
||||
<path d="M203.975 58.0914L197.64 51.7563V21.3723L203.975 15.0371H223.072L229.438 21.3723V31.2748H220.735V25.6162L218.859 23.7402H208.219L206.343 25.6162V47.5124L208.219 49.3883H218.859L220.735 47.5124V41.8538H229.438V51.7563L223.072 58.0914H203.975Z" fill="#FFFFFA"/>
|
||||
<path d="M236.208 58.0914V21.3723L242.544 15.0371H262.41L268.745 21.3723V58.0914H260.073V45.4212H244.881V58.0914H236.208ZM244.881 36.7488H260.073V25.6162L258.197 23.7402H246.757L244.881 25.6162V36.7488Z" fill="#FFFFFA"/>
|
||||
<path d="M276.098 58.0914V15.0371H301.716L308.051 21.3723V51.7563L301.716 58.0914H276.098ZM284.802 49.3883H297.503L299.379 47.5124V25.6162L297.503 23.7402H284.802V49.3883Z" fill="#FFFFFA"/>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@ -1,26 +1,45 @@
|
||||
<svg width="788" height="183" viewBox="0 0 788 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6075 166.835V161.454H5.84388V156.072H0.462097V43.0543H5.84388V37.6725H11.2257V32.2907H16.6075V26.9089H21.9892V16.1454H27.371V10.7636H32.7528V5.38179H38.1346V0H43.5164V5.38179H48.8982V10.7636H54.28V16.1454H59.6617V21.5271H75.8071V16.1454H81.1889V10.7636H86.5707V5.38179H91.9525V0H97.3342V5.38179H102.716V10.7636H108.098V16.1454H113.48V26.9089H118.861V32.2907H124.243V37.6725H129.625V43.0543H135.007V156.072H129.625V161.454H118.861V166.835H102.716V172.217H108.098V182.981H75.8071V172.217H81.1889V166.835H54.28V172.217H59.6617V182.981H27.371V172.217H32.7528V166.835H16.6075Z" fill="#101412"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9892 26.9095L21.9892 26.9102V32.2919H16.6075V37.6737H11.2257V43.0555H5.84388V48.4364V53.8191V123.781V129.163V129.164V134.545V156.073H16.6075V161.455H38.1346V166.836V172.217H32.7528V177.599H54.28V172.217H48.8982V166.836V161.455H86.5707V166.836V172.217H81.1889V177.599H102.716V172.217H97.3342V166.836V161.455H118.861V156.073H129.625V134.545V129.164V129.163V123.781V53.8191V48.4364V43.0555H124.243V37.6737H118.861V32.2919H113.48V26.9102V26.9095H108.098V16.1459H102.716V10.7641H97.3342V5.38232H91.9525V10.7641H86.5707V16.1459H81.1889V21.5277H75.8071V26.9102H59.6617V21.5277H54.28V16.1459H48.8982V10.7641H43.5164V5.38232H38.1346V10.7641H32.7528V16.1459H27.371V26.9095H21.9892ZM11.2257 129.164H124.243V129.163H11.2257V129.164ZM11.2257 139.927V145.309H38.1346V139.927H11.2257ZM21.9893 150.691V156.072H38.1346V150.691H21.9893ZM97.3343 145.309V139.927H102.716V145.309H97.3343ZM108.098 139.927V145.309H113.48V139.927H108.098ZM118.861 145.309V139.927H124.243V145.309H118.861ZM97.3343 150.691V156.072H113.48V150.691H97.3343ZM48.8982 145.309H86.5707V156.073H48.8982V145.309Z" fill="#D0FF00"/>
|
||||
<path d="M5.84388 129.163V123.781H129.625V129.163H5.84388Z" fill="#B1E515"/>
|
||||
<path d="M21.9892 64.5812V59.1995H113.48V64.5812H118.861V107.636H113.48V113.017H21.9892V107.636H16.6075V64.5812H21.9892Z" fill="#1F2320"/>
|
||||
<path d="M86.5707 86.1092V64.582H97.3343V86.1092H86.5707Z" fill="#D0FF00"/>
|
||||
<path d="M59.6617 86.1092H75.8071V91.491H70.4253V102.255H86.5707V96.8727H91.9525V102.255H86.5707V107.636H48.8982V102.255H43.5164V96.8727H48.8982V102.255H65.0435V91.491H59.6617V86.1092Z" fill="#D0FF00"/>
|
||||
<path d="M48.8982 80.7274V75.3456H32.7528V80.7274H27.371V75.3456H32.7528V69.9638H48.8982V75.3456H54.28V80.7274H48.8982Z" fill="#D0FF00"/>
|
||||
<path d="M118.861 43.0534V37.6716H124.243V43.0534H118.861Z" fill="#92C51B"/>
|
||||
<path d="M16.6075 43.0534V37.6716H11.2257V43.0534H16.6075Z" fill="#92C51B"/>
|
||||
<path d="M113.48 37.6728V32.291H118.861V37.6728H113.48Z" fill="#92C51B"/>
|
||||
<path d="M21.9892 37.6728V32.291H16.6075V37.6728H21.9892Z" fill="#92C51B"/>
|
||||
<rect x="65.0435" y="26.9087" width="5.38179" height="10.7636" fill="#B1E515"/>
|
||||
<path d="M86.5707 37.6723V32.2905H91.9525V26.9087H97.3342V32.2905H102.716V37.6723H86.5707Z" fill="#101412"/>
|
||||
<path d="M38.1346 32.2905V26.9087H43.5164V32.2905H48.8982V37.6723H32.7528V32.2905H38.1346Z" fill="#101412"/>
|
||||
<path d="M21.9892 129.163V123.781H38.1346V129.163H21.9892Z" fill="#92C51B"/>
|
||||
<rect x="59.6617" y="26.9087" width="16.1454" height="5.38179" fill="#92C51B"/>
|
||||
<path d="M191.977 42.5414V82.1808L230.437 40.331L246.794 52.7091L215.701 86.3068L257.109 142.745H229.848L200.966 102.074L191.977 111.8V142.745H170.315V42.5414H191.977Z" fill="#101412"/>
|
||||
<path d="M286.165 49.3199C286.165 52.66 284.937 55.5089 282.481 57.8666C280.124 60.1261 277.275 61.2559 273.935 61.2559C270.594 61.2559 267.795 60.1261 265.535 57.8666C263.276 55.5089 262.146 52.66 262.146 49.3199C262.146 45.9797 263.276 43.1308 265.535 40.7731C267.795 38.4153 270.594 37.2365 273.935 37.2365C277.275 37.2365 280.124 38.4153 282.481 40.7731C284.937 43.1308 286.165 45.9797 286.165 49.3199ZM284.839 71.2763V142.745H263.177V71.2763H284.839Z" fill="#101412"/>
|
||||
<path d="M332.949 145.25C324.108 145.25 316.789 142.991 310.993 138.472C305.295 133.953 302.446 127.322 302.446 118.578V89.1066H292.278V72.3078H302.446V54.6248L323.96 51.0882V72.3078H338.402L343.117 89.1066H323.96V115.336C323.96 118.775 324.845 121.624 326.613 123.883C328.381 126.044 330.935 127.125 334.276 127.125C335.553 127.125 336.928 126.929 338.402 126.536C339.875 126.143 341.349 125.602 342.822 124.915L349.159 140.24C347.489 141.615 345.033 142.794 341.791 143.777C338.549 144.759 335.602 145.25 332.949 145.25Z" fill="#101412"/>
|
||||
<path d="M389.435 145.25C380.593 145.25 373.274 142.991 367.478 138.472C361.781 133.953 358.932 127.322 358.932 118.578V89.1066H348.764V72.3078H358.932V54.6248L380.446 51.0882V72.3078H394.887L399.602 89.1066H380.446V115.336C380.446 118.775 381.33 121.624 383.098 123.883C384.867 126.044 387.421 127.125 390.761 127.125C392.038 127.125 393.413 126.929 394.887 126.536C396.361 126.143 397.834 125.602 399.308 124.915L405.644 140.24C403.974 141.615 401.518 142.794 398.276 143.777C395.034 144.759 392.087 145.25 389.435 145.25Z" fill="#101412"/>
|
||||
<path d="M428.679 145.103L431.184 139.061L405.249 73.9287L427.058 71.2763C429.711 78.3495 432.363 85.4227 435.016 92.4959C437.668 99.5691 440.272 106.642 442.826 113.715L457.856 71.2763H480.255L448.425 151.734C446.068 157.53 441.991 162.491 436.195 166.617C430.398 170.841 424.209 173.739 417.627 175.311L410.112 157.776C413.649 156.4 417.333 154.681 421.164 152.618C424.995 150.555 427.5 148.05 428.679 145.103Z" fill="#101412"/>
|
||||
<path d="M508.208 142.745L492.371 126.907V50.9472L508.208 35.1094H555.953L571.867 50.9472V75.7034H550.109V61.557L545.42 56.8672H518.818L514.128 61.557V116.297L518.818 120.987H545.42L550.109 116.297V102.151H571.867V126.907L555.953 142.745H508.208Z" fill="#101412"/>
|
||||
<path d="M588.792 142.745V50.9472L604.63 35.1094H654.296L670.134 50.9472V142.745H648.453V111.069H610.473V142.745H588.792ZM610.473 89.3885H648.453V61.557L643.763 56.8672H615.163L610.473 61.557V89.3885Z" fill="#101412"/>
|
||||
<path d="M688.517 142.745V35.1094H752.561L768.399 50.9472V126.907L752.561 142.745H688.517ZM710.275 120.987H742.028L746.718 116.297V61.557L742.028 56.8672H710.275V120.987Z" fill="#101412"/>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
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)
|
||||
|
331
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.71"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -81,8 +81,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs-extra",
|
||||
"tokio",
|
||||
"toml 0.6.0",
|
||||
"toml 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -109,22 +110,6 @@ dependencies = [
|
||||
"system-deps 6.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@ -233,6 +218,9 @@ name = "bytes"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
@ -265,7 +253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -324,8 +312,10 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -494,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -534,7 +524,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -545,7 +535,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -627,7 +617,7 @@ checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"rustc_version",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
"vswhom",
|
||||
"winreg 0.11.0",
|
||||
]
|
||||
@ -647,6 +637,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
@ -798,7 +794,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -822,6 +818,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
@ -1149,7 +1146,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@ -1162,6 +1159,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
@ -1269,7 +1272,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa 1.0.6",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.4.9",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@ -1290,6 +1293,19 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.56"
|
||||
@ -1377,7 +1393,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -1627,6 +1654,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.6.2"
|
||||
@ -1715,15 +1748,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom8"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -1806,9 +1830,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "4.4.1"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a6e2a2b13a56ebeabba9142f911745be6456163fd6c3d361274ebcd891a80c"
|
||||
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"chrono",
|
||||
@ -1911,7 +1935,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2098,9 +2122,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@ -2121,7 +2145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@ -2160,7 +2184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.8",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2195,9 +2219,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -2387,10 +2411,12 @@ dependencies = [
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@ -2400,11 +2426,14 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg 0.10.1",
|
||||
@ -2501,9 +2530,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.1"
|
||||
version = "0.101.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
|
||||
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
@ -2621,29 +2650,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
|
||||
dependencies = [
|
||||
"itoa 1.0.6",
|
||||
"ryu",
|
||||
@ -2668,14 +2697,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
|
||||
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -2694,14 +2723,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "2.3.3"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
|
||||
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"base64 0.21.2",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@ -2710,14 +2740,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "2.3.3"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
|
||||
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2809,6 +2839,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soup2"
|
||||
version = "0.2.1"
|
||||
@ -2903,9 +2943,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2934,7 +2974,7 @@ dependencies = [
|
||||
"cfg-expr 0.15.2",
|
||||
"heck 0.4.1",
|
||||
"pkg-config",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
@ -3015,12 +3055,13 @@ checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.3.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
|
||||
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
"base64 0.21.2",
|
||||
"bytes",
|
||||
"cocoa",
|
||||
"dirs-next",
|
||||
"embed_plist",
|
||||
@ -3033,6 +3074,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"http",
|
||||
"ignore",
|
||||
"minisign-verify",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"open",
|
||||
@ -3040,6 +3082,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"semver",
|
||||
"serde",
|
||||
@ -3054,19 +3097,21 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.39.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
|
||||
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@ -3077,14 +3122,13 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde"
|
||||
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"brotli",
|
||||
@ -3108,9 +3152,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46"
|
||||
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@ -3120,11 +3164,23 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422"
|
||||
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@ -3143,9 +3199,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1"
|
||||
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@ -3163,12 +3219,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
|
||||
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
"dunce",
|
||||
"glob",
|
||||
"heck 0.4.1",
|
||||
"html5ever",
|
||||
@ -3196,7 +3253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
|
||||
dependencies = [
|
||||
"embed-resource",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3246,7 +3303,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3288,21 +3345,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.29.1"
|
||||
version = "1.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.4",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
@ -3338,69 +3404,60 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.6.0"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.5.1",
|
||||
"toml_edit 0.18.1",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
|
||||
checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.2",
|
||||
"toml_edit 0.19.8",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.5.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.18.1"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"nom8",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.5.1",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.8"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
|
||||
checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.2",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@ -3430,7 +3487,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3655,7 +3712,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -3689,7 +3746,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -3700,6 +3757,19 @@ version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.63"
|
||||
@ -3759,9 +3829,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
@ -4122,9 +4192,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.1"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
|
||||
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -4215,3 +4285,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
@ -12,16 +12,17 @@ rust-version = "1.60"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.3.0", features = [] }
|
||||
tauri-build = { version = "1.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
oauth2 = "4.4.1"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "1.3.0", features = ["dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
||||
tokio = { version = "1.29.1", features = ["time"] }
|
||||
toml = "0.6.0"
|
||||
tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||
tokio = { version = "1.32.0", features = ["time"] }
|
||||
toml = "0.8.0"
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build()
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 122 KiB |
@ -98,6 +98,7 @@ fn main() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
||||
.plugin(tauri_plugin_fs_extra::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@ -7,8 +7,8 @@
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "KittyCAD Modeling",
|
||||
"version": "0.0.3"
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.7.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@ -23,7 +23,8 @@
|
||||
},
|
||||
"fs": {
|
||||
"scope": [
|
||||
"$HOME/**/*"
|
||||
"$HOME/**/*",
|
||||
"$APPDATA/**/*"
|
||||
],
|
||||
"all": true
|
||||
},
|
||||
@ -37,6 +38,9 @@
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@ -54,7 +58,7 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "KittyCAD-modeling-app",
|
||||
"identifier": "io.kittycad.modeling-app",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
@ -76,7 +80,12 @@
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
|
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 { render, screen } from '@testing-library/react'
|
||||
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
|
||||
;(global as any).ResizeObserver = class ResizeObserver {
|
||||
@ -12,12 +15,38 @@ let listener: ((rect: any) => void) | undefined = undefined
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(
|
||||
describe('App tests', () => {
|
||||
test('Renders the modeling app screen, including "Variables" pane.', () => {
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = (await vi.importActual('react-router-dom')) as Record<
|
||||
string,
|
||||
any
|
||||
>
|
||||
return {
|
||||
...actual,
|
||||
useParams: () => ({ id: '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>
|
||||
<App />
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
const linkElement = screen.getByText(/Variables/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
462
src/App.tsx
@ -1,38 +1,16 @@
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
import { useEffect, useCallback, MouseEventHandler } from 'react'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { asyncLexer } from './lang/tokeniser'
|
||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
||||
import { _executor } from './lang/executor'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import { langs } from '@uiw/codemirror-extensions-langs'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { ViewUpdate } from '@codemirror/view'
|
||||
import {
|
||||
lineHighlightField,
|
||||
addLineHighlight,
|
||||
} from './editor/highlightextension'
|
||||
import { PaneType, Selections, Themes, useStore } from './useStore'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
import { Logs, KCLErrors } from './components/Logs'
|
||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||
import { MemoryPanel } from './components/MemoryPanel'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
import {
|
||||
EngineCommand,
|
||||
EngineCommandManager,
|
||||
} from './lang/std/engineConnection'
|
||||
import { isOverlap, throttle } from './lib/utils'
|
||||
import { EngineCommand } from './lang/std/engineConnection'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
faCode,
|
||||
@ -40,94 +18,48 @@ import {
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TEST } from './env'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { getSystemTheme } from './lib/getSystemTheme'
|
||||
|
||||
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'
|
||||
|
||||
export function App() {
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
|
||||
useHotKeyListener()
|
||||
const {
|
||||
editorView,
|
||||
setEditorView,
|
||||
setSelectionRanges,
|
||||
selectionRanges,
|
||||
addLog,
|
||||
addKCLError,
|
||||
code,
|
||||
setCode,
|
||||
setAst,
|
||||
setError,
|
||||
setProgramMemory,
|
||||
resetLogs,
|
||||
resetKCLErrors,
|
||||
selectionRangeTypeMap,
|
||||
setArtifactMap,
|
||||
engineCommandManager,
|
||||
setEngineCommandManager,
|
||||
setHighlightRange,
|
||||
setCursor2,
|
||||
sourceRangeMap,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
isStreamReady,
|
||||
isMouseDownInStream,
|
||||
fileId,
|
||||
cmdId,
|
||||
setCmdId,
|
||||
token,
|
||||
formatCode,
|
||||
debugPanel,
|
||||
theme,
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
onboardingStatus,
|
||||
setDidDragInStream,
|
||||
setStreamDimensions,
|
||||
didDragInStream,
|
||||
streamDimensions,
|
||||
guiMode,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
setEditorView: s.setEditorView,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setGuiMode: s.setGuiMode,
|
||||
addLog: s.addLog,
|
||||
code: s.code,
|
||||
guiMode: s.guiMode,
|
||||
setCode: s.setCode,
|
||||
setAst: s.setAst,
|
||||
setError: s.setError,
|
||||
setProgramMemory: s.setProgramMemory,
|
||||
resetLogs: s.resetLogs,
|
||||
resetKCLErrors: s.resetKCLErrors,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
isShiftDown: s.isShiftDown,
|
||||
setCursor: s.setCursor,
|
||||
setCursor2: s.setCursor2,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
setMediaStream: s.setMediaStream,
|
||||
isStreamReady: s.isStreamReady,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
fileId: s.fileId,
|
||||
cmdId: s.cmdId,
|
||||
setCmdId: s.setCmdId,
|
||||
token: s.token,
|
||||
formatCode: s.formatCode,
|
||||
debugPanel: s.debugPanel,
|
||||
addKCLError: s.addKCLError,
|
||||
theme: s.theme,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
onboardingStatus: s.onboardingStatus,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
didDragInStream: s.didDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
|
||||
// Pane toggling keyboard shortcuts
|
||||
@ -145,229 +77,91 @@ export function App() {
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
|
||||
const paneOpacity =
|
||||
onboardingStatus === 'camera'
|
||||
onboardingStatus === onboardingPaths.CAMERA
|
||||
? 'opacity-20'
|
||||
: isMouseDownInStream
|
||||
: didDragInStream
|
||||
? 'opacity-40'
|
||||
: ''
|
||||
|
||||
// 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 =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
const pixelDensity = window.devicePixelRatio
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
const streamHeight = streamRef?.current?.offsetHeight
|
||||
|
||||
const width = streamWidth ? streamWidth * pixelDensity : 0
|
||||
const quadWidth = Math.round(width / 4) * 4
|
||||
const height = streamHeight ? streamHeight * pixelDensity : 0
|
||||
const quadHeight = Math.round(height / 4) * 4
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
streamHeight: quadHeight,
|
||||
})
|
||||
if (!width || !height) return
|
||||
const eng = new EngineCommandManager({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
token,
|
||||
})
|
||||
setEngineCommandManager(eng)
|
||||
return () => {
|
||||
eng?.tearDown()
|
||||
}
|
||||
}, [quadWidth, quadHeight])
|
||||
|
||||
// Use file code loaded from disk
|
||||
// on mount, and overwrite any locally-stored code
|
||||
useEffect(() => {
|
||||
if (!isStreamReady) return
|
||||
const asyncWrap = async () => {
|
||||
try {
|
||||
if (!code) {
|
||||
setAst(null)
|
||||
return
|
||||
}
|
||||
const tokens = await asyncLexer(code)
|
||||
const _ast = abstractSyntaxTree(tokens)
|
||||
setAst(_ast)
|
||||
resetLogs()
|
||||
resetKCLErrors()
|
||||
if (engineCommandManager) {
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
}
|
||||
if (!engineCommandManager) return
|
||||
const programMemory = await _executor(
|
||||
_ast,
|
||||
{
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
addLog(a)
|
||||
},
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [],
|
||||
sourceRange: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
_0: {
|
||||
type: 'userVal',
|
||||
value: 0,
|
||||
__meta: [],
|
||||
},
|
||||
_90: {
|
||||
type: 'userVal',
|
||||
value: 90,
|
||||
__meta: [],
|
||||
},
|
||||
_180: {
|
||||
type: 'userVal',
|
||||
value: 180,
|
||||
__meta: [],
|
||||
},
|
||||
_270: {
|
||||
type: 'userVal',
|
||||
value: 270,
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
},
|
||||
engineCommandManager,
|
||||
{ bodyType: 'root' },
|
||||
[]
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
|
||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||
engineCommandManager.onHover((id) => {
|
||||
if (!id) {
|
||||
setHighlightRange([0, 0])
|
||||
} else {
|
||||
const sourceRange = sourceRangeMap[id]
|
||||
setHighlightRange(sourceRange)
|
||||
}
|
||||
})
|
||||
engineCommandManager.onClick((selections) => {
|
||||
if (!selections) {
|
||||
setCursor2()
|
||||
return
|
||||
}
|
||||
const { id, type } = selections
|
||||
setCursor2({ range: sourceRangeMap[id], type })
|
||||
})
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setError()
|
||||
} catch (e: any) {
|
||||
if (e instanceof KCLError) {
|
||||
addKCLError(e)
|
||||
} else {
|
||||
setError('problem')
|
||||
console.log(e)
|
||||
addLog(e)
|
||||
}
|
||||
if (isTauri() && loadedCode !== null) {
|
||||
setCode(loadedCode)
|
||||
}
|
||||
return () => {
|
||||
// Clear code on unmount if in desktop app
|
||||
if (isTauri()) {
|
||||
setCode('')
|
||||
}
|
||||
}
|
||||
asyncWrap()
|
||||
}, [code, isStreamReady])
|
||||
}, [loadedCode, setCode])
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager?.sendSceneCommand(message)
|
||||
}, 16)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
currentTarget,
|
||||
}) => {
|
||||
if (isMouseDownInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.nativeEvent.preventDefault()
|
||||
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
el: currentTarget,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: e.currentTarget,
|
||||
...streamDimensions,
|
||||
})
|
||||
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
setCmdId(newCmdId)
|
||||
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
|
||||
}
|
||||
|
||||
if (cmdId && isMouseDownInStream) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -376,59 +170,39 @@ export function App() {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
file_id: fileId,
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
file_id: fileId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const extraExtensions = useMemo(() => {
|
||||
if (TEST) return []
|
||||
return [
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
]
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen overflow-hidden relative flex flex-col"
|
||||
className="relative h-full flex flex-col"
|
||||
onMouseMove={handleMouseMove}
|
||||
ref={streamRef}
|
||||
>
|
||||
<AppHeader
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
project={project}
|
||||
enableMenu={true}
|
||||
/>
|
||||
<ModalContainer />
|
||||
<Resizable
|
||||
className={
|
||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||
(isMouseDownInStream || onboardingStatus === 'camera'
|
||||
(buttonDownInStream || onboardingStatus === 'camera'
|
||||
? ' pointer-events-none '
|
||||
: ' ') +
|
||||
paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '400px',
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={600}
|
||||
maxWidth={800}
|
||||
minHeight={'auto'}
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
@ -442,31 +216,9 @@ export function App() {
|
||||
icon={faCode}
|
||||
className="open:!mb-2"
|
||||
open={openPanes.includes('code')}
|
||||
menu={<CodeMenu />}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
<button
|
||||
// disabled={!shouldFormat}
|
||||
onClick={formatCode}
|
||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
||||
>
|
||||
format
|
||||
</button>
|
||||
</div>
|
||||
<div id="code-mirror-override">
|
||||
<CodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={[
|
||||
langs.javascript({ jsx: true }),
|
||||
lineHighlightField,
|
||||
...extraExtensions,
|
||||
]}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={editorTheme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
<TextEditor theme={editorTheme} />
|
||||
</CollapsiblePanel>
|
||||
<section className="flex flex-col">
|
||||
<MemoryPanel
|
||||
@ -491,13 +243,13 @@ export function App() {
|
||||
</div>
|
||||
</Resizable>
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{debugPanel && (
|
||||
{showDebugPanel && (
|
||||
<DebugPanel
|
||||
title="Debug"
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
|
36
src/Auth.tsx
@ -1,38 +1,14 @@
|
||||
import useSWR from 'swr'
|
||||
import fetcher from './lib/fetcher'
|
||||
import withBaseUrl from './lib/withBaseURL'
|
||||
import { User, useStore } from './useStore'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
import Loading from './components/Loading'
|
||||
import { paths } from './Router'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
// Wrapper around protected routes, used in src/Router.tsx
|
||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const { data: user, isLoading } = useSWR<
|
||||
User | Partial<{ error_code: string }>
|
||||
>(withBaseUrl('/user'), fetcher)
|
||||
const { token, setUser } = useStore((s) => ({
|
||||
token: s.token,
|
||||
setUser: s.setUser,
|
||||
}))
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
auth: { state },
|
||||
} = useGlobalStateContext()
|
||||
const isLoggedIn = state.matches('checkIfLoggedIn')
|
||||
|
||||
useEffect(() => {
|
||||
if (user && 'id' in user) setUser(user)
|
||||
}, [user, setUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(isTauri() && !token) ||
|
||||
(!isTauri() && !isLoading && !(user && 'id' in user))
|
||||
) {
|
||||
navigate(paths.SIGN_IN)
|
||||
}
|
||||
}, [user, token, navigate, isLoading])
|
||||
|
||||
return isLoading ? (
|
||||
return isLoggedIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
) : (
|
||||
<>{children}</>
|
||||
|
262
src/Router.tsx
@ -3,8 +3,15 @@ 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, {
|
||||
@ -13,6 +20,59 @@ import Onboarding, {
|
||||
} 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'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
|
||||
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) => {
|
||||
@ -26,62 +86,182 @@ const prependRoutes =
|
||||
|
||||
export const paths = {
|
||||
INDEX: '/',
|
||||
HOME: '/home',
|
||||
FILE: '/file',
|
||||
SETTINGS: '/settings',
|
||||
SIGN_IN: '/signin',
|
||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||
'/onboarding/'
|
||||
'/onboarding'
|
||||
) as typeof onboardingPaths,
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: paths.INDEX,
|
||||
element: (
|
||||
<Auth>
|
||||
<Outlet />
|
||||
<App />
|
||||
</Auth>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
loader: ({ request }) => {
|
||||
const store = localStorage.getItem('store')
|
||||
if (store === null) {
|
||||
return redirect(paths.ONBOARDING.INDEX)
|
||||
} else {
|
||||
const status = JSON.parse(store).state.onboardingStatus || ''
|
||||
const notEnRouteToOnboarding =
|
||||
!request.url.includes(paths.ONBOARDING.INDEX) &&
|
||||
request.method === 'GET'
|
||||
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'),
|
||||
},
|
||||
{
|
||||
path: paths.FILE + '/:id',
|
||||
element: (
|
||||
<Auth>
|
||||
<Outlet />
|
||||
<ModelingMachineProvider>
|
||||
<App />
|
||||
</ModelingMachineProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
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 !== undefined && status.length === 0) ||
|
||||
!(status === 'done' || status === 'dismissed')
|
||||
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(paths.ONBOARDING.INDEX + status)
|
||||
return redirect(
|
||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: paths.SETTINGS,
|
||||
element: <Settings />,
|
||||
{
|
||||
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,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: paths.ONBOARDING.INDEX,
|
||||
element: <Onboarding />,
|
||||
children: onboardingRoutes,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: paths.SIGN_IN,
|
||||
element: <SignIn />,
|
||||
},
|
||||
])
|
||||
children: [
|
||||
{
|
||||
path: makeUrlPathRelative(paths.SETTINGS),
|
||||
element: <Settings />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: paths.SIGN_IN,
|
||||
element: <SignIn />,
|
||||
},
|
||||
])
|
||||
)
|
||||
|
||||
/**
|
||||
* All routes in the app, used in src/index.tsx
|
||||
|
60
src/Toolbar.module.css
Normal file
@ -0,0 +1,60 @@
|
||||
.toolbarWrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@apply flex gap-4 items-center rounded-full;
|
||||
@apply border border-cool-20/30 bg-cool-10/50;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar {
|
||||
@apply border-cool-100/50 bg-cool-120/50;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbar {
|
||||
@apply border-fern-20/20 bg-fern-10/20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbar {
|
||||
@apply border-fern-120/50 bg-fern-100/30;
|
||||
}
|
||||
|
||||
.toolbarCap {
|
||||
@apply text-sm font-bold;
|
||||
@apply bg-cool-20/50 text-cool-100;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarCap {
|
||||
@apply bg-cool-90/50 text-cool-30;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbarCap {
|
||||
@apply bg-fern-20/50 text-fern-100;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbarCap {
|
||||
@apply bg-fern-90/50 text-fern-30;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply self-stretch flex items-center px-4 py-1;
|
||||
@apply rounded-l-full;
|
||||
}
|
||||
|
||||
.popoverToggle {
|
||||
@apply self-stretch m-0 flex items-center px-4 py-1;
|
||||
@apply rounded-r-full border-none;
|
||||
@apply hover:bg-cool-20;
|
||||
}
|
||||
|
||||
:global(.dark) .popoverToggle {
|
||||
@apply hover:bg-cool-90;
|
||||
}
|
||||
|
||||
:global(.sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-90;
|
||||
}
|
349
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
||||
import { useStore, toolTips } from './useStore'
|
||||
import { useStore, toolTips, Selections } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
@ -8,10 +8,15 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { ExportButton } from './components/ExportButton'
|
||||
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'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -21,6 +26,7 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
@ -28,75 +34,30 @@ export const Toolbar = () => {
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
}))
|
||||
useAppMode()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ExportButton />
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
>
|
||||
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)
|
||||
}}
|
||||
>
|
||||
SketchOnFace
|
||||
</button>
|
||||
)}
|
||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<>
|
||||
useEffect(() => {
|
||||
console.log('guiMode', guiMode)
|
||||
}, [guiMode])
|
||||
|
||||
function ToolbarButtons() {
|
||||
return (
|
||||
<span className="overflow-x-auto">
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch
|
||||
Start Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditExtrude' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
@ -104,77 +65,211 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
const { modifiedAst } = sketchOnExtrudedFace(
|
||||
ast,
|
||||
pathToNode,
|
||||
false
|
||||
programMemory
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
updateAst(modifiedAst)
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch (w/o pipe)
|
||||
SketchOnFace
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
guiMode.mode !== 'sketch' ||
|
||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||
)
|
||||
return null
|
||||
return (
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('guiMode.pathId', guiMode.pathId)
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<>
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() =>
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
? {
|
||||
sketchMode: 'sketchEdit',
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
ExtrudeSketch
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
false
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch (w/o pipe)
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
}}
|
||||
>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
})}
|
||||
<br></br>
|
||||
<ConvertToVariable />
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
<EqualAngle />
|
||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||
<SetAbsDistance buttonType="snapToYAxis" />
|
||||
<SetAbsDistance buttonType="xAbs" />
|
||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||
<SetAbsDistance buttonType="snapToXAxis" />
|
||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||
<SetAbsDistance buttonType="yAbs" />
|
||||
<SetAngleLength angleOrLength="setAngle" />
|
||||
<SetAngleLength angleOrLength="setLength" />
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</div>
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
guiMode.mode !== 'sketch' ||
|
||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||
)
|
||||
return null
|
||||
return (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
? {
|
||||
sketchMode: 'sketchEdit',
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === 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 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 />
|
||||
</section>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -1,52 +1,92 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
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 ActionButtonProps extends React.PropsWithChildren {
|
||||
interface BaseActionButtonProps {
|
||||
icon?: ActionIconProps
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
to?: string
|
||||
Element?:
|
||||
| 'button'
|
||||
| 'link'
|
||||
| React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
||||
}
|
||||
|
||||
export const ActionButton = ({
|
||||
icon,
|
||||
className,
|
||||
onClick,
|
||||
to = paths.INDEX,
|
||||
Element = 'button',
|
||||
children,
|
||||
...props
|
||||
}: ActionButtonProps) => {
|
||||
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
|
||||
icon ? 'pr-2' : 'px-2'
|
||||
} ${className}`
|
||||
type ActionButtonAsButton = BaseActionButtonProps &
|
||||
Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
keyof BaseActionButtonProps
|
||||
> & {
|
||||
Element: 'button'
|
||||
}
|
||||
|
||||
if (Element === 'button') {
|
||||
return (
|
||||
<button onClick={onClick} className={classNames} {...props}>
|
||||
{icon && <ActionIcon {...icon} />}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
} else if (Element === 'link') {
|
||||
return (
|
||||
<Link to={to} className={classNames} {...props}>
|
||||
{icon && <ActionIcon {...icon} />}
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Element onClick={onClick} className={classNames} {...props}>
|
||||
{icon && <ActionIcon {...icon} />}
|
||||
{children}
|
||||
</Element>
|
||||
)
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,28 @@
|
||||
import {
|
||||
IconDefinition,
|
||||
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'
|
||||
|
||||
const iconSizes = {
|
||||
sm: 12,
|
||||
md: 14.4,
|
||||
lg: 18,
|
||||
lg: 20,
|
||||
xl: 28,
|
||||
}
|
||||
|
||||
export interface ActionIconProps extends React.PropsWithChildren {
|
||||
icon?: IconDefinition
|
||||
icon?: SolidIconDefinition | BrandIconDefinition
|
||||
className?: string
|
||||
bgClassName?: string
|
||||
iconClassName?: string
|
||||
size?: keyof typeof iconSizes
|
||||
}
|
||||
|
||||
export const ActionIcon = ({
|
||||
icon,
|
||||
icon = faCircleExclamation,
|
||||
className,
|
||||
bgClassName,
|
||||
iconClassName,
|
||||
size = 'md',
|
||||
@ -27,19 +31,21 @@ export const ActionIcon = ({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'p-1 w-fit inline-grid place-content-center ' +
|
||||
`p-${
|
||||
size === 'xl' ? '2' : '1'
|
||||
} w-fit inline-grid place-content-center ${className} ` +
|
||||
(bgClassName ||
|
||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
||||
}
|
||||
>
|
||||
{children || (
|
||||
<FontAwesomeIcon
|
||||
icon={icon || faCircleExclamation}
|
||||
icon={icon}
|
||||
width={iconSizes[size]}
|
||||
height={iconSizes[size]}
|
||||
className={
|
||||
iconClassName ||
|
||||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
7
src/components/AppHeader.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
Some CSS cannot be represented
|
||||
in Tailwind, such as complex grid layouts.
|
||||
*/
|
||||
.header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
@ -1,38 +1,41 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Toolbar } from '../Toolbar'
|
||||
import { useStore } from '../useStore'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { paths } from '../Router'
|
||||
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 { user } = useStore((s) => ({
|
||||
user: s.user,
|
||||
}))
|
||||
const {
|
||||
auth: {
|
||||
context: { user },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
'overlaid-panes sticky top-0 z-10 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
|
||||
(showToolbar ? '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
|
||||
}
|
||||
>
|
||||
<Link to={paths.INDEX}>
|
||||
<img
|
||||
src="/kitt-arcade-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<span className="sr-only">KittyCAD App</span>
|
||||
</Link>
|
||||
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||
{/* Toolbar if the context deems it */}
|
||||
{showToolbar && (
|
||||
<div className="max-w-4xl">
|
||||
@ -40,7 +43,12 @@ export const AppHeader = ({
|
||||
</div>
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || <UserSidebarMenu user={user} />}
|
||||
{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,5 +1,5 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
||||
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
||||
import { executor } from '../lang/executor'
|
||||
import {
|
||||
@ -9,7 +9,6 @@ import {
|
||||
findUniqueName,
|
||||
} from '../lang/modifyAst'
|
||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||
import { lexer } from '../lang/tokeniser'
|
||||
import { useStore } from '../useStore'
|
||||
|
||||
export const AvailableVars = ({
|
||||
@ -144,8 +143,8 @@ export function useCalc({
|
||||
if (!engineCommandManager) return
|
||||
try {
|
||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||
const ast = abstractSyntaxTree(lexer(code))
|
||||
const _programMem: any = { root: {} }
|
||||
const ast = parser_wasm(code)
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
@ -199,29 +198,25 @@ export const CreateNewVariable = ({
|
||||
isNewVariableNameUnique,
|
||||
setNewVariableName,
|
||||
shouldCreateVariable,
|
||||
setShouldCreateVariable,
|
||||
setShouldCreateVariable = () => {},
|
||||
showCheckbox = true,
|
||||
}: {
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableName: string
|
||||
setNewVariableName: (a: string) => void
|
||||
shouldCreateVariable: boolean
|
||||
setShouldCreateVariable: (a: boolean) => void
|
||||
shouldCreateVariable?: boolean
|
||||
setShouldCreateVariable?: (a: boolean) => void
|
||||
showCheckbox?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
||||
>
|
||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex flex-1">
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
{showCheckbox && (
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
@ -233,7 +228,10 @@ export const CreateNewVariable = ({
|
||||
disabled={!shouldCreateVariable}
|
||||
name="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' : ''
|
||||
}`}
|
||||
value={newVariableName}
|
||||
|
19
src/components/CodeMenu.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
.button {
|
||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
||||
@apply transition-colors ease-out;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply text-chalkboard-30;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||
}
|
||||
|
||||
.button small {
|
||||
@apply text-chalkboard-60;
|
||||
}
|
||||
|
||||
:global(.dark) .button small {
|
||||
@apply text-chalkboard-40;
|
||||
}
|
59
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { 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'
|
||||
|
||||
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) => {
|
||||
if (e.eventPhase === 3) {
|
||||
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.Items>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
.panel {
|
||||
@apply relative overflow-auto z-0;
|
||||
@apply bg-chalkboard-20/40;
|
||||
@apply relative z-0;
|
||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
:global(.dark) .panel {
|
||||
@apply bg-chalkboard-110/50;
|
||||
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 z-10 cursor-pointer;
|
||||
@apply flex items-center gap-2 w-full p-2;
|
||||
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-20;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
||||
title: string
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
@ -18,21 +19,27 @@ export const PanelHeader = ({
|
||||
title,
|
||||
icon,
|
||||
iconClassNames,
|
||||
menu,
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
||||
children,
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
||||
{...props}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
>
|
||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
||||
<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', () => {
|
||||
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="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
||||
as="div"
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||
<div>
|
||||
{inSubCommand && (
|
||||
<p className="text-liquid-70 dark:text-liquid-30">
|
||||
{selectedCommand.item &&
|
||||
getDisplayValue(selectedCommand.item as Command)}
|
||||
</p>
|
||||
)}
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="bg-transparent focus:outline-none w-full"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
setCommandBarOpen(false)
|
||||
if (
|
||||
inSubCommand &&
|
||||
event.key === 'Backspace' &&
|
||||
!event.currentTarget.value
|
||||
) {
|
||||
setSubCommandIndex(subCommandIndex - 1)
|
||||
setSelectedCommand(null)
|
||||
}
|
||||
}}
|
||||
displayValue={(command: SortedCommand) =>
|
||||
command !== null ? command.item.name : ''
|
||||
}
|
||||
placeholder={
|
||||
inSubCommand
|
||||
? `Enter <${currentSubCommand?.name}>`
|
||||
: 'Search for a command'
|
||||
}
|
||||
value={query}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
||||
{filteredCommands?.map((commandResult) => (
|
||||
<Combobox.Option
|
||||
key={commandResult.item.name}
|
||||
value={commandResult}
|
||||
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
|
||||
>
|
||||
<p>{commandResult.item.name}</p>
|
||||
{(commandResult.item as SubCommand).description && (
|
||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||
{(commandResult.item as SubCommand).description}
|
||||
</p>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarProvider
|
@ -5,9 +5,11 @@ 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<
|
||||
EngineCommand['cmd'],
|
||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||
{ type: 'default_camera_enable_sketch_mode' }
|
||||
>
|
||||
|
||||
@ -22,6 +24,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
distance_to_plane: 100,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
})
|
||||
if (!sketchModeCmd) return null
|
||||
return (
|
||||
@ -73,12 +76,12 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
/>
|
||||
</div>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: sketchModeCmd,
|
||||
cmd_id: uuidv4(),
|
||||
file_id: uuidv4(),
|
||||
})
|
||||
}}
|
||||
className="hover:border-succeed-50"
|
||||
@ -92,6 +95,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
>
|
||||
Send sketch mode command
|
||||
</ActionButton>
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
</section>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
|
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://github.com/KittyCAD/modeling-app/releases"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
||||
>
|
||||
our GitHub repository
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</p>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default DownloadAppBanner
|
@ -1,6 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import Modal from 'react-modal'
|
||||
import React from 'react'
|
||||
@ -9,7 +9,15 @@ import { Models } from '@kittycad/lib'
|
||||
|
||||
type OutputFormat = Models['OutputFormat_type']
|
||||
|
||||
export const ExportButton = () => {
|
||||
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,
|
||||
}))
|
||||
@ -19,17 +27,6 @@ export const ExportButton = () => {
|
||||
const defaultType = 'gltf'
|
||||
const [type, setType] = React.useState(defaultType)
|
||||
|
||||
const customModalStyles = {
|
||||
content: {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
marginRight: '-50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
setIsOpen(true)
|
||||
}
|
||||
@ -42,6 +39,7 @@ export const ExportButton = () => {
|
||||
const initialValues: OutputFormat = {
|
||||
type: defaultType,
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
}
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
@ -79,7 +77,6 @@ export const ExportButton = () => {
|
||||
format: values,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
file_id: uuidv4(),
|
||||
})
|
||||
|
||||
closeModal()
|
||||
@ -88,20 +85,26 @@ export const ExportButton = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={openModal}>Export</button>
|
||||
<ActionButton
|
||||
onClick={openModal}
|
||||
Element="button"
|
||||
icon={{ icon: faFileExport }}
|
||||
className={className?.button}
|
||||
>
|
||||
{children || 'Export'}
|
||||
</ActionButton>
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
contentLabel="Export"
|
||||
style={customModalStyles}
|
||||
overlayClassName="z-40 fixed inset-0 grid place-items-center"
|
||||
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
|
||||
>
|
||||
<div className="text-black">
|
||||
<h1 className="text-2xl font-bold">Export your design</h1>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<p>
|
||||
<label htmlFor="type">Type</label>
|
||||
</p>
|
||||
<p>
|
||||
<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"
|
||||
@ -109,6 +112,7 @@ export const ExportButton = () => {
|
||||
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>
|
||||
@ -116,56 +120,51 @@ export const ExportButton = () => {
|
||||
<option value="step">step</option>
|
||||
<option value="stl">stl</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
</label>
|
||||
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
||||
<>
|
||||
<p>
|
||||
{' '}
|
||||
<label htmlFor="storage">Storage</label>
|
||||
</p>
|
||||
<p>
|
||||
<select
|
||||
id="storage"
|
||||
name="storage"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.storage}
|
||||
>
|
||||
{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>
|
||||
</p>
|
||||
</>
|
||||
<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">
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between mt-6">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={closeModal}
|
||||
icon={{
|
||||
icon: faXmark,
|
||||
@ -177,8 +176,15 @@ export const ExportButton = () => {
|
||||
>
|
||||
Close
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
icon={{ icon: faFileExport }}
|
||||
>
|
||||
Export
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
158
src/components/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
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'
|
||||
|
||||
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() {
|
||||
const url = withBaseUrl('/logout')
|
||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useEffect } from 'react'
|
||||
import { Themes, useStore } from '../useStore'
|
||||
import { useStore } from '../useStore'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { processMemory } from './MemoryPanel'
|
||||
import { lexer } from '../lang/tokeniser'
|
||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
||||
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { initPromise } from '../lang/rust'
|
||||
|
||||
@ -11,49 +10,39 @@ describe('processMemory', () => {
|
||||
// Enable rotations #152
|
||||
const code = `
|
||||
const myVar = 5
|
||||
const myFn = (a) => {
|
||||
fn myFn = (a) => {
|
||||
return a - 2
|
||||
}
|
||||
const otherVar = myFn(5)
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|> lineTo([-2.4, myVar], %)
|
||||
|> lineTo([-0.76, otherVar], %)
|
||||
|> extrude(4, %)
|
||||
|
||||
|
||||
const theSketch = startSketchAt([0, 0])
|
||||
|> lineTo([-3.35, 0.17], %)
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)
|
||||
show(theExtrude, theSketch)`
|
||||
const tokens = lexer(code)
|
||||
const ast = abstractSyntaxTree(tokens)
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
console.log('raw log', a)
|
||||
},
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.myFn).toEqual('__function__')
|
||||
expect(output.otherVar).toEqual(3)
|
||||
expect(output).toEqual({
|
||||
myVar: 5,
|
||||
myFn: '__function__',
|
||||
myFn: undefined,
|
||||
otherVar: 3,
|
||||
theExtrude: [],
|
||||
theSketch: [
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
|
||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
|
||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
|
||||
{ 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], name: '' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,9 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes, useStore } from '../useStore'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory } from '../lang/executor'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
@ -48,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
Object.keys(programMemory.root).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
|
106
src/components/ModelingMachineProvider.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import React, { createContext, useRef } from 'react'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useCodeEval } from 'hooks/useCodeEval'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
}
|
||||
|
||||
export const ModelingMachineContext = createContext(
|
||||
{} as MachineContext<typeof modelingMachine>
|
||||
)
|
||||
|
||||
export const ModelingMachineProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const {
|
||||
auth: {
|
||||
context: { token },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useSetupEngineManager(streamRef, token)
|
||||
useCodeEval()
|
||||
|
||||
// const { commands } = useCommandsContext()
|
||||
|
||||
// Settings machine setup
|
||||
// const retrievedSettings = useRef(
|
||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||
// )
|
||||
|
||||
// What should we persist from modeling state? Nothing?
|
||||
// const persistedSettings = Object.assign(
|
||||
// settingsMachine.initialState.context,
|
||||
// JSON.parse(retrievedSettings.current) as Partial<
|
||||
// (typeof settingsMachine)['context']
|
||||
// >
|
||||
// )
|
||||
|
||||
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
||||
// context: persistedSettings,
|
||||
actions: {
|
||||
'Modify AST': () => {},
|
||||
'Make selection horizontal': () => {},
|
||||
'Make selection vertical': () => {},
|
||||
'Update code selection cursors': () => {},
|
||||
},
|
||||
guards: {
|
||||
'Can make selection horizontal': () => true,
|
||||
'Can make selection vertical': () => true,
|
||||
'Selection contains axis': () => true,
|
||||
'Selection contains edge': () => true,
|
||||
'Selection contains face': () => true,
|
||||
'Selection contains line': () => true,
|
||||
'Selection contains point': () => true,
|
||||
'Selection is empty': () => true,
|
||||
'Selection is not empty': () => true,
|
||||
'Selection is one face': () => true,
|
||||
'Selection is one or more edges': () => true,
|
||||
},
|
||||
services: {
|
||||
createSketch: async () => {},
|
||||
createLine: async () => {},
|
||||
createExtrude: async () => {},
|
||||
createFillet: async () => {},
|
||||
},
|
||||
})
|
||||
|
||||
// useStateMachineCommands({
|
||||
// state: settingsState,
|
||||
// send: settingsSend,
|
||||
// commands,
|
||||
// owner: 'settings',
|
||||
// commandBarMeta: settingsCommandBarMeta,
|
||||
// })
|
||||
|
||||
return (
|
||||
<ModelingMachineContext.Provider
|
||||
value={{
|
||||
state: modelingState,
|
||||
context: modelingState.context,
|
||||
send: modelingSend,
|
||||
}}
|
||||
>
|
||||
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
|
||||
{children}
|
||||
</div>
|
||||
</ModelingMachineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelingMachineProvider
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NetworkHealthIndicator,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('NetworkHealthIndicator tests', () => {
|
||||
test('Renders the network indicator', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
)
|
||||
})
|
||||
|
||||
test('Responds to network changes', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
)
|
||||
})
|
||||
})
|
112
src/components/NetworkHealthIndicator.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamation,
|
||||
faWifi,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
|
||||
export const NETWORK_CONTENT = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
|
||||
useEffect(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'p-0 border-none relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
bgClassName={
|
||||
hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faCheck}
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
<span
|
||||
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||
data-testid="network-bad"
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
}
|
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
|
81
src/components/ProjectSidebarMenu.test.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
|
||||
const now = new Date()
|
||||
const projectWellFormed = {
|
||||
name: 'Simple Box',
|
||||
path: '/some/path/Simple Box',
|
||||
children: [
|
||||
{
|
||||
name: 'main.kcl',
|
||||
path: '/some/path/Simple Box/main.kcl',
|
||||
},
|
||||
],
|
||||
entrypoint_metadata: {
|
||||
accessedAt: now,
|
||||
blksize: 32,
|
||||
blocks: 32,
|
||||
createdAt: now,
|
||||
dev: 1,
|
||||
gid: 1,
|
||||
ino: 1,
|
||||
isDir: false,
|
||||
isFile: true,
|
||||
isSymlink: false,
|
||||
mode: 1,
|
||||
modifiedAt: now,
|
||||
nlink: 1,
|
||||
permissions: { readonly: false, mode: 1 },
|
||||
rdev: 1,
|
||||
size: 32,
|
||||
uid: 1,
|
||||
},
|
||||
} satisfies ProjectWithEntryPointMetadata
|
||||
|
||||
describe('ProjectSidebarMenu tests', () => {
|
||||
test('Renders the project name', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
|
||||
|
||||
expect(screen.getByTestId('projectName')).toHaveTextContent(
|
||||
projectWellFormed.name
|
||||
)
|
||||
expect(screen.getByTestId('createdAt')).toHaveTextContent(
|
||||
`Created ${now.toLocaleDateString()}`
|
||||
)
|
||||
})
|
||||
|
||||
test('Renders app name if given no project', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<ProjectSidebarMenu />
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
|
||||
|
||||
expect(screen.getByTestId('projectName')).toHaveTextContent(
|
||||
'KittyCAD Modeling App'
|
||||
)
|
||||
})
|
||||
|
||||
test('Renders as a link if set to do so', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('project-sidebar-link')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('project-sidebar-link-name')).toHaveTextContent(
|
||||
projectWellFormed.name
|
||||
)
|
||||
})
|
||||
})
|
125
src/components/ProjectSidebarMenu.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExportButton } from './ExportButton'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
renderAsLink = false,
|
||||
}: {
|
||||
renderAsLink?: boolean
|
||||
project?: Partial<ProjectWithEntryPointMetadata>
|
||||
}) => {
|
||||
return renderAsLink ? (
|
||||
<Link
|
||||
to={'../'}
|
||||
className="flex items-center gap-4 my-2"
|
||||
data-testid="project-sidebar-link"
|
||||
>
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<span
|
||||
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max"
|
||||
data-testid="project-sidebar-link-name"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className="border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
||||
data-testid="project-sidebar-toggle"
|
||||
>
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max">
|
||||
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
enter="duration-200 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 -translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 -translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-energy-10 text-mono"
|
||||
data-testid="projectName"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</p>
|
||||
{project?.entrypoint_metadata && (
|
||||
<p
|
||||
className="m-0 text-energy-40 text-xs"
|
||||
data-testid="createdAt"
|
||||
>
|
||||
Created{' '}
|
||||
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
}}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectSidebarMenu
|
@ -1,6 +1,9 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Fragment } from 'react'
|
||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export const SetVarNameModal = ({
|
||||
isOpen,
|
||||
@ -19,67 +22,65 @@ export const SetVarNameModal = ({
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onReject}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
onClose={onReject}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
enterFrom="opacity-0 translate-y-4"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="ease-in duration-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
toast.success(`Added variable ${newVariableName}`)
|
||||
}}
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
icon={{ icon: faPlus }}
|
||||
>
|
||||
Set {valueName}
|
||||
</Dialog.Title>
|
||||
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
setShouldCreateVariable={() => {}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||
!isNewVariableNameUnique
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
Add variable
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
|
@ -7,37 +7,58 @@ import {
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { throttle } from '../lib/utils'
|
||||
import { EngineCommand } from '../lang/std/engineConnection'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
compareVec2Epsilon,
|
||||
} from 'lang/std/sketch'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [zoom, setZoom] = useState(0)
|
||||
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const {
|
||||
mediaStream,
|
||||
engineCommandManager,
|
||||
setIsMouseDownInStream,
|
||||
fileId,
|
||||
setFileId,
|
||||
setCmdId,
|
||||
setButtonDownInStream,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
||||
setButtonDownInStream: s.setButtonDownInStream,
|
||||
fileId: s.fileId,
|
||||
setFileId: s.setFileId,
|
||||
setCmdId: s.setCmdId,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { cameraControls },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -48,62 +69,80 @@ export const Stream = ({ className = '' }) => {
|
||||
if (!videoRef.current) return
|
||||
if (!mediaStream) return
|
||||
videoRef.current.srcObject = mediaStream
|
||||
setFileId(uuidv4())
|
||||
setZoom(videoRef.current.getBoundingClientRect().height / 2)
|
||||
}, [mediaStream, engineCommandManager, setFileId])
|
||||
}, [mediaStream, engineCommandManager])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
}) => {
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!videoRef.current) return
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: videoRef.current,
|
||||
...streamDimensions,
|
||||
})
|
||||
console.log('click', x, y)
|
||||
|
||||
const newId = uuidv4()
|
||||
setCmdId(newId)
|
||||
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
if (
|
||||
interactionGuards.pan.callback(e) ||
|
||||
interactionGuards.pan.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'pan'
|
||||
} else if (
|
||||
interactionGuards.rotate.callback(e) ||
|
||||
interactionGuards.rotate.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'rotate'
|
||||
} else if (
|
||||
interactionGuards.zoom.dragCallback(e) ||
|
||||
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'zoom'
|
||||
}
|
||||
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
file_id: fileId,
|
||||
})
|
||||
|
||||
setIsMouseDownInStream(true)
|
||||
}
|
||||
|
||||
// TODO: consolidate this with the same function in App.tsx
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager?.sendSceneCommand(message)
|
||||
}, 16)
|
||||
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||
e.preventDefault()
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction: 'zoom',
|
||||
window: { x: 0, y: zoom + e.deltaY },
|
||||
type: 'default_camera_zoom',
|
||||
magnitude: e.deltaY * 0.4,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
file_id: uuidv4(),
|
||||
})
|
||||
|
||||
setZoom(zoom + e.deltaY)
|
||||
}
|
||||
|
||||
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
||||
@ -112,6 +151,7 @@ export const Stream = ({ className = '' }) => {
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -122,7 +162,7 @@ export const Stream = ({ className = '' }) => {
|
||||
const newCmdId = uuidv4()
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
const command: Models['WebSocketRequest_type'] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_end',
|
||||
@ -130,10 +170,8 @@ export const Stream = ({ className = '' }) => {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
file_id: fileId,
|
||||
})
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(false)
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -143,10 +181,142 @@ export const Stream = ({ className = '' }) => {
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
file_id: fileId,
|
||||
})
|
||||
}
|
||||
|
||||
if (!didDragInStream && guiMode.mode === 'default') {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
(!didDragInStream &&
|
||||
guiMode.mode === 'sketch' &&
|
||||
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||
(guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('move' as any)
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
}
|
||||
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
|
||||
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
||||
if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any as 'line')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
guiMode.waitingFirstClick
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst)
|
||||
} else if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
!guiMode.waitingFirstClick
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
|
||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketchGroup = programMemory.root[variableName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||
const initialCoords = sketchGroup.value[0].from
|
||||
|
||||
const isClose = compareVec2Epsilon(initialCoords, [
|
||||
coords[1].x,
|
||||
coords[1].y,
|
||||
])
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
_modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory,
|
||||
pathToNode: guiMode.pathToNode,
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
}
|
||||
updateAst(_modifiedAst)
|
||||
}
|
||||
})
|
||||
setDidDragInStream(false)
|
||||
setClickCoords(undefined)
|
||||
}
|
||||
|
||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!clickCoords) return
|
||||
|
||||
const delta =
|
||||
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
|
||||
0.5
|
||||
|
||||
if (delta > 5 && !didDragInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -160,9 +330,11 @@ export const Stream = ({ className = '' }) => {
|
||||
onMouseUp={handleMouseUp}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
onWheelCapture={handleScroll}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
className="w-full h-full"
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
|
312
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
import ReactCodeMirror, {
|
||||
Extension,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@uiw/react-codemirror'
|
||||
import { FromServer, IntoServer } from 'editor/lsp/codec'
|
||||
import Server from '../editor/lsp/server'
|
||||
import Client from '../editor/lsp/client'
|
||||
import { TEST } from 'env'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Selections, useStore } from 'useStore'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
EditorView,
|
||||
addLineHighlight,
|
||||
lineHighlightField,
|
||||
} from 'editor/highlightextension'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
codeMirror: 'Alt-Shift-f',
|
||||
display: 'Alt + Shift + F',
|
||||
},
|
||||
convertToVariable: {
|
||||
codeMirror: 'Ctrl-Shift-c',
|
||||
display: 'Ctrl + Shift + C',
|
||||
},
|
||||
}
|
||||
|
||||
export const TextEditor = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const pathParams = useParams()
|
||||
const {
|
||||
code,
|
||||
defferedSetCode,
|
||||
editorView,
|
||||
engineCommandManager,
|
||||
formatCode,
|
||||
isLSPServerReady,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setSelectionRanges,
|
||||
sourceRangeMap,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
defferedSetCode: s.defferedSetCode,
|
||||
editorView: s.editorView,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
formatCode: s.formatCode,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
}))
|
||||
|
||||
const {
|
||||
context: { selectionRanges: machineSelectionRanges },
|
||||
send,
|
||||
} = useModelingContext()
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { textWrapping },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
// Below is the client and server promise.
|
||||
const { lspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
lspServer.start()
|
||||
setIsLSPServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsLSPServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup becuase it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isLSPServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: lspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [lspClient, isLSPServerReady])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
defferedSetCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
...machineSelectionRanges,
|
||||
codeBasedSelections,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
const extensions = [
|
||||
lineHighlightField,
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Meta-k',
|
||||
run: () => {
|
||||
setCommandBarOpen(true)
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.formatCode.codeMirror,
|
||||
run: () => {
|
||||
formatCode()
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.convertToVariable.codeMirror,
|
||||
run: () => {
|
||||
if (convertEnabled) {
|
||||
convertCallback()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]),
|
||||
] as Extension[]
|
||||
|
||||
if (kclLSP) extensions.push(kclLSP)
|
||||
|
||||
// These extensions have proven to mess with vitest
|
||||
if (!TEST) {
|
||||
extensions.push(
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
interact({
|
||||
rules: [
|
||||
// a rule for a number dragger
|
||||
{
|
||||
// the regexp matching the value
|
||||
regexp: /-?\b\d+\.?\d*\b/g,
|
||||
// set cursor to "ew-resize" on hover
|
||||
cursor: 'ew-resize',
|
||||
// change number value based on mouse X movement on drag
|
||||
onDrag: (text, setText, e) => {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey
|
||||
? 0.01
|
||||
: e.metaKey
|
||||
? 0.1
|
||||
: e.shiftKey
|
||||
? 10
|
||||
: 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
|
||||
const newVal = roundOff(
|
||||
Number(text) + delta,
|
||||
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||
)
|
||||
|
||||
if (isNaN(newVal)) return
|
||||
setText(newVal.toString())
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
|
||||
}
|
||||
|
||||
return extensions
|
||||
}, [kclLSP, textWrapping])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="full-height-subtract"
|
||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
||||
>
|
||||
<ReactCodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={editorExtensions}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -13,13 +13,13 @@
|
||||
}
|
||||
|
||||
.toggle > span {
|
||||
@apply relative rounded border border-chalkboard-110;
|
||||
@apply relative rounded border border-chalkboard-110 hover:border-chalkboard-100 cursor-pointer;
|
||||
width: calc(2 * (var(--toggle-size) + var(--padding)));
|
||||
height: calc(var(--toggle-size) + var(--padding));
|
||||
}
|
||||
|
||||
:global(.dark) .toggle > span {
|
||||
@apply border-chalkboard-40;
|
||||
@apply border-chalkboard-40 hover:border-chalkboard-30;
|
||||
}
|
||||
|
||||
.toggle > span::after {
|
||||
|
@ -1,61 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from '../../useStore'
|
||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
|
||||
import { SetVarNameModal } from '../SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export const ConvertToVariable = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
>
|
||||
ConvertToVariable
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,66 +1,103 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { User } from '../useStore'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
|
||||
it("Renders user's name and email if available", () => {
|
||||
const userWellFormed: User = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
}
|
||||
type User = Models['User_type']
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<UserSidebarMenu user={userWellFormed} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
describe('UserSidebarMenu tests', () => {
|
||||
test("Renders user's name and email if available", () => {
|
||||
const userWellFormed: User = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
}
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
render(
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userWellFormed} />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('username')).toHaveTextContent(
|
||||
userWellFormed.name || ''
|
||||
)
|
||||
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email)
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
|
||||
expect(screen.getByTestId('username')).toHaveTextContent(
|
||||
userWellFormed.name || ''
|
||||
)
|
||||
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email)
|
||||
})
|
||||
|
||||
test("Renders just the user's email if no name is available", () => {
|
||||
const userNoName: User = {
|
||||
id: '8675309',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: '',
|
||||
}
|
||||
|
||||
render(
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userNoName} />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
|
||||
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
|
||||
})
|
||||
|
||||
test('Renders a menu button if no user avatar is available', () => {
|
||||
const userNoAvatar: User = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
company: 'Test Company',
|
||||
discord: 'Test User#1234',
|
||||
github: 'testuser',
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
image: '',
|
||||
}
|
||||
|
||||
render(
|
||||
<TestWrap>
|
||||
<UserSidebarMenu user={userNoAvatar} />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
|
||||
})
|
||||
})
|
||||
|
||||
it("Renders just the user's email if no name is available", () => {
|
||||
const userNoName: User = {
|
||||
id: '8675309',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
image: 'https://placekitten.com/200/200',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
}
|
||||
|
||||
render(
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<UserSidebarMenu user={userNoName} />
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
|
||||
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
|
||||
})
|
||||
|
||||
it('Renders a menu button if no user avatar is available', () => {
|
||||
const userNoAvatar: User = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
created_at: 'yesteryear',
|
||||
updated_at: 'today',
|
||||
}
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<UserSidebarMenu user={userNoAvatar} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
|
||||
})
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { User, useStore } from '../useStore'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { paths } from '../Router'
|
||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const { setToken } = useStore((s) => ({
|
||||
setToken: s.setToken,
|
||||
}))
|
||||
const {
|
||||
auth: { send },
|
||||
} = useGlobalStateContext()
|
||||
|
||||
// Fallback logic for displaying user's "name":
|
||||
// 1. user.name
|
||||
@ -33,10 +38,10 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<Popover className="relative">
|
||||
{user?.image && !imageLoadFailed ? (
|
||||
<Popover.Button
|
||||
className="border-0 rounded-full w-fit p-0"
|
||||
className="border-0 rounded-full w-fit p-0 focus:outline-none group"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 overflow-hidden">
|
||||
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 group-focus:border-liquid-50 overflow-hidden">
|
||||
<img
|
||||
src={user?.image || ''}
|
||||
alt={user?.name || ''}
|
||||
@ -56,76 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
Menu
|
||||
</ActionButton>
|
||||
)}
|
||||
<Popover.Overlay className="fixed z-40 inset-0 bg-chalkboard-110/50" />
|
||||
<Transition
|
||||
enter="duration-200 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-50 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg">
|
||||
{({ close }) => (
|
||||
<>
|
||||
{user && (
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || ''}
|
||||
className="h-8 w-8"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-liquid-10 text-mono"
|
||||
data-testid="username"
|
||||
>
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-liquid-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
</p>
|
||||
<Transition
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
|
||||
{({ close }) => (
|
||||
<>
|
||||
{user && (
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || ''}
|
||||
className="h-8 w-8"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-liquid-10 text-mono"
|
||||
data-testid="username"
|
||||
>
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-liquid-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faGear }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||
icon={{ icon: faGithub }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
>
|
||||
Request a feature
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send('Log out')}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
||||
>
|
||||
Sign out
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
icon={{ icon: faGear }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
navigate(paths.SETTINGS)
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
setToken('')
|
||||
navigate(paths.SIGN_IN)
|
||||
}}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
||||
>
|
||||
Sign out
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
185
src/editor/lsp/client.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
import {
|
||||
registerServerCapability,
|
||||
unregisterServerCapability,
|
||||
} from './server-capability-registration'
|
||||
import { Codec, FromServer, IntoServer } from './codec'
|
||||
|
||||
const client_capabilities: LSP.ClientCapabilities = {
|
||||
textDocument: {
|
||||
hover: {
|
||||
dynamicRegistration: true,
|
||||
contentFormat: ['plaintext', 'markdown'],
|
||||
},
|
||||
moniker: {},
|
||||
synchronization: {
|
||||
dynamicRegistration: true,
|
||||
willSave: false,
|
||||
didSave: false,
|
||||
willSaveWaitUntil: false,
|
||||
},
|
||||
completion: {
|
||||
dynamicRegistration: true,
|
||||
completionItem: {
|
||||
snippetSupport: false,
|
||||
commitCharactersSupport: true,
|
||||
documentationFormat: ['plaintext', 'markdown'],
|
||||
deprecatedSupport: false,
|
||||
preselectSupport: false,
|
||||
},
|
||||
contextSupport: false,
|
||||
},
|
||||
signatureHelp: {
|
||||
dynamicRegistration: true,
|
||||
signatureInformation: {
|
||||
documentationFormat: ['plaintext', 'markdown'],
|
||||
},
|
||||
},
|
||||
declaration: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
definition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
typeDefinition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
implementation: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
didChangeConfiguration: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
afterInitializedHooks: (() => Promise<void>)[] = []
|
||||
#fromServer: FromServer
|
||||
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
||||
|
||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
||||
super(
|
||||
new jsrpc.JSONRPCServer(),
|
||||
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
||||
const encoded = Codec.encode(json)
|
||||
intoServer.enqueue(encoded)
|
||||
if (null != json.id) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const response = await fromServer.responses.get(json.id)!
|
||||
this.client.receive(response as jsrpc.JSONRPCResponse)
|
||||
}
|
||||
})
|
||||
)
|
||||
this.#fromServer = fromServer
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// process "window/logMessage": client <- server
|
||||
this.addMethod(LSP.LogMessageNotification.type.method, (params) => {
|
||||
const { type, message } = params as {
|
||||
type: LSP.MessageType
|
||||
message: string
|
||||
}
|
||||
let messageString = ''
|
||||
switch (type) {
|
||||
case LSP.MessageType.Error: {
|
||||
messageString += '[error] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Warning: {
|
||||
messageString += ' [warn] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Info: {
|
||||
messageString += ' [info] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Log: {
|
||||
messageString += ' [log] '
|
||||
break
|
||||
}
|
||||
}
|
||||
messageString += message
|
||||
// console.log(messageString)
|
||||
return
|
||||
})
|
||||
|
||||
// process "client/registerCapability": client <- server
|
||||
this.addMethod(LSP.RegistrationRequest.type.method, (params) => {
|
||||
// Register a server capability.
|
||||
params.registrations.forEach(
|
||||
(capabilityRegistration: LSP.Registration) => {
|
||||
this.serverCapabilities = registerServerCapability(
|
||||
this.serverCapabilities,
|
||||
capabilityRegistration
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// process "client/unregisterCapability": client <- server
|
||||
this.addMethod(LSP.UnregistrationRequest.type.method, (params) => {
|
||||
// Unregister a server capability.
|
||||
params.unregisterations.forEach(
|
||||
(capabilityUnregistration: LSP.Unregistration) => {
|
||||
this.serverCapabilities = unregisterServerCapability(
|
||||
this.serverCapabilities,
|
||||
capabilityUnregistration
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// request "initialize": client <-> server
|
||||
const { capabilities } = await this.request(
|
||||
LSP.InitializeRequest.type.method,
|
||||
{
|
||||
processId: null,
|
||||
clientInfo: {
|
||||
name: 'kcl-language-client',
|
||||
},
|
||||
capabilities: client_capabilities,
|
||||
rootUri: null,
|
||||
} as LSP.InitializeParams
|
||||
)
|
||||
|
||||
this.serverCapabilities = capabilities
|
||||
|
||||
// notify "initialized": client --> server
|
||||
this.notify(LSP.InitializedNotification.type.method, {})
|
||||
|
||||
await Promise.all(
|
||||
this.afterInitializedHooks.map((f: () => Promise<void>) => f())
|
||||
)
|
||||
await Promise.all([this.processNotifications(), this.processRequests()])
|
||||
}
|
||||
|
||||
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
||||
return this.serverCapabilities
|
||||
}
|
||||
|
||||
async processNotifications(): Promise<void> {
|
||||
for await (const notification of this.#fromServer.notifications) {
|
||||
await this.receiveAndSend(notification)
|
||||
}
|
||||
}
|
||||
|
||||
async processRequests(): Promise<void> {
|
||||
for await (const request of this.#fromServer.requests) {
|
||||
await this.receiveAndSend(request)
|
||||
}
|
||||
}
|
||||
|
||||
pushAfterInitializeHook(...hooks: (() => Promise<void>)[]): void {
|
||||
this.afterInitializedHooks.push(...hooks)
|
||||
}
|
||||
}
|
53
src/editor/lsp/codec.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import * as vsrpc from 'vscode-jsonrpc'
|
||||
|
||||
import Bytes from './codec/bytes'
|
||||
import StreamDemuxer from './codec/demuxer'
|
||||
import Headers from './codec/headers'
|
||||
import Queue from './codec/queue'
|
||||
import Tracer from './tracer'
|
||||
|
||||
export const encoder = new TextEncoder()
|
||||
export const decoder = new TextDecoder()
|
||||
|
||||
export class Codec {
|
||||
static encode(
|
||||
json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse
|
||||
): Uint8Array {
|
||||
const message = JSON.stringify(json)
|
||||
const delimited = Headers.add(message)
|
||||
return Bytes.encode(delimited)
|
||||
}
|
||||
|
||||
static decode<T>(data: Uint8Array): T {
|
||||
const delimited = Bytes.decode(data)
|
||||
const message = Headers.remove(delimited)
|
||||
return JSON.parse(message) as T
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: tracing effiency
|
||||
export class IntoServer
|
||||
extends Queue<Uint8Array>
|
||||
implements AsyncGenerator<Uint8Array, never, void>
|
||||
{
|
||||
enqueue(item: Uint8Array): void {
|
||||
Tracer.client(Headers.remove(decoder.decode(item)))
|
||||
super.enqueue(item)
|
||||
}
|
||||
}
|
||||
|
||||
export interface FromServer extends WritableStream<Uint8Array> {
|
||||
readonly responses: {
|
||||
get(key: number | string): null | Promise<vsrpc.ResponseMessage>
|
||||
}
|
||||
readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void>
|
||||
readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FromServer {
|
||||
export function create(): FromServer {
|
||||
return new StreamDemuxer()
|
||||
}
|
||||
}
|
27
src/editor/lsp/codec/bytes.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { encoder, decoder } from '../codec'
|
||||
|
||||
export default class Bytes {
|
||||
static encode(input: string): Uint8Array {
|
||||
return encoder.encode(input)
|
||||
}
|
||||
|
||||
static decode(input: Uint8Array): string {
|
||||
return decoder.decode(input)
|
||||
}
|
||||
|
||||
static append<
|
||||
T extends { length: number; set(arr: T, offset: number): void }
|
||||
>(constructor: { new (length: number): T }, ...arrays: T[]) {
|
||||
let totalLength = 0
|
||||
for (const arr of arrays) {
|
||||
totalLength += arr.length
|
||||
}
|
||||
const result = new constructor(totalLength)
|
||||
let offset = 0
|
||||
for (const arr of arrays) {
|
||||
result.set(arr, offset)
|
||||
offset += arr.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
82
src/editor/lsp/codec/demuxer.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as vsrpc from 'vscode-jsonrpc'
|
||||
|
||||
import Bytes from './bytes'
|
||||
import PromiseMap from './map'
|
||||
import Queue from './queue'
|
||||
import Tracer from '../tracer'
|
||||
|
||||
export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
||||
new PromiseMap()
|
||||
readonly notifications: Queue<vsrpc.NotificationMessage> =
|
||||
new Queue<vsrpc.NotificationMessage>()
|
||||
readonly requests: Queue<vsrpc.RequestMessage> =
|
||||
new Queue<vsrpc.RequestMessage>()
|
||||
|
||||
readonly #start: Promise<void>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.#start = this.start()
|
||||
}
|
||||
|
||||
private async start(): Promise<void> {
|
||||
let contentLength: null | number = null
|
||||
let buffer = new Uint8Array()
|
||||
|
||||
for await (const bytes of this) {
|
||||
buffer = Bytes.append(Uint8Array, buffer, bytes)
|
||||
while (buffer.length > 0) {
|
||||
// check if the content length is known
|
||||
if (null == contentLength) {
|
||||
// if not, try to match the prefixed headers
|
||||
const match = Bytes.decode(buffer).match(
|
||||
/^Content-Length:\s*(\d+)\s*/
|
||||
)
|
||||
if (null == match) continue
|
||||
|
||||
// try to parse the content-length from the headers
|
||||
const length = parseInt(match[1])
|
||||
if (isNaN(length)) throw new Error('invalid content length')
|
||||
|
||||
// slice the headers since we now have the content length
|
||||
buffer = buffer.slice(match[0].length)
|
||||
|
||||
// set the content length
|
||||
contentLength = length
|
||||
}
|
||||
|
||||
// if the buffer doesn't contain a full message; await another iteration
|
||||
if (buffer.length < contentLength) continue
|
||||
|
||||
// Get just the slice of the buffer that is our content length.
|
||||
const slice = buffer.slice(0, contentLength)
|
||||
|
||||
// decode buffer to a string
|
||||
const delimited = Bytes.decode(slice)
|
||||
|
||||
// reset the buffer
|
||||
buffer = buffer.slice(contentLength)
|
||||
// reset the contentLength
|
||||
contentLength = null
|
||||
|
||||
const message = JSON.parse(delimited) as vsrpc.Message
|
||||
Tracer.server(message)
|
||||
|
||||
// demux the message stream
|
||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
||||
this.responses.set(message.id, message)
|
||||
continue
|
||||
}
|
||||
if (vsrpc.Message.isNotification(message)) {
|
||||
this.notifications.enqueue(message)
|
||||
continue
|
||||
}
|
||||
if (vsrpc.Message.isRequest(message)) {
|
||||
this.requests.enqueue(message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/editor/lsp/codec/headers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default class Headers {
|
||||
static add(message: string): string {
|
||||
return `Content-Length: ${message.length}\r\n\r\n${message}`
|
||||
}
|
||||
|
||||
static remove(delimited: string): string {
|
||||
return delimited.replace(/^Content-Length:\s*\d+\s*/, '')
|
||||
}
|
||||
}
|
72
src/editor/lsp/codec/map.ts
Normal file
@ -0,0 +1,72 @@
|
||||
export default class PromiseMap<K, V extends { toString(): string }> {
|
||||
#map: Map<K, PromiseMap.Entry<V>> = new Map()
|
||||
|
||||
get(key: K & { toString(): string }): null | Promise<V> {
|
||||
let initialized: PromiseMap.Entry<V>
|
||||
// if the entry doesn't exist, set it
|
||||
if (!this.#map.has(key)) {
|
||||
initialized = this.#set(key)
|
||||
} else {
|
||||
// otherwise return the entry
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
initialized = this.#map.get(key)!
|
||||
}
|
||||
// if the entry is a pending promise, return it
|
||||
if (initialized.status === 'pending') {
|
||||
return initialized.promise
|
||||
} else {
|
||||
// otherwise return null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
#set(key: K, value?: V): PromiseMap.Entry<V> {
|
||||
if (this.#map.has(key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return this.#map.get(key)!
|
||||
}
|
||||
// placeholder resolver for entry
|
||||
let resolve = (item: V) => {
|
||||
void item
|
||||
}
|
||||
// promise for entry (which assigns the resolver
|
||||
const promise = new Promise<V>((resolver) => {
|
||||
resolve = resolver
|
||||
})
|
||||
// the initialized entry
|
||||
const initialized: PromiseMap.Entry<V> = {
|
||||
status: 'pending',
|
||||
resolve,
|
||||
promise,
|
||||
}
|
||||
if (null != value) {
|
||||
initialized.resolve(value)
|
||||
}
|
||||
// set the entry
|
||||
this.#map.set(key, initialized)
|
||||
return initialized
|
||||
}
|
||||
|
||||
set(key: K & { toString(): string }, value: V): this {
|
||||
const initialized = this.#set(key, value)
|
||||
// if the promise is pending ...
|
||||
if (initialized.status === 'pending') {
|
||||
// ... set the entry status to resolved to free the promise
|
||||
this.#map.set(key, { status: 'resolved' })
|
||||
// ... and resolve the promise with the given value
|
||||
initialized.resolve(value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#map.size
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PromiseMap {
|
||||
export type Entry<V> =
|
||||
| { status: 'pending'; resolve: (item: V) => void; promise: Promise<V> }
|
||||
| { status: 'resolved' }
|
||||
}
|
113
src/editor/lsp/codec/queue.ts
Normal file
@ -0,0 +1,113 @@
|
||||
export default class Queue<T>
|
||||
implements WritableStream<T>, AsyncGenerator<T, never, void>
|
||||
{
|
||||
readonly #promises: Promise<T>[] = []
|
||||
readonly #resolvers: ((item: T) => void)[] = []
|
||||
readonly #observers: ((item: T) => void)[] = []
|
||||
|
||||
#closed = false
|
||||
#locked = false
|
||||
readonly #stream: WritableStream<T>
|
||||
|
||||
static #__add<X>(
|
||||
promises: Promise<X>[],
|
||||
resolvers: ((item: X) => void)[]
|
||||
): void {
|
||||
promises.push(
|
||||
new Promise((resolve) => {
|
||||
resolvers.push(resolve)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static #__enqueue<X>(
|
||||
closed: boolean,
|
||||
promises: Promise<X>[],
|
||||
resolvers: ((item: X) => void)[],
|
||||
item: X
|
||||
): void {
|
||||
if (!closed) {
|
||||
if (!resolvers.length) Queue.#__add(promises, resolvers)
|
||||
const resolve = resolvers.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
resolve(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const closed = this.#closed
|
||||
const promises = this.#promises
|
||||
const resolvers = this.#resolvers
|
||||
this.#stream = new WritableStream({
|
||||
write(item: T): void {
|
||||
Queue.#__enqueue(closed, promises, resolvers, item)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#add(): void {
|
||||
return Queue.#__add(this.#promises, this.#resolvers)
|
||||
}
|
||||
|
||||
enqueue(item: T): void {
|
||||
return Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item)
|
||||
}
|
||||
|
||||
dequeue(): Promise<T> {
|
||||
if (!this.#promises.length) this.#add()
|
||||
const item = this.#promises.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
return item
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return !this.#promises.length
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
return !!this.#resolvers.length
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.#promises.length - this.#resolvers.length
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T, never>> {
|
||||
const done = false
|
||||
const value = await this.dequeue()
|
||||
for (const observer of this.#observers) {
|
||||
observer(value)
|
||||
}
|
||||
return { done, value }
|
||||
}
|
||||
|
||||
return(): Promise<IteratorResult<T, never>> {
|
||||
return new Promise(() => {
|
||||
// empty
|
||||
})
|
||||
}
|
||||
|
||||
throw(err: Error): Promise<IteratorResult<T, never>> {
|
||||
return new Promise((_resolve, reject) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T, never, void> {
|
||||
return this
|
||||
}
|
||||
|
||||
get locked(): boolean {
|
||||
return this.#stream.locked
|
||||
}
|
||||
|
||||
abort(reason?: Error): Promise<void> {
|
||||
return this.#stream.abort(reason)
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.#stream.close()
|
||||
}
|
||||
|
||||
getWriter(): WritableStreamDefaultWriter<T> {
|
||||
return this.#stream.getWriter()
|
||||
}
|
||||
}
|
151
src/editor/lsp/index.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import Client from './client'
|
||||
import { LanguageServerPlugin } from './plugin'
|
||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
|
||||
// Client to server then server to client
|
||||
interface LSPRequestMap {
|
||||
initialize: [LSP.InitializeParams, LSP.InitializeResult]
|
||||
'textDocument/hover': [LSP.HoverParams, LSP.Hover]
|
||||
'textDocument/completion': [
|
||||
LSP.CompletionParams,
|
||||
LSP.CompletionItem[] | LSP.CompletionList | null
|
||||
]
|
||||
'textDocument/semanticTokens/full': [
|
||||
LSP.SemanticTokensParams,
|
||||
LSP.SemanticTokens
|
||||
]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
interface LSPNotifyMap {
|
||||
initialized: LSP.InitializedParams
|
||||
'textDocument/didChange': LSP.DidChangeTextDocumentParams
|
||||
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
|
||||
}
|
||||
|
||||
// Server to client
|
||||
interface LSPEventMap {
|
||||
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
|
||||
}
|
||||
|
||||
export type Notification = {
|
||||
[key in keyof LSPEventMap]: {
|
||||
jsonrpc: '2.0'
|
||||
id?: null | undefined
|
||||
method: key
|
||||
params: LSPEventMap[key]
|
||||
}
|
||||
}[keyof LSPEventMap]
|
||||
|
||||
export interface LanguageServerClientOptions {
|
||||
client: Client
|
||||
}
|
||||
|
||||
export class LanguageServerClient {
|
||||
private client: Client
|
||||
|
||||
public ready: boolean
|
||||
|
||||
private plugins: LanguageServerPlugin[]
|
||||
|
||||
public initializePromise: Promise<void>
|
||||
|
||||
private isUpdatingSemanticTokens: boolean = false
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
|
||||
constructor(options: LanguageServerClientOptions) {
|
||||
this.plugins = []
|
||||
this.client = options.client
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.initializePromise = this.initialize()
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Start the client in the background.
|
||||
this.client.start()
|
||||
|
||||
this.ready = true
|
||||
}
|
||||
|
||||
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
||||
return this.client.getServerCapabilities()
|
||||
}
|
||||
|
||||
close() {}
|
||||
|
||||
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
||||
this.notify('textDocument/didOpen', params)
|
||||
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
|
||||
this.notify('textDocument/didChange', params)
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
async updateSemanticTokens(uri: string) {
|
||||
// Make sure we can only run, if we aren't already running.
|
||||
if (!this.isUpdatingSemanticTokens) {
|
||||
this.isUpdatingSemanticTokens = true
|
||||
|
||||
const result = await this.request('textDocument/semanticTokens/full', {
|
||||
textDocument: {
|
||||
uri,
|
||||
},
|
||||
})
|
||||
|
||||
this.semanticTokens = deserializeTokens(
|
||||
result.data,
|
||||
this.getServerCapabilities().semanticTokensProvider
|
||||
)
|
||||
|
||||
this.isUpdatingSemanticTokens = false
|
||||
}
|
||||
}
|
||||
|
||||
getSemanticTokens(): SemanticToken[] {
|
||||
return this.semanticTokens
|
||||
}
|
||||
|
||||
async textDocumentHover(params: LSP.HoverParams) {
|
||||
return await this.request('textDocument/hover', params)
|
||||
}
|
||||
|
||||
async textDocumentCompletion(params: LSP.CompletionParams) {
|
||||
return await this.request('textDocument/completion', params)
|
||||
}
|
||||
|
||||
attachPlugin(plugin: LanguageServerPlugin) {
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
|
||||
detachPlugin(plugin: LanguageServerPlugin) {
|
||||
const i = this.plugins.indexOf(plugin)
|
||||
if (i === -1) return
|
||||
this.plugins.splice(i, 1)
|
||||
}
|
||||
|
||||
private request<K extends keyof LSPRequestMap>(
|
||||
method: K,
|
||||
params: LSPRequestMap[K][0]
|
||||
): Promise<LSPRequestMap[K][1]> {
|
||||
return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
|
||||
}
|
||||
|
||||
private notify<K extends keyof LSPNotifyMap>(
|
||||
method: K,
|
||||
params: LSPNotifyMap[K]
|
||||
): void {
|
||||
return this.client.notify(method, params)
|
||||
}
|
||||
|
||||
private processNotification(notification: Notification) {
|
||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||
}
|
||||
}
|
36
src/editor/lsp/language.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Code mirror language implementation for kcl.
|
||||
|
||||
import {
|
||||
Language,
|
||||
defineLanguageFacet,
|
||||
LanguageSupport,
|
||||
} from '@codemirror/language'
|
||||
import { LanguageServerClient } from '.'
|
||||
import { kclPlugin } from './plugin'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { parser as jsParser } from '@lezer/javascript'
|
||||
|
||||
const data = defineLanguageFacet({})
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
||||
documentUri: string
|
||||
client: LanguageServerClient
|
||||
}
|
||||
|
||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
const lang = new Language(data, jsParser, [], 'kcl')
|
||||
|
||||
// Create our supporting extension.
|
||||
const kclLsp = kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
})
|
||||
|
||||
return new LanguageSupport(lang, [kclLsp])
|
||||
}
|
168
src/editor/lsp/parser.ts
Normal file
@ -0,0 +1,168 @@
|
||||
// Extends the codemirror Parser for kcl.
|
||||
|
||||
import {
|
||||
Parser,
|
||||
Input,
|
||||
TreeFragment,
|
||||
PartialParse,
|
||||
Tree,
|
||||
NodeType,
|
||||
NodeSet,
|
||||
} from '@lezer/common'
|
||||
import { LanguageServerClient } from '.'
|
||||
import { posToOffset } from './plugin'
|
||||
import { SemanticToken } from './semantic_tokens'
|
||||
import { DocInput } from '@codemirror/language'
|
||||
import { tags, styleTags } from '@lezer/highlight'
|
||||
|
||||
export default class KclParser extends Parser {
|
||||
private client: LanguageServerClient
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
createParse(
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[]
|
||||
): PartialParse {
|
||||
let parse: PartialParse = new Context(this, input, fragments, ranges)
|
||||
return parse
|
||||
}
|
||||
|
||||
getTokenTypes(): string[] {
|
||||
return this.client.getServerCapabilities().semanticTokensProvider!.legend
|
||||
.tokenTypes
|
||||
}
|
||||
|
||||
getSemanticTokens(): SemanticToken[] {
|
||||
return this.client.getSemanticTokens()
|
||||
}
|
||||
}
|
||||
|
||||
class Context implements PartialParse {
|
||||
private parser: KclParser
|
||||
private input: DocInput
|
||||
private fragments: readonly TreeFragment[]
|
||||
private ranges: readonly { from: number; to: number }[]
|
||||
|
||||
private nodeTypes: { [key: string]: NodeType }
|
||||
stoppedAt: number = 0
|
||||
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
private currentLine: number = 0
|
||||
private currentColumn: number = 0
|
||||
private nodeSet: NodeSet
|
||||
|
||||
constructor(
|
||||
/// The parser configuration used.
|
||||
parser: KclParser,
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[]
|
||||
) {
|
||||
this.parser = parser
|
||||
this.input = input as DocInput
|
||||
this.fragments = fragments
|
||||
this.ranges = ranges
|
||||
|
||||
// Iterate over the semantic token types and create a node type for each.
|
||||
this.nodeTypes = {}
|
||||
let nodeArray: NodeType[] = []
|
||||
this.parser.getTokenTypes().forEach((tokenType, index) => {
|
||||
const nodeType = NodeType.define({
|
||||
id: index,
|
||||
name: tokenType,
|
||||
// props: [this.styleTags],
|
||||
})
|
||||
this.nodeTypes[tokenType] = nodeType
|
||||
nodeArray.push(nodeType)
|
||||
})
|
||||
|
||||
this.semanticTokens = this.parser.getSemanticTokens()
|
||||
const styles = styleTags({
|
||||
number: tags.number,
|
||||
variable: tags.variableName,
|
||||
operator: tags.operator,
|
||||
keyword: tags.keyword,
|
||||
string: tags.string,
|
||||
comment: tags.comment,
|
||||
function: tags.function(tags.variableName),
|
||||
})
|
||||
this.nodeSet = new NodeSet(nodeArray).extend(styles)
|
||||
}
|
||||
|
||||
get parsedPos(): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
advance(): Tree | null {
|
||||
if (this.semanticTokens.length === 0) {
|
||||
return new Tree(NodeType.none, [], [], 0)
|
||||
}
|
||||
const tree = this.createTree(this.semanticTokens[0], 0)
|
||||
this.stoppedAt = this.input.doc.length
|
||||
return tree
|
||||
}
|
||||
|
||||
createTree(token: SemanticToken, index: number): Tree {
|
||||
const changedLine = token.delta_line !== 0
|
||||
this.currentLine += token.delta_line
|
||||
if (changedLine) {
|
||||
this.currentColumn = 0
|
||||
}
|
||||
this.currentColumn += token.delta_start
|
||||
|
||||
// Let's get our position relative to the start of the file.
|
||||
let currentPosition = posToOffset(this.input.doc, {
|
||||
line: this.currentLine,
|
||||
character: this.currentColumn,
|
||||
})
|
||||
|
||||
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
|
||||
|
||||
if (currentPosition === undefined) {
|
||||
// This is bad and weird.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
if (index >= this.semanticTokens.length - 1) {
|
||||
// We have no children.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
const nextIndex = index + 1
|
||||
const nextToken = this.semanticTokens[nextIndex]
|
||||
const changedLineNext = nextToken.delta_line !== 0
|
||||
const nextLine = this.currentLine + nextToken.delta_line
|
||||
const nextColumn = changedLineNext
|
||||
? nextToken.delta_start
|
||||
: this.currentColumn + nextToken.delta_start
|
||||
const nextPosition = posToOffset(this.input.doc, {
|
||||
line: nextLine,
|
||||
character: nextColumn,
|
||||
})
|
||||
|
||||
if (nextPosition === undefined) {
|
||||
// This is bad and weird.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
// Let's get the
|
||||
|
||||
return new Tree(
|
||||
nodeType,
|
||||
[this.createTree(nextToken, nextIndex)],
|
||||
|
||||
// The positions (offsets relative to the start of this tree) of the children.
|
||||
[nextPosition - currentPosition],
|
||||
token.length
|
||||
)
|
||||
}
|
||||
|
||||
stopAt(pos: number) {
|
||||
this.stoppedAt = pos
|
||||
}
|
||||
}
|
360
src/editor/lsp/plugin.ts
Normal file
@ -0,0 +1,360 @@
|
||||
import { autocompletion, completeFromList } from '@codemirror/autocomplete'
|
||||
import { setDiagnostics } from '@codemirror/lint'
|
||||
import { Facet } from '@codemirror/state'
|
||||
import {
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
Tooltip,
|
||||
hoverTooltip,
|
||||
tooltips,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
DiagnosticSeverity,
|
||||
CompletionItemKind,
|
||||
CompletionTriggerKind,
|
||||
} from 'vscode-languageserver-protocol'
|
||||
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
} from '@codemirror/autocomplete'
|
||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
||||
import type { Text } from '@codemirror/state'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { LanguageServerClient, Notification } from '.'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
const changesDelay = 500
|
||||
|
||||
const CompletionItemKindMap = Object.fromEntries(
|
||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||
) as Record<CompletionItemKind, string>
|
||||
|
||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||
const documentUri = Facet.define<string, string>({ combine: useLast })
|
||||
const languageId = Facet.define<string, string>({ combine: useLast })
|
||||
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
|
||||
combine: useLast,
|
||||
})
|
||||
|
||||
export interface LanguageServerOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
||||
documentUri: string
|
||||
allowHTMLContent: boolean
|
||||
client: LanguageServerClient
|
||||
}
|
||||
|
||||
export class LanguageServerPlugin implements PluginValue {
|
||||
public client: LanguageServerClient
|
||||
|
||||
private documentUri: string
|
||||
private languageId: string
|
||||
private documentVersion: number
|
||||
|
||||
private changesTimeout: number
|
||||
|
||||
constructor(private view: EditorView, private allowHTMLContent: boolean) {
|
||||
this.client = this.view.state.facet(client)
|
||||
this.documentUri = this.view.state.facet(documentUri)
|
||||
this.languageId = this.view.state.facet(languageId)
|
||||
this.documentVersion = 0
|
||||
this.changesTimeout = 0
|
||||
|
||||
this.client.attachPlugin(this)
|
||||
|
||||
this.initialize({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
update({ docChanged }: ViewUpdate) {
|
||||
if (!docChanged) return
|
||||
if (this.changesTimeout) clearTimeout(this.changesTimeout)
|
||||
this.changesTimeout = window.setTimeout(() => {
|
||||
this.sendChange({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
}, changesDelay)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.client.detachPlugin(this)
|
||||
}
|
||||
|
||||
async initialize({ documentText }: { documentText: string }) {
|
||||
if (this.client.initializePromise) {
|
||||
await this.client.initializePromise
|
||||
}
|
||||
this.client.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
languageId: this.languageId,
|
||||
text: documentText,
|
||||
version: this.documentVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async sendChange({ documentText }: { documentText: string }) {
|
||||
if (!this.client.ready) return
|
||||
try {
|
||||
await this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
contentChanges: [{ text: documentText }],
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
requestDiagnostics(view: EditorView) {
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
}
|
||||
|
||||
async requestHoverTooltip(
|
||||
view: EditorView,
|
||||
{ line, character }: { line: number; character: number }
|
||||
): Promise<Tooltip | null> {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().hoverProvider
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
const result = await this.client.textDocumentHover({
|
||||
textDocument: { uri: this.documentUri },
|
||||
position: { line, character },
|
||||
})
|
||||
if (!result) return null
|
||||
const { contents, range } = result
|
||||
let pos = posToOffset(view.state.doc, { line, character })!
|
||||
let end: number | undefined
|
||||
if (range) {
|
||||
pos = posToOffset(view.state.doc, range.start)!
|
||||
end = posToOffset(view.state.doc, range.end)
|
||||
}
|
||||
if (pos === null) return null
|
||||
const dom = document.createElement('div')
|
||||
dom.classList.add('documentation')
|
||||
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
||||
else dom.textContent = formatContents(contents)
|
||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||
}
|
||||
|
||||
async requestCompletion(
|
||||
context: CompletionContext,
|
||||
{ line, character }: { line: number; character: number },
|
||||
{
|
||||
triggerKind,
|
||||
triggerCharacter,
|
||||
}: {
|
||||
triggerKind: CompletionTriggerKind
|
||||
triggerCharacter: string | undefined
|
||||
}
|
||||
): Promise<CompletionResult | null> {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().completionProvider
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({
|
||||
documentText: context.state.doc.toString(),
|
||||
})
|
||||
|
||||
const result = await this.client.textDocumentCompletion({
|
||||
textDocument: { uri: this.documentUri },
|
||||
position: { line, character },
|
||||
context: {
|
||||
triggerKind,
|
||||
triggerCharacter,
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const items = 'items' in result ? result.items : result
|
||||
|
||||
let options = items.map(
|
||||
({
|
||||
detail,
|
||||
label,
|
||||
labelDetails,
|
||||
kind,
|
||||
textEdit,
|
||||
documentation,
|
||||
deprecated,
|
||||
insertText,
|
||||
insertTextFormat,
|
||||
sortText,
|
||||
filterText,
|
||||
}) => {
|
||||
const completion: Completion & {
|
||||
filterText: string
|
||||
sortText?: string
|
||||
apply: string
|
||||
} = {
|
||||
label,
|
||||
detail: labelDetails ? labelDetails.detail : detail,
|
||||
apply: label,
|
||||
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
||||
sortText: sortText ?? label,
|
||||
filterText: filterText ?? label,
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = () => {
|
||||
const htmlString = formatContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
return { dom: htmlNode }
|
||||
}
|
||||
}
|
||||
|
||||
return completion
|
||||
}
|
||||
)
|
||||
|
||||
return completeFromList(options)(context)
|
||||
}
|
||||
|
||||
processNotification(notification: Notification) {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
this.processDiagnostics(notification.params)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||
if (params.uri !== this.documentUri) return
|
||||
|
||||
const diagnostics = params.diagnostics
|
||||
.map(({ range, message, severity }) => ({
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
severity: (
|
||||
{
|
||||
[DiagnosticSeverity.Error]: 'error',
|
||||
[DiagnosticSeverity.Warning]: 'warning',
|
||||
[DiagnosticSeverity.Information]: 'info',
|
||||
[DiagnosticSeverity.Hint]: 'info',
|
||||
} as const
|
||||
)[severity!],
|
||||
message,
|
||||
}))
|
||||
.filter(
|
||||
({ from, to }) =>
|
||||
from !== null && to !== null && from !== undefined && to !== undefined
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.from < b.from:
|
||||
return -1
|
||||
case a.from > b.from:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
export function kclPlugin(options: LanguageServerOptions) {
|
||||
let plugin: LanguageServerPlugin | null = null
|
||||
|
||||
return [
|
||||
client.of(options.client),
|
||||
documentUri.of(options.documentUri),
|
||||
languageId.of('kcl'),
|
||||
ViewPlugin.define(
|
||||
(view) =>
|
||||
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
||||
),
|
||||
hoverTooltip(
|
||||
(view, pos) =>
|
||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||
null
|
||||
),
|
||||
tooltips({
|
||||
position: 'absolute',
|
||||
}),
|
||||
autocompletion({
|
||||
override: [
|
||||
async (context) => {
|
||||
if (plugin == null) return null
|
||||
|
||||
const { state, pos, explicit } = context
|
||||
const line = state.doc.lineAt(pos)
|
||||
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||
let trigChar: string | undefined
|
||||
if (
|
||||
!explicit &&
|
||||
plugin.client
|
||||
.getServerCapabilities()
|
||||
.completionProvider?.triggerCharacters?.includes(
|
||||
line.text[pos - line.from - 1]
|
||||
)
|
||||
) {
|
||||
trigKind = CompletionTriggerKind.TriggerCharacter
|
||||
trigChar = line.text[pos - line.from - 1]
|
||||
}
|
||||
if (
|
||||
trigKind === CompletionTriggerKind.Invoked &&
|
||||
!context.matchBefore(/\w+$/)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return await plugin.requestCompletion(
|
||||
context,
|
||||
offsetToPos(state.doc, pos),
|
||||
{
|
||||
triggerKind: trigKind,
|
||||
triggerCharacter: trigChar,
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
export function posToOffset(
|
||||
doc: Text,
|
||||
pos: { line: number; character: number }
|
||||
): number | undefined {
|
||||
if (pos.line >= doc.lines) return
|
||||
const offset = doc.line(pos.line + 1).from + pos.character
|
||||
if (offset > doc.length) return
|
||||
return offset
|
||||
}
|
||||
|
||||
function offsetToPos(doc: Text, offset: number) {
|
||||
const line = doc.lineAt(offset)
|
||||
return {
|
||||
line: line.number - 1,
|
||||
character: offset - line.from,
|
||||
}
|
||||
}
|
||||
|
||||
function formatContents(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
if (Array.isArray(contents)) {
|
||||
return contents.map((c) => formatContents(c) + '\n\n').join('')
|
||||
} else if (typeof contents === 'string') {
|
||||
return Marked.parse(contents)
|
||||
} else {
|
||||
return Marked.parse(contents.value)
|
||||
}
|
||||
}
|
51
src/editor/lsp/semantic_tokens.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
export class SemanticToken {
|
||||
delta_line: number
|
||||
delta_start: number
|
||||
length: number
|
||||
token_type: string
|
||||
token_modifiers_bitset: string
|
||||
|
||||
constructor(
|
||||
delta_line = 0,
|
||||
delta_start = 0,
|
||||
length = 0,
|
||||
token_type = '',
|
||||
token_modifiers_bitset = ''
|
||||
) {
|
||||
this.delta_line = delta_line
|
||||
this.delta_start = delta_start
|
||||
this.length = length
|
||||
this.token_type = token_type
|
||||
this.token_modifiers_bitset = token_modifiers_bitset
|
||||
}
|
||||
}
|
||||
|
||||
export function deserializeTokens(
|
||||
data: number[],
|
||||
semanticTokensProvider?: LSP.SemanticTokensOptions
|
||||
): SemanticToken[] {
|
||||
if (!semanticTokensProvider) {
|
||||
return []
|
||||
}
|
||||
// Check if data length is divisible by 5
|
||||
if (data.length % 5 !== 0) {
|
||||
throw new Error('Length is not divisible by 5')
|
||||
}
|
||||
|
||||
const tokens = []
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
tokens.push(
|
||||
new SemanticToken(
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2],
|
||||
semanticTokensProvider.legend.tokenTypes[data[i + 3]],
|
||||
semanticTokensProvider.legend.tokenModifiers[data[i + 4]]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|