Compare commits
72 Commits
Author | SHA1 | Date | |
---|---|---|---|
cccedceea0 | |||
ed68a34560 | |||
00ee913e3f | |||
46cc67e2db | |||
ff1be34f54 | |||
848bf61277 | |||
043333d3bc | |||
19d90b8081 | |||
4837c52908 | |||
afcf820bdd | |||
18959510f8 | |||
798cbe968a | |||
9cbc088ba3 | |||
2693a5609b | |||
3507da7b39 | |||
56cfb6d1f0 | |||
2b974ef1de | |||
253f1992fd | |||
76d3794b45 | |||
e52c8c9db6 | |||
eb48d51309 | |||
f3274e03ff | |||
46937199a3 | |||
e2a4798c2f | |||
659e6d5b45 | |||
1fbd0ad675 | |||
743ea1af4d | |||
2b1a556b81 | |||
853389ba22 | |||
023af60781 | |||
18db6f2dc1 | |||
4afec15323 | |||
152108f7a5 | |||
32d928ae0c | |||
6f0fae625f | |||
9bc47cf14a | |||
eea47aae1e | |||
25b9b4cf98 | |||
0f3f0b3b68 | |||
33eb6126d4 | |||
dccb83f614 | |||
b56a3398ad | |||
11658e2ff5 | |||
de255acc59 | |||
d33ddb2f1b | |||
a0730ded4e | |||
afd2b507ef | |||
8983a8231b | |||
9dd708db5d | |||
c25dd1800c | |||
e56e7ba0fa | |||
dbe4e7faa6 | |||
148e125dd7 | |||
75bb91c7e1 | |||
6809a46b6a | |||
965d2b23cf | |||
25392824cb | |||
43b1272538 | |||
24268fa744 | |||
63dd417d33 | |||
a8d23f5c0d | |||
6ea3a37e8a | |||
01f95d4ace | |||
3b9094e0dd | |||
a6d0f17970 | |||
108827075d | |||
aa24b9d6bd | |||
ff08c30ddc | |||
ea35db506b | |||
81a744faa5 | |||
7866686a1d | |||
19761baba6 |
@ -1,3 +1,7 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
||||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||||
|
VITE_KC_SKIP_AUTH=false
|
||||||
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
|
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
||||||
|
VITE_KC_SENTRY_DSN=
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||||
|
VITE_KC_SKIP_AUTH=false
|
||||||
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
|
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
|
||||||
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||||
|
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
src/wasm-lib/pkg/wasm_lib.js
|
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
|
46
.github/workflows/cargo-clippy.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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: 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
|
51
.github/workflows/cargo-test.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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: cargo test
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo test --all
|
||||||
|
env:
|
||||||
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
|
|
198
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
- run: yarn install
|
||||||
|
|
||||||
|
- run: yarn fmt-check
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- id: export_version
|
||||||
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
|
build-apps:
|
||||||
|
needs: [check-format, build-test-web]
|
||||||
|
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)
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: src-tauri/target/release/bundle/*/*
|
||||||
|
|
||||||
|
|
||||||
|
publish-apps-release:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
needs: [build-test-web, build-apps]
|
||||||
|
env:
|
||||||
|
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: Generate the update static endpoint
|
||||||
|
run: |
|
||||||
|
ls -l artifact
|
||||||
|
ls -l artifact/*
|
||||||
|
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||||
|
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||||
|
WINDOWS_SIG=`cat artifact/nsis/*.nsis.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 darwin_sig "$DARWIN_SIG" \
|
||||||
|
--arg darwin_url "$RELEASE_DIR/macos/kittycad-modeling-app.app.tar.gz" \
|
||||||
|
--arg linux_sig "$LINUX_SIG" \
|
||||||
|
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||||
|
--arg windows_sig "$WINDOWS_SIG" \
|
||||||
|
--arg windows_url "$RELEASE_DIR/nsis/kittycad-modeling-app_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
||||||
|
'{
|
||||||
|
"version": $version,
|
||||||
|
"platforms": {
|
||||||
|
"darwin-x86_64": {
|
||||||
|
"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: 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: '*/kittycad-modeling-app*'
|
||||||
|
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 release files to Github
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: artifact/*/kittycad-modeling-app*
|
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
|
# rust
|
||||||
src/wasm-lib/target
|
src/wasm-lib/target
|
||||||
|
src/wasm-lib/bindings
|
||||||
|
src/wasm-lib/kcl/bindings
|
||||||
public/wasm_lib_bg.wasm
|
public/wasm_lib_bg.wasm
|
||||||
|
src/wasm-lib/lcov.info
|
||||||
|
11
README.md
@ -1,6 +1,6 @@
|
|||||||
## Kurt demo project
|
## Kurt demo project
|
||||||
|
|
||||||
live at [untitled-app.kittycad.io](https://untitled-app.kittycad.io/)
|
live at [app.kittycad.io](https://app.kittycad.io/)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@ -43,6 +43,15 @@ If you want to edit the rust files, you can cd into `src/wasm-lib` and then use
|
|||||||
|
|
||||||
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.
|
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
|
||||||
|
|
||||||
|
## Developing in Chrome
|
||||||
|
|
||||||
|
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".
|
||||||
|
|
||||||
## Tauri
|
## Tauri
|
||||||
|
|
||||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||||
|
BIN
app-icon.png
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 207 KiB |
19014
docs/kcl.json
Normal file
3398
docs/kcl.md
Normal file
@ -11,6 +11,7 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
|
||||||
<title>KittyCAD Modeling App</title>
|
<title>KittyCAD Modeling App</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="body-bg">
|
<body class="body-bg">
|
||||||
|
21
package.json
@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.0.3",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@kittycad/lib": "^0.0.27",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
|
"@kittycad/lib": "^0.0.35",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
|
"@sentry/react": "^7.65.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
@ -18,8 +21,10 @@
|
|||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||||
"@uiw/react-codemirror": "^4.15.1",
|
"@uiw/react-codemirror": "^4.15.1",
|
||||||
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -32,6 +37,7 @@
|
|||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.0.4",
|
"swr": "^2.0.4",
|
||||||
|
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
@ -40,6 +46,7 @@
|
|||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.1.4"
|
"zustand": "^4.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -50,15 +57,15 @@
|
|||||||
"build:both:local": "yarn build:wasm && vite build",
|
"build:both:local": "yarn build:wasm && vite build",
|
||||||
"test": "vitest --mode development",
|
"test": "vitest --mode development",
|
||||||
"test:nowatch": "vitest run --mode development",
|
"test:nowatch": "vitest run --mode development",
|
||||||
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
|
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
|
||||||
"test:cov": "vitest run --coverage --mode development",
|
"test:cov": "vitest run --coverage --mode development",
|
||||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
||||||
"simpleserver": "http-server ./public --cors -p 3000",
|
"simpleserver": "http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src",
|
"fmt": "prettier --write ./src",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src",
|
||||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
"build:wasm": "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//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\"",
|
"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",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
||||||
},
|
},
|
||||||
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 15 KiB |
@ -1,26 +1,45 @@
|
|||||||
<svg width="316" height="75" viewBox="0 0 316 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M7.33449 67.7274V65.5747H3.02906V63.4219H0.876343V18.2149H3.02906V16.0622H5.18177V13.9095H7.33449V11.7568H9.4872V7.45137H11.6399V5.29866H13.7926V3.14594H15.9453V0.993229H18.0981V3.14594H20.2508V5.29866H22.4035V7.45137H24.5562V9.60409H31.0143V7.45137H33.1671V5.29866H35.3198V3.14594H37.4725V0.993229H39.6252V3.14594H41.7779V5.29866H43.9306V7.45137H46.0833V11.7568H48.2361V13.9095H50.3888V16.0622H52.5415V18.2149H54.6942V63.4219H52.5415V65.5747H48.2361V67.7274H41.7779V69.8801H43.9306V74.1855H31.0143V69.8801H33.1671V67.7274H22.4035V69.8801H24.5562V74.1855H11.6399V69.8801H13.7926V67.7274H7.33449Z" fill="#101412"/>
|
<path d="M2.52 26.04V25.2H0.84V24.36H0V6.72H0.84V5.88H1.68V5.04H2.52V4.2H3.36V3.36H17.64V4.2H18.48V5.04H19.32V5.88H20.16V6.72H21V24.36H20.16V25.2H18.48V26.04H15.96V26.88H16.8V28.56H11.76V26.88H12.6V26.04H8.4V26.88H9.24V28.56H4.2V26.88H5.04V26.04H2.52Z" fill="#101412"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5563 11.7568H31.0145V9.60409H33.1672V7.45137H35.3199V5.29866H37.4726V3.14594H39.6253V5.29866H41.778V7.45137H43.9307V11.7568H46.0835V13.9095H48.2361V16.0622H50.3888V18.2149H52.5415V20.3677V22.5204V50.5057V52.6584V54.8111V63.4219H48.2361V65.5747H39.6253V67.7274V69.8801H41.778V72.0328H33.1671V69.8801H35.3199V67.7274V65.5747H20.2509V67.7274V69.8801H22.4036V72.0328H13.7927V69.8801H15.9454V67.7274V65.5747H7.33454V63.4219H3.02911V54.8111V52.6584V50.5057V22.5204V20.3677V18.2149H5.18183V16.0622H7.33454V13.9095H9.48726V11.7568L9.48731 11.7568H11.64V7.45137H13.7927V5.29866H15.9455V3.14594H18.0982V5.29866H20.2509V7.45137H22.4036V9.60409H24.5563V11.7568ZM5.18191 56.9638V59.1165H15.9455V56.9638H5.18191ZM9.48734 61.2692V63.4219H15.9455V61.2692H9.48734ZM39.6253 59.1165V56.9638H41.7781V59.1165H39.6253ZM43.9308 56.9638V59.1165H46.0835V56.9638H43.9308ZM48.2362 59.1165V56.9638H50.3889V59.1165H48.2362ZM39.6253 61.2692V63.4219H46.0835V61.2692H39.6253ZM20.2509 59.1165H35.3199V63.4219H20.2509V59.1165Z" fill="#D0FF00"/>
|
<path d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
<path d="M3.02911 52.6584V50.5057H52.5415V52.6584H3.02911Z" fill="#B1E515"/>
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
<path d="M9.48725 26.8258V24.6731H46.0834V26.8258H48.2361V44.0475H46.0834V46.2002H9.48725V44.0475H7.33453V26.8258H9.48725Z" fill="#1F2320"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.839996 20.58V24.36H2.52V25.2H18.48V24.36H20.16V20.58H0.839996ZM7.56 24.36V22.68H13.44V24.36H7.56Z" fill="#9BADB7"/>
|
||||||
<path d="M35.3198 35.4367V26.8258H39.6252V35.4367H35.3198Z" fill="#D0FF00"/>
|
<path d="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
<path d="M24.5562 35.4367H31.0144V37.5894H28.8617V41.8948H35.3198V39.7421H37.4725V41.8948H35.3198V44.0475H20.2508V41.8948H18.0981V39.7421H20.2508V41.8948H26.709V37.5894H24.5562V35.4367Z" fill="#D0FF00"/>
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
<path d="M20.2508 33.2839V31.1312H13.7927V33.2839H11.64V31.1312H13.7927V28.9785H20.2508V31.1312H22.4035V33.2839H20.2508Z" fill="#D0FF00"/>
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
<path d="M48.2361 18.2149V16.0622H50.3888V18.2149H48.2361Z" fill="#92C51B"/>
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
<path d="M7.33448 18.2149V16.0622H5.18176V18.2149H7.33448Z" fill="#92C51B"/>
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
<path d="M46.0834 16.0622V13.9095H48.2361V16.0622H46.0834Z" fill="#92C51B"/>
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
<path d="M9.48725 16.0622V13.9095H7.33453V16.0622H9.48725Z" fill="#92C51B"/>
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
<rect x="26.709" y="11.7568" width="2.15271" height="4.30543" fill="#B1E515"/>
|
<path d="M18.48 5.88V5.04H17.64V4.2H3.36V5.04H2.52V5.88H1.68V6.72H0.839996V8.4H20.16V6.72H19.32V5.88H18.48Z" fill="#FBF580"/>
|
||||||
<path d="M35.3197 16.0622V13.9095H37.4725V11.7568H39.6252V13.9095H41.7779V16.0622H35.3197Z" fill="#101412"/>
|
<path d="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
<path d="M15.9453 13.9095V11.7568H18.098V13.9095H20.2507V16.0622H13.7926V13.9095H15.9453Z" fill="#101412"/>
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
<path d="M9.48718 52.6584V50.5057H15.9453V52.6584H9.48718Z" fill="#92C51B"/>
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
<rect x="24.5562" y="11.7568" width="6.45814" height="2.15271" fill="#92C51B"/>
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
<path d="M77.4822 18.0099V33.8657L92.8664 17.1258L99.4091 22.077L86.9721 35.5161L103.535 58.0914H92.6306L81.0777 41.8231L77.4822 45.7133V58.0914H68.8175V18.0099H77.4822Z" fill="#FFFFFA"/>
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
<path d="M115.158 20.7213C115.158 22.0574 114.666 23.1969 113.684 24.14C112.741 25.0438 111.601 25.4957 110.265 25.4957C108.929 25.4957 107.809 25.0438 106.906 24.14C106.002 23.1969 105.55 22.0574 105.55 20.7213C105.55 19.3853 106.002 18.2457 106.906 17.3026C107.809 16.3595 108.929 15.888 110.265 15.888C111.601 15.888 112.741 16.3595 113.684 17.3026C114.666 18.2457 115.158 19.3853 115.158 20.7213ZM114.627 29.5039V58.0914H105.962V29.5039H114.627Z" fill="#FFFFFA"/>
|
<path d="M9.24 13.44H11.76V14.28H10.92V15.96H13.44V15.12H14.28V15.96H13.44V16.8H7.56V15.96H6.72V15.12H7.56V15.96H10.08V14.28H9.24V13.44Z" fill="#DBFF3C"/>
|
||||||
<path d="M133.871 59.0935C130.335 59.0935 127.407 58.1897 125.089 56.3821C122.809 54.5745 121.67 51.922 121.67 48.4247V36.636H117.603V29.9165H121.67V22.8433L130.276 21.4286V29.9165H136.052L137.938 36.636H130.276V47.128C130.276 48.5033 130.629 49.6429 131.337 50.5467C132.044 51.4112 133.066 51.8434 134.402 51.8434C134.913 51.8434 135.463 51.7648 136.052 51.6077C136.642 51.4505 137.231 51.2343 137.82 50.9593L140.355 57.0894C139.687 57.6395 138.705 58.1111 137.408 58.504C136.111 58.897 134.932 59.0935 133.871 59.0935Z" fill="#FFFFFA"/>
|
<path d="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
<path d="M156.465 59.0935C152.929 59.0935 150.001 58.1897 147.683 56.3821C145.404 54.5745 144.264 51.922 144.264 48.4247V36.636H140.197V29.9165H144.264V22.8433L152.87 21.4286V29.9165H158.646L160.532 36.636H152.87V47.128C152.87 48.5033 153.223 49.6429 153.931 50.5467C154.638 51.4112 155.66 51.8434 156.996 51.8434C157.507 51.8434 158.057 51.7648 158.646 51.6077C159.236 51.4505 159.825 51.2343 160.415 50.9593L162.949 57.0894C162.281 57.6395 161.299 58.1111 160.002 58.504C158.705 58.897 157.526 59.0935 156.465 59.0935Z" fill="#FFFFFA"/>
|
<path d="M3.36 5.88V5.04H4.2V3.36H5.04V1.68H5.88V0.839996H6.72V1.68H7.56V3.36H8.4V5.04H9.24V5.88H3.36Z" fill="#DBFF3C"/>
|
||||||
<path d="M172.163 59.0345L173.165 56.6178L162.791 30.5649L171.515 29.5039C172.576 32.3332 173.637 35.1625 174.698 37.9917C175.759 40.821 176.8 43.6503 177.822 46.4796L183.834 29.5039H192.793L180.062 61.687C179.119 64.0054 177.488 65.9898 175.169 67.6403C172.851 69.33 170.375 70.4892 167.742 71.1179L164.736 64.1036C166.151 63.5535 167.625 62.8658 169.157 62.0406C170.69 61.2154 171.692 60.2134 172.163 59.0345Z" fill="#FFFFFA"/>
|
<path d="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
<path d="M203.975 58.0914L197.64 51.7563V21.3723L203.975 15.0371H223.072L229.438 21.3723V31.2748H220.735V25.6162L218.859 23.7402H208.219L206.343 25.6162V47.5124L208.219 49.3883H218.859L220.735 47.5124V41.8538H229.438V51.7563L223.072 58.0914H203.975Z" fill="#FFFFFA"/>
|
<path d="M13.44 1.68V0H15.96V1.68H16.8V3.36H17.64V4.62H16.8V3.36H15.96V1.68H15.12V0.84H14.28V1.68H13.44V3.36H12.6V4.62H11.76V3.36H12.6V1.68H13.44Z" fill="#92C51B"/>
|
||||||
<path d="M236.208 58.0914V21.3723L242.544 15.0371H262.41L268.745 21.3723V58.0914H260.073V45.4212H244.881V58.0914H236.208ZM244.881 36.7488H260.073V25.6162L258.197 23.7402H246.757L244.881 25.6162V36.7488Z" fill="#FFFFFA"/>
|
<path d="M5.04 1.68V0H7.56V1.68H8.4V3.36H9.24V4.62H8.4V3.36H7.56V1.68H6.72V0.84H5.88V1.68H5.04V3.36H4.2V4.62H3.36V3.36H4.2V1.68H5.04Z" fill="#92C51B"/>
|
||||||
<path d="M276.098 58.0914V15.0371H301.716L308.051 21.3723V51.7563L301.716 58.0914H276.098ZM284.802 49.3883H297.503L299.379 47.5124V25.6162L297.503 23.7402H284.802V49.3883Z" fill="#FFFFFA"/>
|
<rect x="9.24" y="5.03999" width="2.52" height="0.84" fill="#D0CC6A"/>
|
||||||
|
<path d="M13.44 5.88V5.04H14.28V4.2H15.12V5.04H15.96V5.88H13.44Z" fill="#76AA1D"/>
|
||||||
|
<path d="M5.88 5.04V4.2H6.72V5.04H7.56V5.88H5.04V5.04H5.88Z" fill="#76AA1D"/>
|
||||||
|
<path d="M17.64 5.88V5.04H16.8V4.2H17.64V5.04H18.48V5.88H17.64Z" fill="#76AA1D"/>
|
||||||
|
<path d="M3.36 5.04V5.88H2.52V5.04H3.36V4.2H4.2V5.04H3.36Z" fill="#76AA1D"/>
|
||||||
|
<path d="M8.4 4.2H9.24V5.04H10.08V5.88H9.24V5.04H8.4V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M11.76 4.2H12.6V5.04H11.76V5.88H10.92V5.04H11.76V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M14.28 10.92H13.44V13.44H14.28V10.92Z" fill="#92C51B"/>
|
||||||
|
<path d="M1.68 21V19.32H0.839996V21H1.68Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M19.32 21V19.32H20.16V21H19.32Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M3.36 20.16V19.32H5.88V20.16H3.36Z" fill="#D56161"/>
|
||||||
|
<path d="M3.36 21V20.16H5.88V21H3.36Z" fill="#AC3232"/>
|
||||||
|
<path d="M29.991 6.64V12.827L35.994 6.295L38.547 8.227L33.694 13.471L40.157 22.28H35.902L31.394 15.932L29.991 17.45V22.28H26.61V6.64H29.991Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M44.6921 7.698C44.6921 8.21933 44.5005 8.664 44.1171 9.032C43.7491 9.38466 43.3045 9.561 42.7831 9.561C42.2618 9.561 41.8248 9.38466 41.4721 9.032C41.1195 8.664 40.9431 8.21933 40.9431 7.698C40.9431 7.17666 41.1195 6.732 41.4721 6.364C41.8248 5.996 42.2618 5.812 42.7831 5.812C43.3045 5.812 43.7491 5.996 44.1171 6.364C44.5005 6.732 44.6921 7.17666 44.6921 7.698ZM44.4851 11.125V22.28H41.1041V11.125H44.4851Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M51.9943 22.671C50.6143 22.671 49.4719 22.3183 48.5673 21.613C47.6779 20.9077 47.2333 19.8727 47.2333 18.508V13.908H45.6463V11.286H47.2333V8.526L50.5913 7.974V11.286H52.8453L53.5813 13.908H50.5913V18.002C50.5913 18.5387 50.7293 18.9833 51.0053 19.336C51.2813 19.6733 51.6799 19.842 52.2013 19.842C52.4006 19.842 52.6153 19.8113 52.8453 19.75C53.0753 19.6887 53.3053 19.6043 53.5353 19.497L54.5243 21.889C54.2636 22.1037 53.8803 22.2877 53.3743 22.441C52.8683 22.5943 52.4083 22.671 51.9943 22.671Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M60.8106 22.671C59.4306 22.671 58.2883 22.3183 57.3836 21.613C56.4943 20.9077 56.0496 19.8727 56.0496 18.508V13.908H54.4626V11.286H56.0496V8.526L59.4076 7.974V11.286H61.6616L62.3976 13.908H59.4076V18.002C59.4076 18.5387 59.5456 18.9833 59.8216 19.336C60.0976 19.6733 60.4963 19.842 61.0176 19.842C61.217 19.842 61.4316 19.8113 61.6616 19.75C61.8916 19.6887 62.1216 19.6043 62.3516 19.497L63.3406 21.889C63.08 22.1037 62.6966 22.2877 62.1906 22.441C61.6846 22.5943 61.2246 22.671 60.8106 22.671Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M66.936 22.648L67.327 21.705L63.279 11.539L66.683 11.125C67.097 12.229 67.511 13.333 67.925 14.437C68.339 15.541 68.7453 16.645 69.144 17.749L71.49 11.125H74.986L70.018 23.683C69.65 24.5877 69.0137 25.362 68.109 26.006C67.2043 26.6653 66.2383 27.1177 65.211 27.363L64.038 24.626C64.59 24.4113 65.165 24.143 65.763 23.821C66.361 23.499 66.752 23.108 66.936 22.648Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M79.3491 22.28L76.8771 19.808V7.952L79.3491 5.48H86.8011L89.2851 7.952V11.816H85.8891V9.608L85.1571 8.876H81.0051L80.2731 9.608V18.152L81.0051 18.884H85.1571L85.8891 18.152V15.944H89.2851V19.808L86.8011 22.28H79.3491Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M91.9268 22.28V7.952L94.3988 5.48H102.151L104.623 7.952V22.28H101.239V17.336H95.3108V22.28H91.9268ZM95.3108 13.952H101.239V9.608L100.507 8.876H96.0428L95.3108 9.608V13.952Z" fill="#FFFFFA"/>
|
||||||
|
<path d="M107.492 22.28V5.48H117.488L119.96 7.952V19.808L117.488 22.28H107.492ZM110.888 18.884H115.844L116.576 18.152V9.608L115.844 8.876H110.888V18.884Z" fill="#FFFFFA"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@ -1,26 +1,45 @@
|
|||||||
<svg width="788" height="183" viewBox="0 0 788 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="123" height="29" viewBox="0 0 123 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M16.6075 166.835V161.454H5.84388V156.072H0.462097V43.0543H5.84388V37.6725H11.2257V32.2907H16.6075V26.9089H21.9892V16.1454H27.371V10.7636H32.7528V5.38179H38.1346V0H43.5164V5.38179H48.8982V10.7636H54.28V16.1454H59.6617V21.5271H75.8071V16.1454H81.1889V10.7636H86.5707V5.38179H91.9525V0H97.3342V5.38179H102.716V10.7636H108.098V16.1454H113.48V26.9089H118.861V32.2907H124.243V37.6725H129.625V43.0543H135.007V156.072H129.625V161.454H118.861V166.835H102.716V172.217H108.098V182.981H75.8071V172.217H81.1889V166.835H54.28V172.217H59.6617V182.981H27.371V172.217H32.7528V166.835H16.6075Z" fill="#101412"/>
|
<path d="M2.52 26.04V25.2H0.84V24.36H0V6.72H0.84V5.88H1.68V5.04H2.52V4.2H3.36V3.36H17.64V4.2H18.48V5.04H19.32V5.88H20.16V6.72H21V24.36H20.16V25.2H18.48V26.04H15.96V26.88H16.8V28.56H11.76V26.88H12.6V26.04H8.4V26.88H9.24V28.56H4.2V26.88H5.04V26.04H2.52Z" fill="#101412"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9892 26.9095L21.9892 26.9102V32.2919H16.6075V37.6737H11.2257V43.0555H5.84388V48.4364V53.8191V123.781V129.163V129.164V134.545V156.073H16.6075V161.455H38.1346V166.836V172.217H32.7528V177.599H54.28V172.217H48.8982V166.836V161.455H86.5707V166.836V172.217H81.1889V177.599H102.716V172.217H97.3342V166.836V161.455H118.861V156.073H129.625V134.545V129.164V129.163V123.781V53.8191V48.4364V43.0555H124.243V37.6737H118.861V32.2919H113.48V26.9102V26.9095H108.098V16.1459H102.716V10.7641H97.3342V5.38232H91.9525V10.7641H86.5707V16.1459H81.1889V21.5277H75.8071V26.9102H59.6617V21.5277H54.28V16.1459H48.8982V10.7641H43.5164V5.38232H38.1346V10.7641H32.7528V16.1459H27.371V26.9095H21.9892ZM11.2257 129.164H124.243V129.163H11.2257V129.164ZM11.2257 139.927V145.309H38.1346V139.927H11.2257ZM21.9893 150.691V156.072H38.1346V150.691H21.9893ZM97.3343 145.309V139.927H102.716V145.309H97.3343ZM108.098 139.927V145.309H113.48V139.927H108.098ZM118.861 145.309V139.927H124.243V145.309H118.861ZM97.3343 150.691V156.072H113.48V150.691H97.3343ZM48.8982 145.309H86.5707V156.073H48.8982V145.309Z" fill="#D0FF00"/>
|
<path d="M5.04 26.04V24.78H8.4V26.04H7.56V26.88H8.4V27.72H5.04V26.88H5.88V26.04H5.04Z" fill="#4B4862"/>
|
||||||
<path d="M5.84388 129.163V123.781H129.625V129.163H5.84388Z" fill="#B1E515"/>
|
<path d="M12.6 26.04V24.78H15.96V26.04H15.12V26.88H15.96V27.72H12.6V26.88H13.44V26.04H12.6Z" fill="#4B4862"/>
|
||||||
<path d="M21.9892 64.5812V59.1995H113.48V64.5812H118.861V107.636H113.48V113.017H21.9892V107.636H16.6075V64.5812H21.9892Z" fill="#1F2320"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.839996 20.58V24.36H2.52V25.2H18.48V24.36H20.16V20.58H0.839996ZM7.56 24.36V22.68H13.44V24.36H7.56Z" fill="#9BADB7"/>
|
||||||
<path d="M86.5707 86.1092V64.582H97.3343V86.1092H86.5707Z" fill="#D0FF00"/>
|
<path d="M0.839996 21V19.32H20.16V21H0.839996Z" fill="#BECAD0"/>
|
||||||
<path d="M59.6617 86.1092H75.8071V91.491H70.4253V102.255H86.5707V96.8727H91.9525V102.255H86.5707V107.636H48.8982V102.255H43.5164V96.8727H48.8982V102.255H65.0435V91.491H59.6617V86.1092Z" fill="#D0FF00"/>
|
<path d="M1.68 22.68V21.84H5.88V22.68H1.68Z" fill="#2B3E48"/>
|
||||||
<path d="M48.8982 80.7274V75.3456H32.7528V80.7274H27.371V75.3456H32.7528V69.9638H48.8982V75.3456H54.28V80.7274H48.8982Z" fill="#D0FF00"/>
|
<path d="M3.36 24.36V23.52H5.88V24.36H3.36Z" fill="#2B3E48"/>
|
||||||
<path d="M118.861 43.0534V37.6716H124.243V43.0534H118.861Z" fill="#92C51B"/>
|
<path d="M15.12 22.68V21.84H15.96V22.68H15.12Z" fill="#2B3E48"/>
|
||||||
<path d="M16.6075 43.0534V37.6716H11.2257V43.0534H16.6075Z" fill="#92C51B"/>
|
<path d="M16.8 22.68V21.84H17.64V22.68H16.8Z" fill="#2B3E48"/>
|
||||||
<path d="M113.48 37.6728V32.291H118.861V37.6728H113.48Z" fill="#92C51B"/>
|
<path d="M18.48 22.68V21.84H19.32V22.68H18.48Z" fill="#2B3E48"/>
|
||||||
<path d="M21.9892 37.6728V32.291H16.6075V37.6728H21.9892Z" fill="#92C51B"/>
|
<path d="M15.12 24.36V23.52H17.64V24.36H15.12Z" fill="#2B3E48"/>
|
||||||
<rect x="65.0435" y="26.9087" width="5.38179" height="10.7636" fill="#B1E515"/>
|
<path d="M18.48 5.88V5.04H17.64V4.2H3.36V5.04H2.52V5.88H1.68V6.72H0.839996V8.4H20.16V6.72H19.32V5.88H18.48Z" fill="#FBF580"/>
|
||||||
<path d="M86.5707 37.6723V32.2905H91.9525V26.9087H97.3342V32.2905H102.716V37.6723H86.5707Z" fill="#101412"/>
|
<path d="M0.839996 20.16V18.48H20.16V20.16H0.839996Z" fill="#AEAA4C"/>
|
||||||
<path d="M38.1346 32.2905V26.9087H43.5164V32.2905H48.8982V37.6723H32.7528V32.2905H38.1346Z" fill="#101412"/>
|
<path d="M20.16 7.56H0.839996V19.32H20.16V7.56Z" fill="#E5E3A1"/>
|
||||||
<path d="M21.9892 129.163V123.781H38.1346V129.163H21.9892Z" fill="#92C51B"/>
|
<path d="M3.36 10.08V9.24001H17.64V10.08H18.48V16.8H17.64V17.64H3.36V16.8H2.52V10.08H3.36Z" fill="#1F2320"/>
|
||||||
<rect x="59.6617" y="26.9087" width="16.1454" height="5.38179" fill="#92C51B"/>
|
<rect x="8.4" y="4.2" width="4.2" height="1.68" fill="#AEAA4C"/>
|
||||||
<path d="M191.977 42.5414V82.1808L230.437 40.331L246.794 52.7091L215.701 86.3068L257.109 142.745H229.848L200.966 102.074L191.977 111.8V142.745H170.315V42.5414H191.977Z" fill="#101412"/>
|
<path d="M13.44 10.92V10.08H15.12V13.44H14.28L13.44 10.92Z" fill="#DBFF3C"/>
|
||||||
<path d="M286.165 49.3199C286.165 52.66 284.937 55.5089 282.481 57.8666C280.124 60.1261 277.275 61.2559 273.935 61.2559C270.594 61.2559 267.795 60.1261 265.535 57.8666C263.276 55.5089 262.146 52.66 262.146 49.3199C262.146 45.9797 263.276 43.1308 265.535 40.7731C267.795 38.4153 270.594 37.2365 273.935 37.2365C277.275 37.2365 280.124 38.4153 282.481 40.7731C284.937 43.1308 286.165 45.9797 286.165 49.3199ZM284.839 71.2763V142.745H263.177V71.2763H284.839Z" fill="#101412"/>
|
<path d="M9.24 13.44H11.76V14.28H10.92V15.96H13.44V15.12H14.28V15.96H13.44V16.8H7.56V15.96H6.72V15.12H7.56V15.96H10.08V14.28H9.24V13.44Z" fill="#DBFF3C"/>
|
||||||
<path d="M332.949 145.25C324.108 145.25 316.789 142.991 310.993 138.472C305.295 133.953 302.446 127.322 302.446 118.578V89.1066H292.278V72.3078H302.446V54.6248L323.96 51.0882V72.3078H338.402L343.117 89.1066H323.96V115.336C323.96 118.775 324.845 121.624 326.613 123.883C328.381 126.044 330.935 127.125 334.276 127.125C335.553 127.125 336.928 126.929 338.402 126.536C339.875 126.143 341.349 125.602 342.822 124.915L349.159 140.24C347.489 141.615 345.033 142.794 341.791 143.777C338.549 144.759 335.602 145.25 332.949 145.25Z" fill="#101412"/>
|
<path d="M7.56 12.6V11.76H5.04V12.6H4.2V11.76H5.04V10.92H7.56V11.76H8.4V12.6H7.56Z" fill="#DBFF3C"/>
|
||||||
<path d="M389.435 145.25C380.593 145.25 373.274 142.991 367.478 138.472C361.781 133.953 358.932 127.322 358.932 118.578V89.1066H348.764V72.3078H358.932V54.6248L380.446 51.0882V72.3078H394.887L399.602 89.1066H380.446V115.336C380.446 118.775 381.33 121.624 383.098 123.883C384.867 126.044 387.421 127.125 390.761 127.125C392.038 127.125 393.413 126.929 394.887 126.536C396.361 126.143 397.834 125.602 399.308 124.915L405.644 140.24C403.974 141.615 401.518 142.794 398.276 143.777C395.034 144.759 392.087 145.25 389.435 145.25Z" fill="#101412"/>
|
<path d="M3.36 5.88V5.04H4.2V3.36H5.04V1.68H5.88V0.839996H6.72V1.68H7.56V3.36H8.4V5.04H9.24V5.88H3.36Z" fill="#DBFF3C"/>
|
||||||
<path d="M428.679 145.103L431.184 139.061L405.249 73.9287L427.058 71.2763C429.711 78.3495 432.363 85.4227 435.016 92.4959C437.668 99.5691 440.272 106.642 442.826 113.715L457.856 71.2763H480.255L448.425 151.734C446.068 157.53 441.991 162.491 436.195 166.617C430.398 170.841 424.209 173.739 417.627 175.311L410.112 157.776C413.649 156.4 417.333 154.681 421.164 152.618C424.995 150.555 427.5 148.05 428.679 145.103Z" fill="#101412"/>
|
<path d="M17.64 5.04V5.88H11.76V5.04H12.6V3.36H13.44V1.68H14.28V0.839996H15.12V1.68H15.96V3.36H16.8V5.04H17.64Z" fill="#DBFF3C"/>
|
||||||
<path d="M508.208 142.745L492.371 126.907V50.9472L508.208 35.1094H555.953L571.867 50.9472V75.7034H550.109V61.557L545.42 56.8672H518.818L514.128 61.557V116.297L518.818 120.987H545.42L550.109 116.297V102.151H571.867V126.907L555.953 142.745H508.208Z" fill="#101412"/>
|
<path d="M13.44 1.68V0H15.96V1.68H16.8V3.36H17.64V4.62H16.8V3.36H15.96V1.68H15.12V0.84H14.28V1.68H13.44V3.36H12.6V4.62H11.76V3.36H12.6V1.68H13.44Z" fill="#92C51B"/>
|
||||||
<path d="M588.792 142.745V50.9472L604.63 35.1094H654.296L670.134 50.9472V142.745H648.453V111.069H610.473V142.745H588.792ZM610.473 89.3885H648.453V61.557L643.763 56.8672H615.163L610.473 61.557V89.3885Z" fill="#101412"/>
|
<path d="M5.04 1.68V0H7.56V1.68H8.4V3.36H9.24V4.62H8.4V3.36H7.56V1.68H6.72V0.84H5.88V1.68H5.04V3.36H4.2V4.62H3.36V3.36H4.2V1.68H5.04Z" fill="#92C51B"/>
|
||||||
<path d="M688.517 142.745V35.1094H752.561L768.399 50.9472V126.907L752.561 142.745H688.517ZM710.275 120.987H742.028L746.718 116.297V61.557L742.028 56.8672H710.275V120.987Z" fill="#101412"/>
|
<rect x="9.24" y="5.03999" width="2.52" height="0.84" fill="#D0CC6A"/>
|
||||||
|
<path d="M13.44 5.88V5.04H14.28V4.2H15.12V5.04H15.96V5.88H13.44Z" fill="#76AA1D"/>
|
||||||
|
<path d="M5.88 5.04V4.2H6.72V5.04H7.56V5.88H5.04V5.04H5.88Z" fill="#76AA1D"/>
|
||||||
|
<path d="M17.64 5.88V5.04H16.8V4.2H17.64V5.04H18.48V5.88H17.64Z" fill="#76AA1D"/>
|
||||||
|
<path d="M3.36 5.04V5.88H2.52V5.04H3.36V4.2H4.2V5.04H3.36Z" fill="#76AA1D"/>
|
||||||
|
<path d="M8.4 4.2H9.24V5.04H10.08V5.88H9.24V5.04H8.4V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M11.76 4.2H12.6V5.04H11.76V5.88H10.92V5.04H11.76V4.2Z" fill="#76AA1D"/>
|
||||||
|
<path d="M14.28 10.92H13.44V13.44H14.28V10.92Z" fill="#92C51B"/>
|
||||||
|
<path d="M1.68 21V19.32H0.839996V21H1.68Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M19.32 21V19.32H20.16V21H19.32Z" fill="#D0CC6A"/>
|
||||||
|
<path d="M3.36 20.16V19.32H5.88V20.16H3.36Z" fill="#D56161"/>
|
||||||
|
<path d="M3.36 21V20.16H5.88V21H3.36Z" fill="#AC3232"/>
|
||||||
|
<path d="M29.991 6.64V12.827L35.994 6.295L38.547 8.227L33.694 13.471L40.157 22.28H35.902L31.394 15.932L29.991 17.45V22.28H26.61V6.64H29.991Z" fill="#08110D"/>
|
||||||
|
<path d="M44.6921 7.698C44.6921 8.21933 44.5005 8.664 44.1171 9.032C43.7491 9.38466 43.3045 9.561 42.7831 9.561C42.2618 9.561 41.8248 9.38466 41.4721 9.032C41.1195 8.664 40.9431 8.21933 40.9431 7.698C40.9431 7.17666 41.1195 6.732 41.4721 6.364C41.8248 5.996 42.2618 5.812 42.7831 5.812C43.3045 5.812 43.7491 5.996 44.1171 6.364C44.5005 6.732 44.6921 7.17666 44.6921 7.698ZM44.4851 11.125V22.28H41.1041V11.125H44.4851Z" fill="#08110D"/>
|
||||||
|
<path d="M51.9943 22.671C50.6143 22.671 49.4719 22.3183 48.5673 21.613C47.6779 20.9077 47.2333 19.8727 47.2333 18.508V13.908H45.6463V11.286H47.2333V8.526L50.5913 7.974V11.286H52.8453L53.5813 13.908H50.5913V18.002C50.5913 18.5387 50.7293 18.9833 51.0053 19.336C51.2813 19.6733 51.6799 19.842 52.2013 19.842C52.4006 19.842 52.6153 19.8113 52.8453 19.75C53.0753 19.6887 53.3053 19.6043 53.5353 19.497L54.5243 21.889C54.2636 22.1037 53.8803 22.2877 53.3743 22.441C52.8683 22.5943 52.4083 22.671 51.9943 22.671Z" fill="#08110D"/>
|
||||||
|
<path d="M60.8106 22.671C59.4306 22.671 58.2883 22.3183 57.3836 21.613C56.4943 20.9077 56.0496 19.8727 56.0496 18.508V13.908H54.4626V11.286H56.0496V8.526L59.4076 7.974V11.286H61.6616L62.3976 13.908H59.4076V18.002C59.4076 18.5387 59.5456 18.9833 59.8216 19.336C60.0976 19.6733 60.4963 19.842 61.0176 19.842C61.217 19.842 61.4316 19.8113 61.6616 19.75C61.8916 19.6887 62.1216 19.6043 62.3516 19.497L63.3406 21.889C63.08 22.1037 62.6966 22.2877 62.1906 22.441C61.6846 22.5943 61.2246 22.671 60.8106 22.671Z" fill="#08110D"/>
|
||||||
|
<path d="M66.936 22.648L67.327 21.705L63.279 11.539L66.683 11.125C67.097 12.229 67.511 13.333 67.925 14.437C68.339 15.541 68.7453 16.645 69.144 17.749L71.49 11.125H74.986L70.018 23.683C69.65 24.5877 69.0137 25.362 68.109 26.006C67.2043 26.6653 66.2383 27.1177 65.211 27.363L64.038 24.626C64.59 24.4113 65.165 24.143 65.763 23.821C66.361 23.499 66.752 23.108 66.936 22.648Z" fill="#08110D"/>
|
||||||
|
<path d="M79.3491 22.28L76.8771 19.808V7.952L79.3491 5.48H86.8011L89.2851 7.952V11.816H85.8891V9.608L85.1571 8.876H81.0051L80.2731 9.608V18.152L81.0051 18.884H85.1571L85.8891 18.152V15.944H89.2851V19.808L86.8011 22.28H79.3491Z" fill="#08110D"/>
|
||||||
|
<path d="M91.9268 22.28V7.952L94.3988 5.48H102.151L104.623 7.952V22.28H101.239V17.336H95.3108V22.28H91.9268ZM95.3108 13.952H101.239V9.608L100.507 8.876H96.0428L95.3108 9.608V13.952Z" fill="#08110D"/>
|
||||||
|
<path d="M107.492 22.28V5.48H117.488L119.96 7.952V19.808L117.488 22.28H107.492ZM110.888 18.884H115.844L116.576 18.152V9.608L115.844 8.876H110.888V18.884Z" fill="#08110D"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
90
src-tauri/Cargo.lock
generated
@ -81,6 +81,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-fs-extra",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.6.0",
|
"toml 0.6.0",
|
||||||
]
|
]
|
||||||
@ -647,6 +648,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -1149,7 +1156,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@ -1162,6 +1169,12 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -1377,7 +1390,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"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",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1627,6 +1651,12 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -2121,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
|
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2694,14 +2724,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "2.3.3"
|
version = "3.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
|
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.21.2",
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.0.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
@ -2710,9 +2741,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with_macros"
|
name = "serde_with_macros"
|
||||||
version = "2.3.3"
|
version = "3.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
|
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3021,6 +3052,7 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"attohttpc",
|
"attohttpc",
|
||||||
|
"base64 0.21.2",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
@ -3033,6 +3065,7 @@ dependencies = [
|
|||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"http",
|
"http",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"minisign-verify",
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
@ -3054,19 +3087,21 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows 0.39.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
|
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@ -3077,7 +3112,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tauri-winres",
|
"tauri-winres",
|
||||||
"winnow",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3120,6 +3154,18 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs-extra"
|
||||||
|
version = "0.0.0"
|
||||||
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7e58dc8502f654b99d51c087421f84ccc0e03119"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@ -3163,12 +3209,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
|
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
@ -3384,7 +3431,7 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
|
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"nom8",
|
"nom8",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
@ -3397,7 +3444,7 @@ version = "0.19.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
|
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 0.6.2",
|
"toml_datetime 0.6.2",
|
||||||
@ -4215,3 +4262,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.3.0", features = [] }
|
tauri-build = { version = "1.4.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
oauth2 = "4.4.1"
|
oauth2 = "4.4.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.3.0", features = ["dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
||||||
tokio = { version = "1.29.1", features = ["time"] }
|
tokio = { version = "1.29.1", features = ["time"] }
|
||||||
toml = "0.6.0"
|
toml = "0.6.0"
|
||||||
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
||||||
|
.plugin(tauri_plugin_fs_extra::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling",
|
"productName": "kittycad-modeling-app",
|
||||||
"version": "0.0.3"
|
"version": "0.3.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -23,7 +23,8 @@
|
|||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"scope": [
|
"scope": [
|
||||||
"$HOME/**/*"
|
"$HOME/**/*",
|
||||||
|
"$APPDATA/**/*"
|
||||||
],
|
],
|
||||||
"all": true
|
"all": true
|
||||||
},
|
},
|
||||||
@ -37,6 +38,9 @@
|
|||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@ -54,7 +58,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"identifier": "KittyCAD-modeling-app",
|
"identifier": "io.kittycad.modeling-app",
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": null,
|
"entitlements": null,
|
||||||
@ -76,7 +80,12 @@
|
|||||||
"csp": null
|
"csp": null
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": false
|
"active": true,
|
||||||
|
"endpoints": [
|
||||||
|
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
import { describe, test, vi } from 'vitest'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
let listener: ((rect: any) => void) | undefined = undefined
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
;(global as any).ResizeObserver = class ResizeObserver {
|
||||||
@ -12,12 +15,38 @@ let listener: ((rect: any) => void) | undefined = undefined
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
describe('App tests', () => {
|
||||||
render(
|
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>
|
<BrowserRouter>
|
||||||
<App />
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
const linkElement = screen.getByText(/Variables/i)
|
}
|
||||||
expect(linkElement).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
153
src/App.tsx
@ -8,8 +8,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { asyncLexer } from './lang/tokeniser'
|
import { asyncParser } from './lang/abstractSyntaxTree'
|
||||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
|
||||||
import { _executor } from './lang/executor'
|
import { _executor } from './lang/executor'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { langs } from '@uiw/codemirror-extensions-langs'
|
import { langs } from '@uiw/codemirror-extensions-langs'
|
||||||
@ -19,7 +18,7 @@ import {
|
|||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
addLineHighlight,
|
addLineHighlight,
|
||||||
} from './editor/highlightextension'
|
} from './editor/highlightextension'
|
||||||
import { PaneType, Selections, Themes, useStore } from './useStore'
|
import { PaneType, Selections, useStore } from './useStore'
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
import { MemoryPanel } from './components/MemoryPanel'
|
||||||
@ -42,9 +41,19 @@ import {
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TEST } from './env'
|
import { TEST } from './env'
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { getSystemTheme } from './lib/getSystemTheme'
|
import { Themes, getSystemTheme } from './lib/theme'
|
||||||
|
import { isTauri } from './lib/isTauri'
|
||||||
|
import { useLoaderData, useParams } from 'react-router-dom'
|
||||||
|
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||||
|
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
||||||
|
import { IndexLoaderData } from './Router'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { onboardingPaths } from 'routes/Onboarding'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
|
const pathParams = useParams()
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
@ -72,16 +81,12 @@ export function App() {
|
|||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
isStreamReady,
|
isStreamReady,
|
||||||
isMouseDownInStream,
|
isMouseDownInStream,
|
||||||
fileId,
|
|
||||||
cmdId,
|
cmdId,
|
||||||
setCmdId,
|
setCmdId,
|
||||||
token,
|
|
||||||
formatCode,
|
formatCode,
|
||||||
debugPanel,
|
|
||||||
theme,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
onboardingStatus,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
setStreamDimensions,
|
setStreamDimensions,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
@ -112,22 +117,27 @@ export function App() {
|
|||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
setIsStreamReady: s.setIsStreamReady,
|
setIsStreamReady: s.setIsStreamReady,
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
isMouseDownInStream: s.isMouseDownInStream,
|
||||||
fileId: s.fileId,
|
|
||||||
cmdId: s.cmdId,
|
cmdId: s.cmdId,
|
||||||
setCmdId: s.setCmdId,
|
setCmdId: s.setCmdId,
|
||||||
token: s.token,
|
|
||||||
formatCode: s.formatCode,
|
formatCode: s.formatCode,
|
||||||
debugPanel: s.debugPanel,
|
|
||||||
addKCLError: s.addKCLError,
|
addKCLError: s.addKCLError,
|
||||||
theme: s.theme,
|
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
onboardingStatus: s.onboardingStatus,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const {
|
||||||
|
auth: {
|
||||||
|
context: { token },
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
context: { showDebugPanel, theme, onboardingStatus },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
// Pane toggling keyboard shortcuts
|
// Pane toggling keyboard shortcuts
|
||||||
@ -145,15 +155,40 @@ export function App() {
|
|||||||
useHotkeys('shift + d', () => togglePane('debug'))
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
|
|
||||||
const paneOpacity =
|
const paneOpacity =
|
||||||
onboardingStatus === 'camera'
|
onboardingStatus === onboardingPaths.CAMERA
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: isMouseDownInStream
|
: didDragInStream
|
||||||
? 'opacity-40'
|
? 'opacity-40'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
// Use file code loaded from disk
|
||||||
|
// on mount, and overwrite any locally-stored code
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTauri() && loadedCode !== null) {
|
||||||
|
setCode(loadedCode)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
// Clear code on unmount if in desktop app
|
||||||
|
if (isTauri()) {
|
||||||
|
setCode('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loadedCode, setCode])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
setCode(value)
|
setCode(value)
|
||||||
|
if (isTauri() && pathParams.id) {
|
||||||
|
// Save the file to disk
|
||||||
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
|
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||||
|
(err) => {
|
||||||
|
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||||
|
console.error('error saving file', err)
|
||||||
|
toast.error('Error saving file, please check file permissions')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
if (editorView) {
|
if (editorView) {
|
||||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||||
}
|
}
|
||||||
@ -218,9 +253,9 @@ export function App() {
|
|||||||
const streamWidth = streamRef?.current?.offsetWidth
|
const streamWidth = streamRef?.current?.offsetWidth
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
const streamHeight = streamRef?.current?.offsetHeight
|
||||||
|
|
||||||
const width = streamWidth ? streamWidth * pixelDensity : 0
|
const width = streamWidth ? streamWidth : 0
|
||||||
const quadWidth = Math.round(width / 4) * 4
|
const quadWidth = Math.round(width / 4) * 4
|
||||||
const height = streamHeight ? streamHeight * pixelDensity : 0
|
const height = streamHeight ? streamHeight : 0
|
||||||
const quadHeight = Math.round(height / 4) * 4
|
const quadHeight = Math.round(height / 4) * 4
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -244,38 +279,24 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isStreamReady) return
|
if (!isStreamReady) return
|
||||||
|
if (!engineCommandManager) return
|
||||||
|
let unsubFn: any[] = []
|
||||||
const asyncWrap = async () => {
|
const asyncWrap = async () => {
|
||||||
try {
|
try {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
setAst(null)
|
setAst(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const tokens = await asyncLexer(code)
|
const _ast = await asyncParser(code)
|
||||||
const _ast = abstractSyntaxTree(tokens)
|
|
||||||
setAst(_ast)
|
setAst(_ast)
|
||||||
resetLogs()
|
resetLogs()
|
||||||
resetKCLErrors()
|
resetKCLErrors()
|
||||||
if (engineCommandManager) {
|
engineCommandManager.endSession()
|
||||||
engineCommandManager.endSession()
|
engineCommandManager.startNewSession()
|
||||||
engineCommandManager.startNewSession()
|
|
||||||
}
|
|
||||||
if (!engineCommandManager) return
|
|
||||||
const programMemory = await _executor(
|
const programMemory = await _executor(
|
||||||
_ast,
|
_ast,
|
||||||
{
|
{
|
||||||
root: {
|
root: {
|
||||||
log: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: (a: any) => {
|
|
||||||
addLog(a)
|
|
||||||
},
|
|
||||||
__meta: [
|
|
||||||
{
|
|
||||||
pathToNode: [],
|
|
||||||
sourceRange: [0, 0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
_0: {
|
_0: {
|
||||||
type: 'userVal',
|
type: 'userVal',
|
||||||
value: 0,
|
value: 0,
|
||||||
@ -297,33 +318,37 @@ export function App() {
|
|||||||
__meta: [],
|
__meta: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pendingMemory: {},
|
|
||||||
},
|
},
|
||||||
engineCommandManager,
|
engineCommandManager
|
||||||
{ bodyType: 'root' },
|
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { artifactMap, sourceRangeMap } =
|
const { artifactMap, sourceRangeMap } =
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
|
|
||||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||||
engineCommandManager.onHover((id) => {
|
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||||
if (!id) {
|
event: 'highlight_set_entity',
|
||||||
setHighlightRange([0, 0])
|
callback: ({ data }) => {
|
||||||
} else {
|
if (!data?.entity_id) {
|
||||||
const sourceRange = sourceRangeMap[id]
|
setHighlightRange([0, 0])
|
||||||
setHighlightRange(sourceRange)
|
} else {
|
||||||
}
|
const sourceRange = sourceRangeMap[data.entity_id]
|
||||||
|
setHighlightRange(sourceRange)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
engineCommandManager.onClick((selections) => {
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
if (!selections) {
|
event: 'select_with_point',
|
||||||
setCursor2()
|
callback: ({ data }) => {
|
||||||
return
|
if (!data?.entity_id) {
|
||||||
}
|
setCursor2()
|
||||||
const { id, type } = selections
|
return
|
||||||
setCursor2({ range: sourceRangeMap[id], type })
|
}
|
||||||
|
const sourceRange = sourceRangeMap[data.entity_id]
|
||||||
|
setCursor2({ range: sourceRange, type: 'default' })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
unsubFn.push(unSubHover, unSubClick)
|
||||||
if (programMemory !== undefined) {
|
if (programMemory !== undefined) {
|
||||||
setProgramMemory(programMemory)
|
setProgramMemory(programMemory)
|
||||||
}
|
}
|
||||||
@ -340,7 +365,10 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
asyncWrap()
|
asyncWrap()
|
||||||
}, [code, isStreamReady])
|
return () => {
|
||||||
|
unsubFn.forEach((fn) => fn())
|
||||||
|
}
|
||||||
|
}, [code, isStreamReady, engineCommandManager])
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager?.sendSceneCommand(message)
|
engineCommandManager?.sendSceneCommand(message)
|
||||||
@ -349,8 +377,11 @@ export function App() {
|
|||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
|
shiftKey,
|
||||||
currentTarget,
|
currentTarget,
|
||||||
|
nativeEvent,
|
||||||
}) => {
|
}) => {
|
||||||
|
nativeEvent.preventDefault()
|
||||||
if (isMouseDownInStream) {
|
if (isMouseDownInStream) {
|
||||||
setDidDragInStream(true)
|
setDidDragInStream(true)
|
||||||
}
|
}
|
||||||
@ -362,7 +393,7 @@ export function App() {
|
|||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
setCmdId(newCmdId)
|
setCmdId(newCmdId)
|
||||||
@ -376,7 +407,6 @@ export function App() {
|
|||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
@ -386,7 +416,6 @@ export function App() {
|
|||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,7 +432,7 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen overflow-hidden relative flex flex-col"
|
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
ref={streamRef}
|
ref={streamRef}
|
||||||
>
|
>
|
||||||
@ -413,6 +442,8 @@ export function App() {
|
|||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
|
project={project}
|
||||||
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<Resizable
|
||||||
@ -491,7 +522,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<Stream className="absolute inset-0 z-0" />
|
||||||
{debugPanel && (
|
{showDebugPanel && (
|
||||||
<DebugPanel
|
<DebugPanel
|
||||||
title="Debug"
|
title="Debug"
|
||||||
className={
|
className={
|
||||||
|
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 Loading from './components/Loading'
|
||||||
import { paths } from './Router'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const { data: user, isLoading } = useSWR<
|
const {
|
||||||
User | Partial<{ error_code: string }>
|
auth: { state },
|
||||||
>(withBaseUrl('/user'), fetcher)
|
} = useGlobalStateContext()
|
||||||
const { token, setUser } = useStore((s) => ({
|
const isLoggedIn = state.matches('checkIfLoggedIn')
|
||||||
token: s.token,
|
|
||||||
setUser: s.setUser,
|
|
||||||
}))
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
useEffect(() => {
|
return isLoggedIn ? (
|
||||||
if (user && 'id' in user) setUser(user)
|
|
||||||
}, [user, setUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(isTauri() && !token) ||
|
|
||||||
(!isTauri() && !isLoading && !(user && 'id' in user))
|
|
||||||
) {
|
|
||||||
navigate(paths.SIGN_IN)
|
|
||||||
}
|
|
||||||
}, [user, token, navigate, isLoading])
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
259
src/Router.tsx
@ -3,8 +3,15 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
|
useLocation,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
matchRoutes,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
useNavigationType,
|
||||||
|
} from 'react-router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
import Onboarding, {
|
import Onboarding, {
|
||||||
@ -13,6 +20,58 @@ import Onboarding, {
|
|||||||
} from './routes/Onboarding'
|
} from './routes/Onboarding'
|
||||||
import SignIn from './routes/SignIn'
|
import SignIn from './routes/SignIn'
|
||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
|
import { isTauri } from './lib/isTauri'
|
||||||
|
import Home from './routes/Home'
|
||||||
|
import { FileEntry, readDir, readTextFile } from '@tauri-apps/api/fs'
|
||||||
|
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||||
|
import {
|
||||||
|
initializeProjectDirectory,
|
||||||
|
isProjectDirectory,
|
||||||
|
PROJECT_ENTRYPOINT,
|
||||||
|
} from './lib/tauriFS'
|
||||||
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsMachine,
|
||||||
|
} from './machines/settingsMachine'
|
||||||
|
import { ContextFrom } from 'xstate'
|
||||||
|
import CommandBarProvider from 'components/CommandBar'
|
||||||
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
|
import * as Sentry from '@sentry/react'
|
||||||
|
|
||||||
|
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: VITE_KC_SENTRY_DSN,
|
||||||
|
// TODO(paultag): pass in the right env here.
|
||||||
|
// environment: "production",
|
||||||
|
integrations: [
|
||||||
|
new Sentry.BrowserTracing({
|
||||||
|
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||||
|
useEffect,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new Sentry.Replay(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for performance monitoring.
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
|
||||||
|
// TODO: Add in kittycad.io endpoints
|
||||||
|
tracePropagationTargets: ['localhost'],
|
||||||
|
|
||||||
|
// Capture Replay for 10% of all sessions,
|
||||||
|
// plus for 100% of sessions with an error
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -26,62 +85,180 @@ const prependRoutes =
|
|||||||
|
|
||||||
export const paths = {
|
export const paths = {
|
||||||
INDEX: '/',
|
INDEX: '/',
|
||||||
|
HOME: '/home',
|
||||||
|
FILE: '/file',
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
SIGN_IN: '/signin',
|
SIGN_IN: '/signin',
|
||||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||||
'/onboarding/'
|
'/onboarding'
|
||||||
) as typeof onboardingPaths,
|
) as typeof onboardingPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
export type IndexLoaderData = {
|
||||||
{
|
code: string | null
|
||||||
path: paths.INDEX,
|
project?: ProjectWithEntryPointMetadata
|
||||||
element: (
|
}
|
||||||
<Auth>
|
|
||||||
<Outlet />
|
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||||
<App />
|
entrypoint_metadata: Metadata
|
||||||
</Auth>
|
}
|
||||||
),
|
export type HomeLoaderData = {
|
||||||
errorElement: <ErrorPage />,
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
loader: ({ request }) => {
|
}
|
||||||
const store = localStorage.getItem('store')
|
|
||||||
if (store === null) {
|
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||||
return redirect(paths.ONBOARDING.INDEX)
|
|
||||||
} else {
|
const addGlobalContextToElements = (
|
||||||
const status = JSON.parse(store).state.onboardingStatus || ''
|
routes: CreateBrowserRouterArg
|
||||||
const notEnRouteToOnboarding =
|
): CreateBrowserRouterArg =>
|
||||||
!request.url.includes(paths.ONBOARDING.INDEX) &&
|
routes.map((route) =>
|
||||||
request.method === 'GET'
|
'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 />
|
||||||
|
<App />
|
||||||
|
{!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
|
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||||
const hasValidOnboardingStatus =
|
const hasValidOnboardingStatus =
|
||||||
(status !== undefined && status.length === 0) ||
|
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||||
!(status === 'done' || status === 'dismissed')
|
|
||||||
const shouldRedirectToOnboarding =
|
const shouldRedirectToOnboarding =
|
||||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
if (shouldRedirectToOnboarding) {
|
||||||
return redirect(paths.ONBOARDING.INDEX + status)
|
return redirect(
|
||||||
|
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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.HOME,
|
||||||
path: paths.SETTINGS,
|
element: (
|
||||||
element: <Settings />,
|
<Auth>
|
||||||
|
<Outlet />
|
||||||
|
<Home />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
loader: async () => {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return redirect(paths.FILE + '/new')
|
||||||
|
}
|
||||||
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
|
ContextFrom<typeof settingsMachine>
|
||||||
|
>
|
||||||
|
const projectDir = await initializeProjectDirectory(
|
||||||
|
persistedSettings.defaultDirectory || ''
|
||||||
|
)
|
||||||
|
if (projectDir !== persistedSettings.defaultDirectory) {
|
||||||
|
localStorage.setItem(
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
...persistedSettings,
|
||||||
|
defaultDirectory: projectDir,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const projectsNoMeta = (await readDir(projectDir)).filter(
|
||||||
|
isProjectDirectory
|
||||||
|
)
|
||||||
|
const projects = await Promise.all(
|
||||||
|
projectsNoMeta.map(async (p) => ({
|
||||||
|
entrypoint_metadata: await metadata(
|
||||||
|
p.path + '/' + PROJECT_ENTRYPOINT
|
||||||
|
),
|
||||||
|
...p,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
children: [
|
||||||
path: paths.ONBOARDING.INDEX,
|
{
|
||||||
element: <Onboarding />,
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
children: onboardingRoutes,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.SIGN_IN,
|
path: paths.SIGN_IN,
|
||||||
element: <SignIn />,
|
element: <SignIn />,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All routes in the app, used in src/index.tsx
|
* All routes in the app, used in src/index.tsx
|
||||||
|
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;
|
||||||
|
}
|
312
src/Toolbar.tsx
@ -11,7 +11,11 @@ import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
|||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||||
import { 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'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const {
|
||||||
@ -30,73 +34,26 @@ export const Toolbar = () => {
|
|||||||
programMemory: s.programMemory,
|
programMemory: s.programMemory,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div>
|
console.log('guiMode', guiMode)
|
||||||
<ExportButton />
|
}, [guiMode])
|
||||||
{guiMode.mode === 'default' && (
|
|
||||||
<button
|
function ToolbarButtons() {
|
||||||
onClick={() => {
|
return (
|
||||||
setGuiMode({
|
<>
|
||||||
mode: 'sketch',
|
{guiMode.mode === 'default' && (
|
||||||
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' && (
|
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ast) return
|
setGuiMode({
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
mode: 'sketch',
|
||||||
ast,
|
sketchMode: 'selectFace',
|
||||||
selectionRanges.codeBasedSelections[0].range
|
})
|
||||||
)
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ExtrudeSketch
|
Start Sketch
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{guiMode.mode === 'canEditExtrude' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ast) return
|
if (!ast) return
|
||||||
@ -104,77 +61,182 @@ export const Toolbar = () => {
|
|||||||
ast,
|
ast,
|
||||||
selectionRanges.codeBasedSelections[0].range
|
selectionRanges.codeBasedSelections[0].range
|
||||||
)
|
)
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
const { modifiedAst } = sketchOnExtrudedFace(
|
||||||
ast,
|
ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
false
|
programMemory
|
||||||
)
|
)
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
updateAst(modifiedAst)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ExtrudeSketch (w/o pipe)
|
SketchOnFace
|
||||||
</button>
|
</button>
|
||||||
</>
|
)}
|
||||||
)}
|
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||||
|
<button
|
||||||
{guiMode.mode === 'sketch' && (
|
onClick={() => {
|
||||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
setGuiMode({
|
||||||
Exit sketch
|
mode: 'sketch',
|
||||||
</button>
|
sketchMode: 'sketchEdit',
|
||||||
)}
|
pathToNode: guiMode.pathToNode,
|
||||||
{toolTips
|
rotation: guiMode.rotation,
|
||||||
.filter(
|
position: guiMode.position,
|
||||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
})
|
||||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
}}
|
||||||
)
|
>
|
||||||
.map((sketchFnName) => {
|
Edit Sketch
|
||||||
if (
|
</button>
|
||||||
guiMode.mode !== 'sketch' ||
|
)}
|
||||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
)
|
<>
|
||||||
return null
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
key={sketchFnName}
|
onClick={() => {
|
||||||
onClick={() =>
|
if (!ast) return
|
||||||
setGuiMode({
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
...guiMode,
|
ast,
|
||||||
...(guiMode.sketchMode === sketchFnName
|
selectionRanges.codeBasedSelections[0].range
|
||||||
? {
|
)
|
||||||
sketchMode: 'sketchEdit',
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
ast,
|
||||||
}
|
pathToNode
|
||||||
: {
|
)
|
||||||
sketchMode: sketchFnName,
|
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||||
isTooltip: true,
|
}}
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{sketchFnName}
|
ExtrudeSketch
|
||||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!ast) return
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections[0].range
|
||||||
|
)
|
||||||
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ExtrudeSketch (w/o pipe)
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{guiMode.mode === 'sketch' && (
|
||||||
|
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||||
|
Exit sketch
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{toolTips
|
||||||
|
.filter(
|
||||||
|
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||||
|
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||||
)
|
)
|
||||||
})}
|
.map((sketchFnName) => {
|
||||||
<br></br>
|
if (
|
||||||
<ConvertToVariable />
|
guiMode.mode !== 'sketch' ||
|
||||||
<HorzVert horOrVert="horizontal" />
|
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||||
<HorzVert horOrVert="vertical" />
|
)
|
||||||
<EqualLength />
|
return null
|
||||||
<EqualAngle />
|
return (
|
||||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
<button
|
||||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
key={sketchFnName}
|
||||||
<SetAbsDistance buttonType="snapToYAxis" />
|
onClick={() =>
|
||||||
<SetAbsDistance buttonType="xAbs" />
|
setGuiMode({
|
||||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
...guiMode,
|
||||||
<SetAbsDistance buttonType="snapToXAxis" />
|
...(guiMode.sketchMode === sketchFnName
|
||||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
? {
|
||||||
<SetAbsDistance buttonType="yAbs" />
|
sketchMode: 'sketchEdit',
|
||||||
<SetAngleLength angleOrLength="setAngle" />
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
<SetAngleLength angleOrLength="setLength" />
|
}
|
||||||
<Intersect />
|
: {
|
||||||
<RemoveConstrainingValues />
|
sketchMode: sketchFnName,
|
||||||
<SetAngleBetween />
|
isTooltip: true,
|
||||||
</div>
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sketchFnName}
|
||||||
|
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ConvertToVariable />
|
||||||
|
<HorzVert horOrVert="horizontal" />
|
||||||
|
<HorzVert horOrVert="vertical" />
|
||||||
|
<EqualLength />
|
||||||
|
<EqualAngle />
|
||||||
|
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||||
|
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||||
|
<SetAbsDistance buttonType="snapToYAxis" />
|
||||||
|
<SetAbsDistance buttonType="xAbs" />
|
||||||
|
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||||
|
<SetAbsDistance buttonType="snapToXAxis" />
|
||||||
|
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||||
|
<SetAbsDistance buttonType="yAbs" />
|
||||||
|
<SetAngleLength angleOrLength="setAngle" />
|
||||||
|
<SetAngleLength angleOrLength="setLength" />
|
||||||
|
<Intersect />
|
||||||
|
<RemoveConstrainingValues />
|
||||||
|
<SetAngleBetween />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import type { LinkProps } from 'react-router-dom'
|
||||||
|
|
||||||
interface ActionButtonProps extends React.PropsWithChildren {
|
interface BaseActionButtonProps {
|
||||||
icon?: ActionIconProps
|
icon?: ActionIconProps
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
|
||||||
to?: string
|
|
||||||
Element?:
|
|
||||||
| 'button'
|
|
||||||
| 'link'
|
|
||||||
| React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionButton = ({
|
type ActionButtonAsButton = BaseActionButtonProps &
|
||||||
icon,
|
Omit<
|
||||||
className,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
onClick,
|
keyof BaseActionButtonProps
|
||||||
to = paths.INDEX,
|
> & {
|
||||||
Element = 'button',
|
Element: 'button'
|
||||||
children,
|
}
|
||||||
...props
|
|
||||||
}: ActionButtonProps) => {
|
|
||||||
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
|
|
||||||
icon ? 'pr-2' : 'px-2'
|
|
||||||
} ${className}`
|
|
||||||
|
|
||||||
if (Element === 'button') {
|
type ActionButtonAsLink = BaseActionButtonProps &
|
||||||
return (
|
Omit<LinkProps, keyof BaseActionButtonProps> & {
|
||||||
<button onClick={onClick} className={classNames} {...props}>
|
Element: 'link'
|
||||||
{icon && <ActionIcon {...icon} />}
|
}
|
||||||
{children}
|
|
||||||
</button>
|
type ActionButtonAsExternal = BaseActionButtonProps &
|
||||||
)
|
Omit<
|
||||||
} else if (Element === 'link') {
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
return (
|
keyof BaseActionButtonProps
|
||||||
<Link to={to} className={classNames} {...props}>
|
> & {
|
||||||
{icon && <ActionIcon {...icon} />}
|
Element: 'externalLink'
|
||||||
{children}
|
}
|
||||||
</Link>
|
|
||||||
)
|
type ActionButtonAsElement = BaseActionButtonProps &
|
||||||
} else {
|
Omit<React.HTMLAttributes<HTMLElement>, keyof BaseActionButtonProps> & {
|
||||||
return (
|
Element: React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
||||||
<Element onClick={onClick} className={classNames} {...props}>
|
}
|
||||||
{icon && <ActionIcon {...icon} />}
|
|
||||||
{children}
|
type ActionButtonProps =
|
||||||
</Element>
|
| 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 {
|
import {
|
||||||
IconDefinition,
|
IconDefinition as SolidIconDefinition,
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
const iconSizes = {
|
const iconSizes = {
|
||||||
sm: 12,
|
sm: 12,
|
||||||
md: 14.4,
|
md: 14.4,
|
||||||
lg: 18,
|
lg: 20,
|
||||||
|
xl: 28,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionIconProps extends React.PropsWithChildren {
|
export interface ActionIconProps extends React.PropsWithChildren {
|
||||||
icon?: IconDefinition
|
icon?: SolidIconDefinition | BrandIconDefinition
|
||||||
|
className?: string
|
||||||
bgClassName?: string
|
bgClassName?: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
size?: keyof typeof iconSizes
|
size?: keyof typeof iconSizes
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionIcon = ({
|
export const ActionIcon = ({
|
||||||
icon,
|
icon = faCircleExclamation,
|
||||||
|
className,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
iconClassName,
|
iconClassName,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@ -27,19 +31,21 @@ export const ActionIcon = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'p-1 w-fit inline-grid place-content-center ' +
|
`p-${
|
||||||
|
size === 'xl' ? '2' : '1'
|
||||||
|
} w-fit inline-grid place-content-center ${className} ` +
|
||||||
(bgClassName ||
|
(bgClassName ||
|
||||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children || (
|
{children || (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon || faCircleExclamation}
|
icon={icon}
|
||||||
width={iconSizes[size]}
|
width={iconSizes[size]}
|
||||||
height={iconSizes[size]}
|
height={iconSizes[size]}
|
||||||
className={
|
className={
|
||||||
iconClassName ||
|
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,40 @@
|
|||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { paths } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import styles from './AppHeader.module.css'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
|
project?: ProjectWithEntryPointMetadata
|
||||||
className?: string
|
className?: string
|
||||||
|
enableMenu?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppHeader = ({
|
export const AppHeader = ({
|
||||||
showToolbar = true,
|
showToolbar = true,
|
||||||
|
project,
|
||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { user } = useStore((s) => ({
|
const {
|
||||||
user: s.user,
|
auth: {
|
||||||
}))
|
context: { user },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={
|
className={
|
||||||
'overlaid-panes sticky top-0 z-10 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
|
(showToolbar ? 'grid ' : 'flex justify-between ') +
|
||||||
|
styles.header +
|
||||||
|
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||||
className
|
className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link to={paths.INDEX}>
|
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||||
<img
|
|
||||||
src="/kitt-arcade-winking.svg"
|
|
||||||
alt="KittyCAD App"
|
|
||||||
className="h-9 w-auto"
|
|
||||||
/>
|
|
||||||
<span className="sr-only">KittyCAD App</span>
|
|
||||||
</Link>
|
|
||||||
{/* Toolbar if the context deems it */}
|
{/* Toolbar if the context deems it */}
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
@ -40,7 +42,11 @@ export const AppHeader = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || <UserSidebarMenu user={user} />}
|
{children || (
|
||||||
|
<div className="ml-auto">
|
||||||
|
<UserSidebarMenu user={user} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
|
||||||
import { executor } from '../lang/executor'
|
import { executor } from '../lang/executor'
|
||||||
import {
|
import {
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
} from '../lang/modifyAst'
|
} from '../lang/modifyAst'
|
||||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||||
import { lexer } from '../lang/tokeniser'
|
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
@ -144,7 +143,7 @@ export function useCalc({
|
|||||||
if (!engineCommandManager) return
|
if (!engineCommandManager) return
|
||||||
try {
|
try {
|
||||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const _programMem: any = { root: {} }
|
const _programMem: any = { root: {} }
|
||||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
.panel {
|
.panel {
|
||||||
@apply relative overflow-auto z-0;
|
@apply relative overflow-auto z-0;
|
||||||
@apply bg-chalkboard-20/40;
|
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .panel {
|
:global(.dark) .panel {
|
||||||
@apply bg-chalkboard-110/50;
|
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
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 overflow-y-auto p-4 pt-[25vh]"
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||||
|
</Transition.Child>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<Combobox
|
||||||
|
value={selectedCommand}
|
||||||
|
onChange={handleCommandSelection}
|
||||||
|
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||||
|
<div>
|
||||||
|
{inSubCommand && (
|
||||||
|
<p className="text-liquid-70 dark:text-liquid-30">
|
||||||
|
{selectedCommand.item &&
|
||||||
|
getDisplayValue(selectedCommand.item as Command)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="bg-transparent focus:outline-none w-full"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k')
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
if (
|
||||||
|
inSubCommand &&
|
||||||
|
event.key === 'Backspace' &&
|
||||||
|
!event.currentTarget.value
|
||||||
|
) {
|
||||||
|
setSubCommandIndex(subCommandIndex - 1)
|
||||||
|
setSelectedCommand(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
displayValue={(command: SortedCommand) =>
|
||||||
|
command !== null ? command.item.name : ''
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
inSubCommand
|
||||||
|
? `Enter <${currentSubCommand?.name}>`
|
||||||
|
: 'Search for a command'
|
||||||
|
}
|
||||||
|
value={query}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
||||||
|
{filteredCommands?.map((commandResult) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={commandResult.item.name}
|
||||||
|
value={commandResult}
|
||||||
|
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
|
||||||
|
>
|
||||||
|
<p>{commandResult.item.name}</p>
|
||||||
|
{(commandResult.item as SubCommand).description && (
|
||||||
|
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||||
|
{(commandResult.item as SubCommand).description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</Transition.Child>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarProvider
|
@ -5,9 +5,10 @@ import { EngineCommand } from '../lang/std/engineConnection'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { isReducedMotion } from 'lang/util'
|
||||||
|
|
||||||
type SketchModeCmd = Extract<
|
type SketchModeCmd = Extract<
|
||||||
EngineCommand['cmd'],
|
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||||
{ type: 'default_camera_enable_sketch_mode' }
|
{ type: 'default_camera_enable_sketch_mode' }
|
||||||
>
|
>
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
|||||||
y_axis: { x: 0, y: 1, z: 0 },
|
y_axis: { x: 0, y: 1, z: 0 },
|
||||||
distance_to_plane: 100,
|
distance_to_plane: 100,
|
||||||
ortho: true,
|
ortho: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
})
|
})
|
||||||
if (!sketchModeCmd) return null
|
if (!sketchModeCmd) return null
|
||||||
return (
|
return (
|
||||||
@ -73,12 +75,12 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: sketchModeCmd,
|
cmd: sketchModeCmd,
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className="hover:border-succeed-50"
|
className="hover:border-succeed-50"
|
||||||
|
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 { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
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 { ActionButton } from './ActionButton'
|
||||||
import Modal from 'react-modal'
|
import Modal from 'react-modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@ -9,7 +9,15 @@ import { Models } from '@kittycad/lib'
|
|||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
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) => ({
|
const { engineCommandManager } = useStore((s) => ({
|
||||||
engineCommandManager: s.engineCommandManager,
|
engineCommandManager: s.engineCommandManager,
|
||||||
}))
|
}))
|
||||||
@ -19,17 +27,6 @@ export const ExportButton = () => {
|
|||||||
const defaultType = 'gltf'
|
const defaultType = 'gltf'
|
||||||
const [type, setType] = React.useState(defaultType)
|
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() {
|
function openModal() {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
@ -42,6 +39,7 @@ export const ExportButton = () => {
|
|||||||
const initialValues: OutputFormat = {
|
const initialValues: OutputFormat = {
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
storage: 'embedded',
|
storage: 'embedded',
|
||||||
|
presentation: 'compact',
|
||||||
}
|
}
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues,
|
initialValues,
|
||||||
@ -79,29 +77,36 @@ export const ExportButton = () => {
|
|||||||
format: values,
|
format: values,
|
||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
closeModal()
|
closeModal()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const yo = formik.values
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={openModal}>Export</button>
|
<ActionButton
|
||||||
|
onClick={openModal}
|
||||||
|
Element="button"
|
||||||
|
icon={{ icon: faFileExport }}
|
||||||
|
className={className?.button}
|
||||||
|
>
|
||||||
|
{children || 'Export'}
|
||||||
|
</ActionButton>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onRequestClose={closeModal}
|
onRequestClose={closeModal}
|
||||||
contentLabel="Export"
|
contentLabel="Export"
|
||||||
style={customModalStyles}
|
overlayClassName="z-40 fixed inset-0 grid place-items-center"
|
||||||
|
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
|
||||||
>
|
>
|
||||||
<div className="text-black">
|
<h1 className="text-2xl font-bold">Export your design</h1>
|
||||||
<h1 className="text-2xl font-bold">Export your design</h1>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
|
||||||
<p>
|
<label htmlFor="type" className="flex-1">
|
||||||
<label htmlFor="type">Type</label>
|
<p className="mb-2">Type</p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
name="type"
|
name="type"
|
||||||
@ -109,6 +114,7 @@ export const ExportButton = () => {
|
|||||||
setType(e.target.value)
|
setType(e.target.value)
|
||||||
formik.handleChange(e)
|
formik.handleChange(e)
|
||||||
}}
|
}}
|
||||||
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
>
|
>
|
||||||
<option value="gltf">gltf</option>
|
<option value="gltf">gltf</option>
|
||||||
<option value="obj">obj</option>
|
<option value="obj">obj</option>
|
||||||
@ -116,56 +122,51 @@ export const ExportButton = () => {
|
|||||||
<option value="step">step</option>
|
<option value="step">step</option>
|
||||||
<option value="stl">stl</option>
|
<option value="stl">stl</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</label>
|
||||||
|
|
||||||
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
||||||
<>
|
<label htmlFor="storage" className="flex-1">
|
||||||
<p>
|
<p className="mb-2">Storage</p>
|
||||||
{' '}
|
<select
|
||||||
<label htmlFor="storage">Storage</label>
|
id="storage"
|
||||||
</p>
|
name="storage"
|
||||||
<p>
|
onChange={formik.handleChange}
|
||||||
<select
|
value={
|
||||||
id="storage"
|
'storage' in formik.values ? formik.values.storage : ''
|
||||||
name="storage"
|
}
|
||||||
onChange={formik.handleChange}
|
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||||
value={formik.values.storage}
|
>
|
||||||
>
|
{type === 'gltf' && (
|
||||||
{type === 'gltf' && (
|
<>
|
||||||
<>
|
<option value="embedded">embedded</option>
|
||||||
<option value="embedded">embedded</option>
|
<option value="binary">binary</option>
|
||||||
<option value="binary">binary</option>
|
<option value="standard">standard</option>
|
||||||
<option value="standard">standard</option>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
{type === 'ply' && (
|
||||||
{type === 'ply' && (
|
<>
|
||||||
<>
|
<option value="ascii">ascii</option>
|
||||||
<option value="ascii">ascii</option>
|
<option value="binary">binary</option>
|
||||||
<option value="binary">binary</option>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
{type === 'stl' && (
|
||||||
{type === 'stl' && (
|
<>
|
||||||
<>
|
<option value="ascii">ascii</option>
|
||||||
<option value="ascii">ascii</option>
|
<option value="binary_little_endian">
|
||||||
<option value="binary_little_endian">
|
binary_little_endian
|
||||||
binary_little_endian
|
</option>
|
||||||
</option>
|
<option value="binary_big_endian">
|
||||||
<option value="binary_big_endian">
|
binary_big_endian
|
||||||
binary_big_endian
|
</option>
|
||||||
</option>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</select>
|
||||||
</select>
|
</label>
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faXmark,
|
icon: faXmark,
|
||||||
@ -177,8 +178,15 @@ export const ExportButton = () => {
|
|||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
icon={{ icon: faFileExport }}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
158
src/components/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 ReactJson from 'react-json-view'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Themes, useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
const ReactJsonTypeHack = ReactJson as any
|
const ReactJsonTypeHack = ReactJson as any
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { processMemory } from './MemoryPanel'
|
import { processMemory } from './MemoryPanel'
|
||||||
import { lexer } from '../lang/tokeniser'
|
import { parser_wasm } from '../lang/abstractSyntaxTree'
|
||||||
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
|
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { initPromise } from '../lang/rust'
|
import { initPromise } from '../lang/rust'
|
||||||
|
|
||||||
@ -15,45 +14,34 @@ describe('processMemory', () => {
|
|||||||
return a - 2
|
return a - 2
|
||||||
}
|
}
|
||||||
const otherVar = myFn(5)
|
const otherVar = myFn(5)
|
||||||
|
|
||||||
const theExtrude = startSketchAt([0, 0])
|
const theExtrude = startSketchAt([0, 0])
|
||||||
|> lineTo([-2.4, myVar], %)
|
|> lineTo([-2.4, myVar], %)
|
||||||
|> lineTo([-0.76, otherVar], %)
|
|> lineTo([-0.76, otherVar], %)
|
||||||
|> extrude(4, %)
|
|> extrude(4, %)
|
||||||
|
|
||||||
const theSketch = startSketchAt([0, 0])
|
const theSketch = startSketchAt([0, 0])
|
||||||
|> lineTo([-3.35, 0.17], %)
|
|> lineTo([-3.35, 0.17], %)
|
||||||
|> lineTo([0.98, 5.16], %)
|
|> lineTo([0.98, 5.16], %)
|
||||||
|> lineTo([2.15, 4.32], %)
|
|> lineTo([2.15, 4.32], %)
|
||||||
// |> rx(90, %)
|
// |> rx(90, %)
|
||||||
show(theExtrude, theSketch)`
|
show(theExtrude, theSketch)`
|
||||||
const tokens = lexer(code)
|
const ast = parser_wasm(code)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
|
||||||
const programMemory = await enginelessExecutor(ast, {
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
root: {
|
root: {},
|
||||||
log: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: (a: any) => {
|
|
||||||
console.log('raw log', a)
|
|
||||||
},
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pendingMemory: {},
|
|
||||||
})
|
})
|
||||||
const output = processMemory(programMemory)
|
const output = processMemory(programMemory)
|
||||||
expect(output.myVar).toEqual(5)
|
expect(output.myVar).toEqual(5)
|
||||||
expect(output.myFn).toEqual('__function__')
|
|
||||||
expect(output.otherVar).toEqual(3)
|
expect(output.otherVar).toEqual(3)
|
||||||
expect(output).toEqual({
|
expect(output).toEqual({
|
||||||
myVar: 5,
|
myVar: 5,
|
||||||
myFn: '__function__',
|
myFn: undefined,
|
||||||
otherVar: 3,
|
otherVar: 3,
|
||||||
theExtrude: [],
|
theExtrude: [],
|
||||||
theSketch: [
|
theSketch: [
|
||||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
|
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
|
||||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
|
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
|
||||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
|
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { Themes, useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory } from '../lang/executor'
|
import { ProgramMemory } from '../lang/executor'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
theme?: Exclude<Themes, Themes.System>
|
theme?: Exclude<Themes, Themes.System>
|
||||||
|
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,5 +1,5 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||||
|
|
||||||
export const SetVarNameModal = ({
|
export const SetVarNameModal = ({
|
||||||
|
@ -7,21 +7,16 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { throttle } from '../lib/utils'
|
|
||||||
import { EngineCommand } from '../lang/std/engineConnection'
|
|
||||||
import { getNormalisedCoordinates } from '../lib/utils'
|
import { getNormalisedCoordinates } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }) => {
|
export const Stream = ({ className = '' }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [zoom, setZoom] = useState(0)
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const {
|
const {
|
||||||
mediaStream,
|
mediaStream,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
setIsMouseDownInStream,
|
setIsMouseDownInStream,
|
||||||
fileId,
|
|
||||||
setFileId,
|
|
||||||
setCmdId,
|
setCmdId,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
@ -32,7 +27,6 @@ export const Stream = ({ className = '' }) => {
|
|||||||
isMouseDownInStream: s.isMouseDownInStream,
|
isMouseDownInStream: s.isMouseDownInStream,
|
||||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
||||||
fileId: s.fileId,
|
fileId: s.fileId,
|
||||||
setFileId: s.setFileId,
|
|
||||||
setCmdId: s.setCmdId,
|
setCmdId: s.setCmdId,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
@ -48,9 +42,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (!mediaStream) return
|
if (!mediaStream) return
|
||||||
videoRef.current.srcObject = mediaStream
|
videoRef.current.srcObject = mediaStream
|
||||||
setFileId(uuidv4())
|
}, [mediaStream, engineCommandManager])
|
||||||
setZoom(videoRef.current.getBoundingClientRect().height / 2)
|
|
||||||
}, [mediaStream, engineCommandManager, setFileId])
|
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
||||||
clientX,
|
clientX,
|
||||||
@ -79,31 +71,21 @@ export const Stream = ({ className = '' }) => {
|
|||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newId,
|
cmd_id: newId,
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsMouseDownInStream(true)
|
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) => {
|
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
debounceSocketSend({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_move',
|
type: 'default_camera_zoom',
|
||||||
interaction: 'zoom',
|
magnitude: e.deltaY * 0.4,
|
||||||
window: { x: 0, y: zoom + e.deltaY },
|
|
||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setZoom(zoom + e.deltaY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
||||||
@ -130,7 +112,6 @@ export const Stream = ({ className = '' }) => {
|
|||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsMouseDownInStream(false)
|
setIsMouseDownInStream(false)
|
||||||
@ -143,7 +124,6 @@ export const Stream = ({ className = '' }) => {
|
|||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
file_id: fileId,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setDidDragInStream(false)
|
setDidDragInStream(false)
|
||||||
@ -160,7 +140,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
onWheelCapture={handleScroll}
|
onWheel={handleScroll}
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
/>
|
/>
|
||||||
|
@ -13,13 +13,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle > span {
|
.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)));
|
width: calc(2 * (var(--toggle-size) + var(--padding)));
|
||||||
height: calc(var(--toggle-size) + var(--padding));
|
height: calc(var(--toggle-size) + var(--padding));
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .toggle > span {
|
:global(.dark) .toggle > span {
|
||||||
@apply border-chalkboard-40;
|
@apply border-chalkboard-40 hover:border-chalkboard-30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle > span::after {
|
.toggle > span::after {
|
||||||
|
@ -1,66 +1,103 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { User } from '../useStore'
|
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
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", () => {
|
type User = Models['User_type']
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
render(
|
describe('UserSidebarMenu tests', () => {
|
||||||
<BrowserRouter>
|
test("Renders user's name and email if available", () => {
|
||||||
<UserSidebarMenu user={userWellFormed} />
|
const userWellFormed: User = {
|
||||||
</BrowserRouter>
|
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(
|
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||||
userWellFormed.name || ''
|
|
||||||
)
|
expect(screen.getByTestId('username')).toHaveTextContent(
|
||||||
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email)
|
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", () => {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
const userNoName: User = {
|
// wrap in router and xState context
|
||||||
id: '8675309',
|
return (
|
||||||
email: 'kittycad.sidebar.test@example.com',
|
|
||||||
image: 'https://placekitten.com/200/200',
|
|
||||||
created_at: 'yesteryear',
|
|
||||||
updated_at: 'today',
|
|
||||||
}
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<UserSidebarMenu user={userNoName} />
|
<CommandBarProvider>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</CommandBarProvider>
|
||||||
</BrowserRouter>
|
</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 { Popover, Transition } from '@headlessui/react'
|
||||||
import { User, useStore } from '../useStore'
|
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
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 { useNavigate } from 'react-router-dom'
|
||||||
import { useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
|
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
|
||||||
|
type User = Models['User_type']
|
||||||
|
|
||||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||||
const displayedName = getDisplayName(user)
|
const displayedName = getDisplayName(user)
|
||||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setToken } = useStore((s) => ({
|
const {
|
||||||
setToken: s.setToken,
|
auth: { send },
|
||||||
}))
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
// Fallback logic for displaying user's "name":
|
// Fallback logic for displaying user's "name":
|
||||||
// 1. user.name
|
// 1. user.name
|
||||||
@ -33,10 +38,10 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{user?.image && !imageLoadFailed ? (
|
{user?.image && !imageLoadFailed ? (
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="border-0 rounded-full w-fit p-0"
|
className="border-0 rounded-full w-fit p-0 focus:outline-none group"
|
||||||
data-testid="user-sidebar-toggle"
|
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
|
<img
|
||||||
src={user?.image || ''}
|
src={user?.image || ''}
|
||||||
alt={user?.name || ''}
|
alt={user?.name || ''}
|
||||||
@ -56,76 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
Menu
|
Menu
|
||||||
</ActionButton>
|
</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">
|
<Transition
|
||||||
{({ close }) => (
|
enter="duration-100 ease-out"
|
||||||
<>
|
enterFrom="opacity-0 translate-x-1/4"
|
||||||
{user && (
|
enterTo="opacity-100 translate-x-0"
|
||||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
leave="duration-75 ease-in"
|
||||||
{user.image && !imageLoadFailed && (
|
leaveFrom="opacity-100 translate-x-0"
|
||||||
<div className="rounded-full shadow-inner overflow-hidden">
|
leaveTo="opacity-0 translate-x-4"
|
||||||
<img
|
as={Fragment}
|
||||||
src={user.image}
|
>
|
||||||
alt={user.name || ''}
|
<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">
|
||||||
className="h-8 w-8"
|
{({ close }) => (
|
||||||
referrerPolicy="no-referrer"
|
<>
|
||||||
onError={() => setImageLoadFailed(true)}
|
{user && (
|
||||||
/>
|
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||||
</div>
|
{user.image && !imageLoadFailed && (
|
||||||
)}
|
<div className="rounded-full shadow-inner overflow-hidden">
|
||||||
|
<img
|
||||||
<div>
|
src={user.image}
|
||||||
<p
|
alt={user.name || ''}
|
||||||
className="m-0 text-liquid-10 text-mono"
|
className="h-8 w-8"
|
||||||
data-testid="username"
|
referrerPolicy="no-referrer"
|
||||||
>
|
onError={() => setImageLoadFailed(true)}
|
||||||
{displayedName || ''}
|
/>
|
||||||
</p>
|
</div>
|
||||||
{displayedName !== user.email && (
|
|
||||||
<p
|
|
||||||
className="m-0 text-liquid-40 text-xs"
|
|
||||||
data-testid="email"
|
|
||||||
>
|
|
||||||
{user.email}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
)}
|
</>
|
||||||
<div className="p-4 flex flex-col gap-2">
|
)}
|
||||||
<ActionButton
|
</Popover.Panel>
|
||||||
icon={{ icon: faGear }}
|
</Transition>
|
||||||
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>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,9 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
|||||||
.VITE_KC_API_WS_MODELING_URL
|
.VITE_KC_API_WS_MODELING_URL
|
||||||
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
||||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||||
|
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
|
||||||
|
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
|
||||||
|
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||||
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
|
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||||
export const TEST = import.meta.env.TEST
|
export const TEST = import.meta.env.TEST
|
||||||
|
6
src/hooks/useCommandsContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { CommandsContext } from 'components/CommandBar'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useCommandsContext = () => {
|
||||||
|
return useContext(CommandsContext)
|
||||||
|
}
|
6
src/hooks/useGlobalStateContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { GlobalStateContext } from 'components/GlobalStateProvider'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useGlobalStateContext = () => {
|
||||||
|
return useContext(GlobalStateContext)
|
||||||
|
}
|
42
src/hooks/useStateMachineCommands.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||||
|
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
||||||
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
|
|
||||||
|
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||||
|
state: StateFrom<T>
|
||||||
|
send: Function
|
||||||
|
commandBarMeta?: CommandBarMeta
|
||||||
|
commands: Command[]
|
||||||
|
owner: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commandBarMeta,
|
||||||
|
owner,
|
||||||
|
}: UseStateMachineCommandsArgs<T>) {
|
||||||
|
const { addCommands, removeCommands } = useCommandsContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newCommands = state.nextEvents
|
||||||
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
|
.map((type) =>
|
||||||
|
createMachineCommand<T>({
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commandBarMeta,
|
||||||
|
owner,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((c) => c !== null) as Command[]
|
||||||
|
|
||||||
|
addCommands(newCommands)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeCommands(newCommands)
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { parse } from 'toml'
|
|
||||||
import {
|
|
||||||
createDir,
|
|
||||||
BaseDirectory,
|
|
||||||
readDir,
|
|
||||||
readTextFile,
|
|
||||||
} from '@tauri-apps/api/fs'
|
|
||||||
|
|
||||||
export const useTauriBoot = () => {
|
|
||||||
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
|
|
||||||
defaultDir: s.defaultDir,
|
|
||||||
setDefaultDir: s.setDefaultDir,
|
|
||||||
setHomeMenuItems: s.setHomeMenuItems,
|
|
||||||
}))
|
|
||||||
useEffect(() => {
|
|
||||||
const isTauri = (window as any).__TAURI__
|
|
||||||
if (!isTauri) return
|
|
||||||
const run = async () => {
|
|
||||||
if (!defaultDir.base) {
|
|
||||||
createDir('puffin-projects/example', {
|
|
||||||
dir: BaseDirectory.Home,
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
setDefaultDir({
|
|
||||||
base: BaseDirectory.Home,
|
|
||||||
dir: 'puffin-projects',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const directoryResult = await readDir(defaultDir.dir, {
|
|
||||||
dir: defaultDir.base,
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
const puffinProjects = directoryResult.filter(
|
|
||||||
(file) =>
|
|
||||||
!file?.name?.startsWith('.') &&
|
|
||||||
file?.children?.find((child) => child?.name === 'wax.toml')
|
|
||||||
)
|
|
||||||
|
|
||||||
const tomlFiles = await Promise.all(
|
|
||||||
puffinProjects.map(async (file) => {
|
|
||||||
const parsedToml = parse(
|
|
||||||
await readTextFile(`${file.path}/wax.toml`, {
|
|
||||||
dir: defaultDir.base,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const mainPath = parsedToml?.package?.main
|
|
||||||
const projectName = parsedToml?.package?.name
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
mainPath,
|
|
||||||
projectName,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setHomeMenuItems(
|
|
||||||
tomlFiles.map(({ file, mainPath, projectName }) => ({
|
|
||||||
name: projectName,
|
|
||||||
path: mainPath ? `${file.path}/${mainPath}` : file.path,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run()
|
|
||||||
}, [])
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ body.dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@apply w-2 rounded-sm;
|
@apply w-2 h-2 rounded-sm;
|
||||||
@apply bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,8 +86,18 @@ code {
|
|||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#code-mirror-override .cm-activeLine,
|
||||||
|
#code-mirror-override .cm-activeLineGutter {
|
||||||
|
@apply bg-liquid-10/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #code-mirror-override .cm-activeLine,
|
||||||
|
.dark #code-mirror-override .cm-activeLineGutter {
|
||||||
|
@apply bg-liquid-80/50;
|
||||||
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-gutters {
|
#code-mirror-override .cm-gutters {
|
||||||
@apply bg-chalkboard-10/50;
|
@apply bg-chalkboard-10/30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark #code-mirror-override .cm-gutters {
|
.dark #code-mirror-override .cm-gutters {
|
||||||
@ -99,14 +109,24 @@ code {
|
|||||||
}
|
}
|
||||||
#code-mirror-override .cm-cursor {
|
#code-mirror-override .cm-cursor {
|
||||||
display: block;
|
display: block;
|
||||||
width: 200px;
|
width: 1ch;
|
||||||
background: linear-gradient(
|
@apply bg-liquid-40 mix-blend-multiply;
|
||||||
to right,
|
|
||||||
rgb(0, 55, 94) 0%,
|
animation: blink 2s ease-out infinite;
|
||||||
#0084e2ff 2%,
|
}
|
||||||
#0084e255 5%,
|
|
||||||
transparent 100%
|
.dark #code-mirror-override .cm-cursor {
|
||||||
);
|
@apply bg-liquid-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
15% {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-json-view {
|
.react-json-view {
|
||||||
|
@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import reportWebVitals from './reportWebVitals'
|
import reportWebVitals from './reportWebVitals'
|
||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { Themes, useStore } from './useStore'
|
|
||||||
import { Router } from './Router'
|
import { Router } from './Router'
|
||||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||||
import { getSystemTheme } from './lib/getSystemTheme'
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
function setThemeClass(state: Partial<{ theme: Themes }>) {
|
|
||||||
const systemTheme = state.theme === Themes.System && getSystemTheme()
|
|
||||||
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
|
|
||||||
document.body.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('dark')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { theme } = useStore.getState()
|
|
||||||
setThemeClass({ theme })
|
|
||||||
useStore.subscribe(setThemeClass)
|
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<HotkeysProvider>
|
<HotkeysProvider>
|
||||||
|
@ -1,74 +1,43 @@
|
|||||||
import {
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
abstractSyntaxTree,
|
import { KCLUnexpectedError } from './errors'
|
||||||
findClosingBrace,
|
|
||||||
hasPipeOperator,
|
|
||||||
findEndOfBinaryExpression,
|
|
||||||
} from './abstractSyntaxTree'
|
|
||||||
import { lexer } from './tokeniser'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
describe('findClosingBrace', () => {
|
|
||||||
test('finds the closing brace', () => {
|
|
||||||
const basic = '( hey )'
|
|
||||||
expect(findClosingBrace(lexer(basic), 0)).toBe(4)
|
|
||||||
|
|
||||||
const handlesNonZeroIndex =
|
|
||||||
'(indexForBracketToRightOfThisIsTwo(shouldBeFour)AndNotThisSix)'
|
|
||||||
expect(findClosingBrace(lexer(handlesNonZeroIndex), 2)).toBe(4)
|
|
||||||
expect(findClosingBrace(lexer(handlesNonZeroIndex), 0)).toBe(6)
|
|
||||||
|
|
||||||
const handlesNested =
|
|
||||||
'{a{b{c(}d]}eathou athoeu tah u} thatOneToTheLeftIsLast }'
|
|
||||||
expect(findClosingBrace(lexer(handlesNested), 0)).toBe(18)
|
|
||||||
|
|
||||||
// throws when not started on a brace
|
|
||||||
expect(() => findClosingBrace(lexer(handlesNested), 1)).toThrow()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('testing AST', () => {
|
describe('testing AST', () => {
|
||||||
test('5 + 6', () => {
|
test('5 + 6', () => {
|
||||||
const tokens = lexer('5 +6')
|
const result = parser_wasm('5 +6')
|
||||||
const result = abstractSyntaxTree(tokens)
|
|
||||||
delete (result as any).nonCodeMeta
|
delete (result as any).nonCodeMeta
|
||||||
expect(result).toEqual({
|
expect(result.body).toEqual([
|
||||||
type: 'Program',
|
{
|
||||||
start: 0,
|
type: 'ExpressionStatement',
|
||||||
end: 4,
|
start: 0,
|
||||||
body: [
|
end: 4,
|
||||||
{
|
expression: {
|
||||||
type: 'ExpressionStatement',
|
type: 'BinaryExpression',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 4,
|
end: 4,
|
||||||
expression: {
|
left: {
|
||||||
type: 'BinaryExpression',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
|
end: 1,
|
||||||
|
value: 5,
|
||||||
|
raw: '5',
|
||||||
|
},
|
||||||
|
operator: '+',
|
||||||
|
right: {
|
||||||
|
type: 'Literal',
|
||||||
|
start: 3,
|
||||||
end: 4,
|
end: 4,
|
||||||
left: {
|
value: 6,
|
||||||
type: 'Literal',
|
raw: '6',
|
||||||
start: 0,
|
|
||||||
end: 1,
|
|
||||||
value: 5,
|
|
||||||
raw: '5',
|
|
||||||
},
|
|
||||||
operator: '+',
|
|
||||||
right: {
|
|
||||||
type: 'Literal',
|
|
||||||
start: 3,
|
|
||||||
end: 4,
|
|
||||||
value: 6,
|
|
||||||
raw: '6',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
})
|
])
|
||||||
})
|
})
|
||||||
test('const myVar = 5', () => {
|
test('const myVar = 5', () => {
|
||||||
const tokens = lexer('const myVar = 5')
|
const { body } = parser_wasm('const myVar = 5')
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -102,8 +71,7 @@ describe('testing AST', () => {
|
|||||||
const code = `const myVar = 5
|
const code = `const myVar = 5
|
||||||
const newVar = myVar + 1
|
const newVar = myVar + 1
|
||||||
`
|
`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -173,8 +141,7 @@ const newVar = myVar + 1
|
|||||||
})
|
})
|
||||||
test('using std function "log"', () => {
|
test('using std function "log"', () => {
|
||||||
const code = `log(5, "hello", aIdentifier)`
|
const code = `log(5, "hello", aIdentifier)`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'ExpressionStatement',
|
type: 'ExpressionStatement',
|
||||||
@ -221,8 +188,7 @@ const newVar = myVar + 1
|
|||||||
|
|
||||||
describe('testing function declaration', () => {
|
describe('testing function declaration', () => {
|
||||||
test('fn funcN = () => {}', () => {
|
test('fn funcN = () => {}', () => {
|
||||||
const tokens = lexer('fn funcN = () => {}')
|
const { body } = parser_wasm('fn funcN = () => {}')
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -248,7 +214,6 @@ describe('testing function declaration', () => {
|
|||||||
id: null,
|
id: null,
|
||||||
params: [],
|
params: [],
|
||||||
body: {
|
body: {
|
||||||
type: 'BlockStatement',
|
|
||||||
start: 17,
|
start: 17,
|
||||||
end: 19,
|
end: 19,
|
||||||
body: [],
|
body: [],
|
||||||
@ -260,10 +225,9 @@ describe('testing function declaration', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
test('fn funcN = (a, b) => {return a + b}', () => {
|
test('fn funcN = (a, b) => {return a + b}', () => {
|
||||||
const tokens = lexer(
|
const { body } = parser_wasm(
|
||||||
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
|
['fn funcN = (a, b) => {', ' return a + b', '}'].join('\n')
|
||||||
)
|
)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -302,7 +266,6 @@ describe('testing function declaration', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
body: {
|
body: {
|
||||||
type: 'BlockStatement',
|
|
||||||
start: 21,
|
start: 21,
|
||||||
end: 39,
|
end: 39,
|
||||||
body: [
|
body: [
|
||||||
@ -338,11 +301,9 @@ describe('testing function declaration', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
test('call expression assignment', () => {
|
test('call expression assignment', () => {
|
||||||
const tokens = lexer(
|
const code = `fn funcN = (a, b) => { return a + b }
|
||||||
`fn funcN = (a, b) => { return a + b }
|
|
||||||
const myVar = funcN(1, 2)`
|
const myVar = funcN(1, 2)`
|
||||||
)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -381,7 +342,6 @@ const myVar = funcN(1, 2)`
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
body: {
|
body: {
|
||||||
type: 'BlockStatement',
|
|
||||||
start: 21,
|
start: 21,
|
||||||
end: 37,
|
end: 37,
|
||||||
body: [
|
body: [
|
||||||
@ -465,99 +425,15 @@ const myVar = funcN(1, 2)`
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing hasPipeOperator', () => {
|
|
||||||
test('hasPipeOperator is true', () => {
|
|
||||||
let code = `sketch mySketch {
|
|
||||||
lineTo(2, 3)
|
|
||||||
} |> rx(45, %)
|
|
||||||
`
|
|
||||||
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const result = hasPipeOperator(tokens, 0)
|
|
||||||
delete (result as any).bonusNonCodeNode
|
|
||||||
expect(result).toEqual({
|
|
||||||
index: 16,
|
|
||||||
token: { end: 37, start: 35, type: 'operator', value: '|>' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
test('matches the first pipe', () => {
|
|
||||||
let code = `sketch mySketch {
|
|
||||||
lineTo(2, 3)
|
|
||||||
} |> rx(45, %) |> rx(45, %)
|
|
||||||
`
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const result = hasPipeOperator(tokens, 0)
|
|
||||||
delete (result as any).bonusNonCodeNode
|
|
||||||
expect(result).toEqual({
|
|
||||||
index: 16,
|
|
||||||
token: { end: 37, start: 35, type: 'operator', value: '|>' },
|
|
||||||
})
|
|
||||||
if (!result) throw new Error('should not happen')
|
|
||||||
expect(code.slice(result.token.start, result.token.end)).toEqual('|>')
|
|
||||||
})
|
|
||||||
test('hasPipeOperator is false when the pipe operator is after a new variable declaration', () => {
|
|
||||||
let code = `sketch mySketch {
|
|
||||||
lineTo(2, 3)
|
|
||||||
}
|
|
||||||
const yo = myFunc(9()
|
|
||||||
|> rx(45, %)
|
|
||||||
`
|
|
||||||
const tokens = lexer(code)
|
|
||||||
expect(hasPipeOperator(tokens, 0)).toEqual(false)
|
|
||||||
})
|
|
||||||
test('hasPipeOperator with binary expression', () => {
|
|
||||||
let code = `const myVar2 = 5 + 1 |> myFn(%)`
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const result = hasPipeOperator(tokens, 1)
|
|
||||||
delete (result as any).bonusNonCodeNode
|
|
||||||
expect(result).toEqual({
|
|
||||||
index: 12,
|
|
||||||
token: { end: 23, start: 21, type: 'operator', value: '|>' },
|
|
||||||
})
|
|
||||||
if (!result) throw new Error('should not happen')
|
|
||||||
expect(code.slice(result.token.start, result.token.end)).toEqual('|>')
|
|
||||||
})
|
|
||||||
test('hasPipeOperator of called mid sketchExpression on a callExpression, and called at the start of the sketchExpression at "{"', () => {
|
|
||||||
const code = [
|
|
||||||
'sketch mySk1 {',
|
|
||||||
' lineTo(1,1)',
|
|
||||||
' path myPath = lineTo(0, 1)',
|
|
||||||
' lineTo(1,1)',
|
|
||||||
'} |> rx(90, %)',
|
|
||||||
'show(mySk1)',
|
|
||||||
].join('\n')
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const tokenWithMyPathIndex = tokens.findIndex(
|
|
||||||
({ value }) => value === 'myPath'
|
|
||||||
)
|
|
||||||
const tokenWithLineToIndexForVarDecIndex = tokens.findIndex(
|
|
||||||
({ value }, index) => value === 'lineTo' && index > tokenWithMyPathIndex
|
|
||||||
)
|
|
||||||
const result = hasPipeOperator(tokens, tokenWithLineToIndexForVarDecIndex)
|
|
||||||
expect(result).toBe(false)
|
|
||||||
|
|
||||||
const braceTokenIndex = tokens.findIndex(({ value }) => value === '{')
|
|
||||||
const result2 = hasPipeOperator(tokens, braceTokenIndex)
|
|
||||||
delete (result2 as any).bonusNonCodeNode
|
|
||||||
expect(result2).toEqual({
|
|
||||||
index: 36,
|
|
||||||
token: { end: 76, start: 74, type: 'operator', value: '|>' },
|
|
||||||
})
|
|
||||||
if (!result2) throw new Error('should not happen')
|
|
||||||
expect(code.slice(result2?.token?.start, result2?.token?.end)).toEqual('|>')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('testing pipe operator special', () => {
|
describe('testing pipe operator special', () => {
|
||||||
test('pipe operator with sketch', () => {
|
test('pipe operator with sketch', () => {
|
||||||
let code = `const mySketch = startSketchAt([0, 0])
|
let code = `const mySketch = startSketchAt([0, 0])
|
||||||
|> lineTo([2, 3], %)
|
|> lineTo([2, 3], %)
|
||||||
|> lineTo({ to: [0, 1], tag: "myPath" }, %)
|
|> lineTo({ to: [0, 1], tag: "myPath" }, %)
|
||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
} |> rx(45, %)
|
|> rx(45, %)
|
||||||
`
|
`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
delete (body[0] as any).declarations[0].init.nonCodeMeta
|
delete (body[0] as any).declarations[0].init.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -786,8 +662,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('pipe operator with binary expression', () => {
|
test('pipe operator with binary expression', () => {
|
||||||
let code = `const myVar = 5 + 6 |> myFunc(45, %)`
|
let code = `const myVar = 5 + 6 |> myFunc(45, %)`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
delete (body as any)[0].declarations[0].init.nonCodeMeta
|
delete (body as any)[0].declarations[0].init.nonCodeMeta
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
@ -866,8 +741,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('array expression', () => {
|
test('array expression', () => {
|
||||||
let code = `const yo = [1, '2', three, 4 + 5]`
|
let code = `const yo = [1, '2', three, 4 + 5]`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -942,8 +816,7 @@ describe('testing pipe operator special', () => {
|
|||||||
'const three = 3',
|
'const three = 3',
|
||||||
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
"const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}",
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1087,8 +960,7 @@ describe('testing pipe operator special', () => {
|
|||||||
const code = `const yo = {key: {
|
const code = `const yo = {key: {
|
||||||
key2: 'value'
|
key2: 'value'
|
||||||
}}`
|
}}`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1156,8 +1028,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object expression with array ast', () => {
|
test('object expression with array ast', () => {
|
||||||
const code = `const yo = {key: [1, '2']}`
|
const code = `const yo = {key: [1, '2']}`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1221,8 +1092,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression simple', () => {
|
test('object memberExpression simple', () => {
|
||||||
const code = `const prop = yo.one.two`
|
const code = `const prop = yo.one.two`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1277,8 +1147,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression with square braces', () => {
|
test('object memberExpression with square braces', () => {
|
||||||
const code = `const prop = yo.one["two"]`
|
const code = `const prop = yo.one["two"]`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1334,8 +1203,7 @@ describe('testing pipe operator special', () => {
|
|||||||
})
|
})
|
||||||
test('object memberExpression with two square braces literal and identifier', () => {
|
test('object memberExpression with two square braces literal and identifier', () => {
|
||||||
const code = `const prop = yo["one"][two]`
|
const code = `const prop = yo["one"][two]`
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([
|
||||||
{
|
{
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
@ -1394,7 +1262,7 @@ describe('testing pipe operator special', () => {
|
|||||||
describe('nests binary expressions correctly', () => {
|
describe('nests binary expressions correctly', () => {
|
||||||
it('works with the simple case', () => {
|
it('works with the simple case', () => {
|
||||||
const code = `const yo = 1 + 2`
|
const code = `const yo = 1 + 2`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1438,7 +1306,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
it('should nest according to precedence with multiply first', () => {
|
it('should nest according to precedence with multiply first', () => {
|
||||||
// should be binExp { binExp { lit-1 * lit-2 } + lit}
|
// should be binExp { binExp { lit-1 * lit-2 } + lit}
|
||||||
const code = `const yo = 1 * 2 + 3`
|
const code = `const yo = 1 * 2 + 3`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1495,7 +1363,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
it('should nest according to precedence with sum first', () => {
|
it('should nest according to precedence with sum first', () => {
|
||||||
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
|
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
|
||||||
const code = `const yo = 1 + 2 * 3`
|
const code = `const yo = 1 + 2 * 3`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
expect(body[0]).toEqual({
|
expect(body[0]).toEqual({
|
||||||
type: 'VariableDeclaration',
|
type: 'VariableDeclaration',
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -1551,7 +1419,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with two opperators of equal precedence', () => {
|
it('should nest properly with two opperators of equal precedence', () => {
|
||||||
const code = `const yo = 1 + 2 - 3`
|
const code = `const yo = 1 + 2 - 3`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
expect((body[0] as any).declarations[0].init).toEqual({
|
expect((body[0] as any).declarations[0].init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
@ -1588,7 +1456,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with two opperators of equal (but higher) precedence', () => {
|
it('should nest properly with two opperators of equal (but higher) precedence', () => {
|
||||||
const code = `const yo = 1 * 2 / 3`
|
const code = `const yo = 1 * 2 / 3`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
expect((body[0] as any).declarations[0].init).toEqual({
|
expect((body[0] as any).declarations[0].init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
@ -1625,7 +1493,7 @@ describe('nests binary expressions correctly', () => {
|
|||||||
})
|
})
|
||||||
it('should nest properly with longer example', () => {
|
it('should nest properly with longer example', () => {
|
||||||
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
|
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
const init = (body[0] as any).declarations[0].init
|
const init = (body[0] as any).declarations[0].init
|
||||||
expect(init).toEqual({
|
expect(init).toEqual({
|
||||||
type: 'BinaryExpression',
|
type: 'BinaryExpression',
|
||||||
@ -1684,13 +1552,13 @@ const key = 'c'`
|
|||||||
end: code.indexOf('const key'),
|
end: code.indexOf('const key'),
|
||||||
value: '\n// this is a comment\n',
|
value: '\n// this is a comment\n',
|
||||||
}
|
}
|
||||||
const { nonCodeMeta } = abstractSyntaxTree(lexer(code))
|
const { nonCodeMeta } = parser_wasm(code)
|
||||||
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||||
|
|
||||||
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
||||||
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
||||||
const { nonCodeMeta: nonCodeMeta2 } = abstractSyntaxTree(
|
const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
||||||
lexer(codeWithExtraStartWhitespace)
|
codeWithExtraStartWhitespace
|
||||||
)
|
)
|
||||||
expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
|
expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
|
||||||
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
||||||
@ -1700,23 +1568,22 @@ const key = 'c'`
|
|||||||
it('comments nested within a block statement', () => {
|
it('comments nested within a block statement', () => {
|
||||||
const code = `const mySketch = startSketchAt([0,0])
|
const code = `const mySketch = startSketchAt([0,0])
|
||||||
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|
||||||
|> lineTo([1, 1], %) /* this is
|
|> lineTo([1, 1], %) /* this is
|
||||||
a comment
|
a comment
|
||||||
spanning a few lines */
|
spanning a few lines */
|
||||||
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
`
|
`
|
||||||
|
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
const indexOfSecondLineToExpression = 2
|
const indexOfSecondLineToExpression = 2
|
||||||
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
||||||
.noneCodeNodes
|
.noneCodeNodes
|
||||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||||
type: 'NoneCodeNode',
|
type: 'NoneCodeNode',
|
||||||
start: 106,
|
start: 106,
|
||||||
end: 168,
|
end: 166,
|
||||||
value:
|
value: ' /* this is\n a comment\n spanning a few lines */\n ',
|
||||||
' /* this is \n a comment \n spanning a few lines */\n ',
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('comments in a pipe expression', () => {
|
it('comments in a pipe expression', () => {
|
||||||
@ -1729,7 +1596,7 @@ const key = 'c'`
|
|||||||
' |> rx(90, %)',
|
' |> rx(90, %)',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
||||||
.noneCodeNodes
|
.noneCodeNodes
|
||||||
expect(sketchNonCodeMeta[3]).toEqual({
|
expect(sketchNonCodeMeta[3]).toEqual({
|
||||||
@ -1741,72 +1608,10 @@ const key = 'c'`
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing findEndofBinaryExpression', () => {
|
|
||||||
it('1 + 2 * 3', () => {
|
|
||||||
const code = `1 + 2 * 3\nconst yo = 5`
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].value).toBe('3')
|
|
||||||
})
|
|
||||||
it('(1 + 2) / 5 - 3', () => {
|
|
||||||
const code = `(1 + 25) / 5 - 3\nconst yo = 5`
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].value).toBe('3')
|
|
||||||
|
|
||||||
// expect to have the same end if started later in the string at a legitimate place
|
|
||||||
const indexOf5 = code.indexOf('5')
|
|
||||||
const endStartingAtThe5 = findEndOfBinaryExpression(tokens, indexOf5)
|
|
||||||
expect(endStartingAtThe5).toBe(end)
|
|
||||||
})
|
|
||||||
it('whole thing wraped: ((1 + 2) / 5 - 3)', () => {
|
|
||||||
const code = '((1 + 2) / 5 - 3)\nconst yo = 5'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].end).toBe(code.indexOf('3)') + 2)
|
|
||||||
})
|
|
||||||
it('whole thing wraped but given index after the first brace: ((1 + 2) / 5 - 3)', () => {
|
|
||||||
const code = '((1 + 2) / 5 - 3)\nconst yo = 5'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 1)
|
|
||||||
expect(tokens[end].value).toBe('3')
|
|
||||||
})
|
|
||||||
it('given the index of a small wrapped section i.e. `1 + 2` in ((1 + 2) / 5 - 3)', () => {
|
|
||||||
const code = '((1 + 2) / 5 - 3)\nconst yo = 5'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 2)
|
|
||||||
expect(tokens[end].value).toBe('2')
|
|
||||||
})
|
|
||||||
it('lots of silly nesting: (1 + 2) / (5 - (3))', () => {
|
|
||||||
const code = '(1 + 2) / (5 - (3))\nconst yo = 5'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].end).toBe(code.indexOf('))') + 2)
|
|
||||||
})
|
|
||||||
it('with pipe operator at the end', () => {
|
|
||||||
const code = '(1 + 2) / (5 - (3))\n |> fn(%)'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].end).toBe(code.indexOf('))') + 2)
|
|
||||||
})
|
|
||||||
it('with call expression at the start of binary expression', () => {
|
|
||||||
const code = 'yo(2) + 3\n |> fn(%)'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].value).toBe('3')
|
|
||||||
})
|
|
||||||
it('with call expression at the end of binary expression', () => {
|
|
||||||
const code = '3 + yo(2)\n |> fn(%)'
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const end = findEndOfBinaryExpression(tokens, 0)
|
|
||||||
expect(tokens[end].value).toBe(')')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('test UnaryExpression', () => {
|
describe('test UnaryExpression', () => {
|
||||||
it('should parse a unary expression in simple var dec situation', () => {
|
it('should parse a unary expression in simple var dec situation', () => {
|
||||||
const code = `const myVar = -min(4, 100)`
|
const code = `const myVar = -min(4, 100)`
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
||||||
expect(myVarInit).toEqual({
|
expect(myVarInit).toEqual({
|
||||||
type: 'UnaryExpression',
|
type: 'UnaryExpression',
|
||||||
@ -1831,7 +1636,7 @@ describe('test UnaryExpression', () => {
|
|||||||
describe('testing nested call expressions', () => {
|
describe('testing nested call expressions', () => {
|
||||||
it('callExp in a binExp in a callExp', () => {
|
it('callExp in a binExp in a callExp', () => {
|
||||||
const code = 'const myVar = min(100, 1 + legLen(5, 3))'
|
const code = 'const myVar = min(100, 1 + legLen(5, 3))'
|
||||||
const { body } = abstractSyntaxTree(lexer(code))
|
const { body } = parser_wasm(code)
|
||||||
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
const myVarInit = (body?.[0] as any).declarations[0]?.init
|
||||||
expect(myVarInit).toEqual({
|
expect(myVarInit).toEqual({
|
||||||
type: 'CallExpression',
|
type: 'CallExpression',
|
||||||
@ -1867,8 +1672,7 @@ describe('testing nested call expressions', () => {
|
|||||||
describe('should recognise callExpresions in binaryExpressions', () => {
|
describe('should recognise callExpresions in binaryExpressions', () => {
|
||||||
const code = "xLineTo(segEndX('seg02', %) + 1, %)"
|
const code = "xLineTo(segEndX('seg02', %) + 1, %)"
|
||||||
it('should recognise the callExp', () => {
|
it('should recognise the callExp', () => {
|
||||||
const tokens = lexer(code)
|
const { body } = parser_wasm(code)
|
||||||
const { body } = abstractSyntaxTree(tokens)
|
|
||||||
const callExpArgs = (body?.[0] as any).expression?.arguments
|
const callExpArgs = (body?.[0] as any).expression?.arguments
|
||||||
expect(callExpArgs).toEqual([
|
expect(callExpArgs).toEqual([
|
||||||
{
|
{
|
||||||
@ -1899,3 +1703,19 @@ describe('should recognise callExpresions in binaryExpressions', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('parsing errors', () => {
|
||||||
|
it('should return an error when there is a unexpected closed curly brace', async () => {
|
||||||
|
const code = `const myVar = startSketchAt([}], %)`
|
||||||
|
|
||||||
|
let _theError
|
||||||
|
try {
|
||||||
|
const result = expect(parser_wasm(code))
|
||||||
|
console.log('result', result)
|
||||||
|
} catch (e) {
|
||||||
|
_theError = e
|
||||||
|
}
|
||||||
|
const theError = _theError as any
|
||||||
|
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
|
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||||
|
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||||
|
export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
|
||||||
|
export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
|
||||||
|
export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
|
||||||
|
export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution'
|
||||||
|
export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier'
|
||||||
|
export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression'
|
||||||
|
export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression'
|
||||||
|
export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement'
|
||||||
|
export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement'
|
||||||
|
export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
|
||||||
|
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
||||||
|
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
||||||
|
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
||||||
|
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||||
|
|
||||||
export type SyntaxType =
|
export type SyntaxType =
|
||||||
| 'Program'
|
| 'Program'
|
||||||
| 'ExpressionStatement'
|
| 'ExpressionStatement'
|
||||||
| 'BinaryExpression'
|
| 'BinaryExpression'
|
||||||
| 'CallExpression'
|
| 'CallExpression'
|
||||||
| 'Identifier'
|
| 'Identifier'
|
||||||
| 'BlockStatement'
|
|
||||||
| 'ReturnStatement'
|
| 'ReturnStatement'
|
||||||
| 'VariableDeclaration'
|
| 'VariableDeclaration'
|
||||||
| 'VariableDeclarator'
|
| 'VariableDeclarator'
|
||||||
@ -18,160 +35,3 @@ export type SyntaxType =
|
|||||||
| 'Literal'
|
| 'Literal'
|
||||||
| 'NoneCodeNode'
|
| 'NoneCodeNode'
|
||||||
| 'UnaryExpression'
|
| 'UnaryExpression'
|
||||||
|
|
||||||
export interface Program {
|
|
||||||
type: SyntaxType
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
body: BodyItem[]
|
|
||||||
nonCodeMeta: NoneCodeMeta
|
|
||||||
}
|
|
||||||
interface GeneralStatement {
|
|
||||||
type: SyntaxType
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BodyItem =
|
|
||||||
| ExpressionStatement
|
|
||||||
| VariableDeclaration
|
|
||||||
| ReturnStatement
|
|
||||||
|
|
||||||
export type Value =
|
|
||||||
| Literal
|
|
||||||
| Identifier
|
|
||||||
| BinaryExpression
|
|
||||||
| FunctionExpression
|
|
||||||
| CallExpression
|
|
||||||
| PipeExpression
|
|
||||||
| PipeSubstitution
|
|
||||||
| ArrayExpression
|
|
||||||
| ObjectExpression
|
|
||||||
| MemberExpression
|
|
||||||
| UnaryExpression
|
|
||||||
|
|
||||||
export type BinaryPart =
|
|
||||||
| Literal
|
|
||||||
| Identifier
|
|
||||||
| BinaryExpression
|
|
||||||
| CallExpression
|
|
||||||
| UnaryExpression
|
|
||||||
|
|
||||||
export interface NoneCodeNode extends GeneralStatement {
|
|
||||||
type: 'NoneCodeNode'
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NoneCodeMeta {
|
|
||||||
// Stores the whitespace/comments that go after the statement who's index we're using here
|
|
||||||
noneCodeNodes: { [statementIndex: number]: NoneCodeNode }
|
|
||||||
// Which is why we also need `start` for and whitespace at the start of the file/block
|
|
||||||
start?: NoneCodeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExpressionStatement extends GeneralStatement {
|
|
||||||
type: 'ExpressionStatement'
|
|
||||||
expression: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallExpression extends GeneralStatement {
|
|
||||||
type: 'CallExpression'
|
|
||||||
callee: Identifier
|
|
||||||
arguments: Value[]
|
|
||||||
optional: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VariableDeclaration extends GeneralStatement {
|
|
||||||
type: 'VariableDeclaration'
|
|
||||||
declarations: VariableDeclarator[]
|
|
||||||
kind: 'const' | 'unknown' | 'fn' //| "solid" | "surface" | "face"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VariableDeclarator extends GeneralStatement {
|
|
||||||
type: 'VariableDeclarator'
|
|
||||||
id: Identifier
|
|
||||||
init: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Literal extends GeneralStatement {
|
|
||||||
type: 'Literal'
|
|
||||||
value: string | number | boolean | null
|
|
||||||
raw: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Identifier extends GeneralStatement {
|
|
||||||
type: 'Identifier'
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PipeSubstitution extends GeneralStatement {
|
|
||||||
type: 'PipeSubstitution'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ArrayExpression extends GeneralStatement {
|
|
||||||
type: 'ArrayExpression'
|
|
||||||
elements: Value[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ObjectExpression extends GeneralStatement {
|
|
||||||
type: 'ObjectExpression'
|
|
||||||
properties: ObjectProperty[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ObjectProperty extends GeneralStatement {
|
|
||||||
type: 'ObjectProperty'
|
|
||||||
key: Identifier
|
|
||||||
value: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberExpression extends GeneralStatement {
|
|
||||||
type: 'MemberExpression'
|
|
||||||
object: MemberExpression | Identifier
|
|
||||||
property: Identifier | Literal
|
|
||||||
computed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ObjectKeyInfo {
|
|
||||||
key: Identifier | Literal
|
|
||||||
index: number
|
|
||||||
computed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BinaryExpression extends GeneralStatement {
|
|
||||||
type: 'BinaryExpression'
|
|
||||||
operator: string
|
|
||||||
left: BinaryPart
|
|
||||||
right: BinaryPart
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UnaryExpression extends GeneralStatement {
|
|
||||||
type: 'UnaryExpression'
|
|
||||||
operator: '-' | '!'
|
|
||||||
argument: BinaryPart
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PipeExpression extends GeneralStatement {
|
|
||||||
type: 'PipeExpression'
|
|
||||||
body: Value[]
|
|
||||||
nonCodeMeta: NoneCodeMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FunctionExpression extends GeneralStatement {
|
|
||||||
type: 'FunctionExpression'
|
|
||||||
id: Identifier | null
|
|
||||||
params: Identifier[]
|
|
||||||
body: BlockStatement
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockStatement extends GeneralStatement {
|
|
||||||
type: 'BlockStatement'
|
|
||||||
body: BodyItem[]
|
|
||||||
nonCodeMeta: NoneCodeMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReturnStatement extends GeneralStatement {
|
|
||||||
type: 'ReturnStatement'
|
|
||||||
argument: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
export type All = Program | ExpressionStatement[] | BinaryExpression | Literal
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { lexer } from './tokeniser'
|
|
||||||
import { SketchGroup, ExtrudeGroup } from './executor'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor, executor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -13,53 +11,52 @@ describe('testing artifacts', () => {
|
|||||||
const mySketch001 = startSketchAt([0, 0])
|
const mySketch001 = startSketchAt([0, 0])
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const programMemory = await enginelessExecutor(
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
abstractSyntaxTree(lexer(code))
|
// @ts-ignore
|
||||||
)
|
|
||||||
const shown = programMemory?.return?.map(
|
const shown = programMemory?.return?.map(
|
||||||
|
// @ts-ignore
|
||||||
(a) => programMemory?.root?.[a.name]
|
(a) => programMemory?.root?.[a.name]
|
||||||
)
|
)
|
||||||
expect(shown).toEqual([
|
expect(shown).toEqual([
|
||||||
{
|
{
|
||||||
type: 'sketchGroup',
|
type: 'sketchGroup',
|
||||||
start: {
|
start: {
|
||||||
type: 'base',
|
|
||||||
to: [0, 0],
|
to: [0, 0],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: '66366561-6465-4734-a463-366330356563',
|
id: expect.any(String),
|
||||||
sourceRange: [21, 42],
|
sourceRange: [21, 42],
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
|
name: '',
|
||||||
to: [-1.59, -1.54],
|
to: [-1.59, -1.54],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [48, 73],
|
sourceRange: [48, 73],
|
||||||
id: '30366338-6462-4330-a364-303935626163',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to: [0.46, -5.82],
|
to: [0.46, -5.82],
|
||||||
from: [-1.59, -1.54],
|
from: [-1.59, -1.54],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [79, 103],
|
sourceRange: [79, 103],
|
||||||
id: '32653334-6331-4231-b162-663334363535',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
id: '39643164-6130-4734-b432-623638393262',
|
id: expect.any(String),
|
||||||
__meta: [{ sourceRange: [21, 42], pathToNode: [] }],
|
__meta: [{ sourceRange: [21, 42] }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -72,24 +69,21 @@ const mySketch001 = startSketchAt([0, 0])
|
|||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const programMemory = await enginelessExecutor(
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
abstractSyntaxTree(lexer(code))
|
// @ts-ignore
|
||||||
)
|
|
||||||
const shown = programMemory?.return?.map(
|
const shown = programMemory?.return?.map(
|
||||||
|
// @ts-ignore
|
||||||
(a) => programMemory?.root?.[a.name]
|
(a) => programMemory?.root?.[a.name]
|
||||||
)
|
)
|
||||||
expect(shown).toEqual([
|
expect(shown).toEqual([
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'extrudeGroup',
|
||||||
id: '65383433-3839-4333-b836-343263636638',
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [
|
__meta: [{ sourceRange: [21, 42] }],
|
||||||
{ sourceRange: [127, 140], pathToNode: [] },
|
|
||||||
{ sourceRange: [21, 42], pathToNode: [] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -112,39 +106,33 @@ const sk2 = startSketchAt([0, 0])
|
|||||||
|> lineTo([2.5, 0], %)
|
|> lineTo([2.5, 0], %)
|
||||||
// |> transform(theTransf, %)
|
// |> transform(theTransf, %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
|
|
||||||
|
|
||||||
show(theExtrude, sk2)`
|
show(theExtrude, sk2)`
|
||||||
const programMemory = await enginelessExecutor(
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
abstractSyntaxTree(lexer(code))
|
// @ts-ignore
|
||||||
)
|
|
||||||
const geos = programMemory?.return?.map(
|
const geos = programMemory?.return?.map(
|
||||||
|
// @ts-ignore
|
||||||
({ name }) => programMemory?.root?.[name]
|
({ name }) => programMemory?.root?.[name]
|
||||||
)
|
)
|
||||||
expect(geos).toEqual([
|
expect(geos).toEqual([
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'extrudeGroup',
|
||||||
id: '63333330-3631-4230-b664-623132643731',
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [
|
__meta: [{ sourceRange: [13, 34] }],
|
||||||
{ sourceRange: [212, 227], pathToNode: [] },
|
|
||||||
{ sourceRange: [13, 34], pathToNode: [] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'extrudeGroup',
|
||||||
id: '33316639-3438-4661-a334-663262383737',
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
__meta: [
|
__meta: [{ sourceRange: [302, 323] }],
|
||||||
{ sourceRange: [453, 466], pathToNode: [] },
|
|
||||||
{ sourceRange: [302, 323], pathToNode: [] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
import { parseExpression, reversePolishNotation } from './astMathExpressions'
|
|
||||||
import { lexer } from './tokeniser'
|
|
||||||
import { initPromise } from './rust'
|
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
|
||||||
|
|
||||||
describe('parseExpression', () => {
|
|
||||||
it('parses a simple expression', () => {
|
|
||||||
const result = parseExpression(lexer('1 + 2'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 0,
|
|
||||||
end: 5,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: { type: 'Literal', value: 2, raw: '2', start: 4, end: 5 },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('parses a more complex expression + followed by *', () => {
|
|
||||||
const tokens = lexer('1 + 2 * 3')
|
|
||||||
const result = parseExpression(tokens)
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 0,
|
|
||||||
end: 9,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '*',
|
|
||||||
start: 4,
|
|
||||||
end: 9,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 4, end: 5 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 8, end: 9 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('parses a more complex expression with parentheses: 1 * ( 2 + 3 )', () => {
|
|
||||||
const result = parseExpression(lexer('1 * ( 2 + 3 )'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '*',
|
|
||||||
start: 0,
|
|
||||||
end: 13,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 6,
|
|
||||||
end: 11,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('parses a more complex expression with parentheses with more', () => {
|
|
||||||
const result = parseExpression(lexer('1 * ( 2 + 3 ) / 4'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '/',
|
|
||||||
start: 0,
|
|
||||||
end: 17,
|
|
||||||
left: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '*',
|
|
||||||
start: 0,
|
|
||||||
end: 13,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 6,
|
|
||||||
end: 11,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
right: { type: 'Literal', value: 4, raw: '4', start: 16, end: 17 },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('same as last one but with a 1 + at the start', () => {
|
|
||||||
const result = parseExpression(lexer('1 + ( 2 + 3 ) / 4'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 0,
|
|
||||||
end: 17,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '/',
|
|
||||||
start: 4,
|
|
||||||
end: 17,
|
|
||||||
left: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 6,
|
|
||||||
end: 11,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 6, end: 7 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 10, end: 11 },
|
|
||||||
},
|
|
||||||
right: { type: 'Literal', value: 4, raw: '4', start: 16, end: 17 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('nested braces', () => {
|
|
||||||
const result = parseExpression(lexer('1 * (( 2 + 3 ) / 4 + 5 )'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '*',
|
|
||||||
start: 0,
|
|
||||||
end: 24,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 5,
|
|
||||||
end: 22,
|
|
||||||
left: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '/',
|
|
||||||
start: 5,
|
|
||||||
end: 18,
|
|
||||||
left: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 7,
|
|
||||||
end: 12,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 7, end: 8 },
|
|
||||||
right: {
|
|
||||||
type: 'Literal',
|
|
||||||
value: 3,
|
|
||||||
raw: '3',
|
|
||||||
start: 11,
|
|
||||||
end: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
right: { type: 'Literal', value: 4, raw: '4', start: 17, end: 18 },
|
|
||||||
},
|
|
||||||
right: { type: 'Literal', value: 5, raw: '5', start: 21, end: 22 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('multiple braces around the same thing', () => {
|
|
||||||
const result = parseExpression(lexer('1 * ((( 2 + 3 )))'))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '*',
|
|
||||||
start: 0,
|
|
||||||
end: 17,
|
|
||||||
left: { type: 'Literal', value: 1, raw: '1', start: 0, end: 1 },
|
|
||||||
right: {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 8,
|
|
||||||
end: 13,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 8, end: 9 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 12, end: 13 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('multiple braces around a sing literal', () => {
|
|
||||||
const code = '2 + (((3)))'
|
|
||||||
const result = parseExpression(lexer(code))
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: '+',
|
|
||||||
start: 0,
|
|
||||||
end: code.indexOf(')))') + 3,
|
|
||||||
left: { type: 'Literal', value: 2, raw: '2', start: 0, end: 1 },
|
|
||||||
right: { type: 'Literal', value: 3, raw: '3', start: 7, end: 8 },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('reversePolishNotation', () => {
|
|
||||||
it('converts a simple expression', () => {
|
|
||||||
const result = reversePolishNotation(lexer('1 + 2'))
|
|
||||||
expect(result).toEqual([
|
|
||||||
{ type: 'number', value: '1', start: 0, end: 1 },
|
|
||||||
{ type: 'number', value: '2', start: 4, end: 5 },
|
|
||||||
{ type: 'operator', value: '+', start: 2, end: 3 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
it('converts a more complex expression', () => {
|
|
||||||
const result = reversePolishNotation(lexer('1 + 2 * 3'))
|
|
||||||
expect(result).toEqual([
|
|
||||||
{ type: 'number', value: '1', start: 0, end: 1 },
|
|
||||||
{ type: 'number', value: '2', start: 4, end: 5 },
|
|
||||||
{ type: 'number', value: '3', start: 8, end: 9 },
|
|
||||||
{ type: 'operator', value: '*', start: 6, end: 7 },
|
|
||||||
{ type: 'operator', value: '+', start: 2, end: 3 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
it('converts a more complex expression with parentheses', () => {
|
|
||||||
const result = reversePolishNotation(lexer('1 * ( 2 + 3 )'))
|
|
||||||
expect(result).toEqual([
|
|
||||||
{ type: 'number', value: '1', start: 0, end: 1 },
|
|
||||||
{ type: 'brace', value: '(', start: 4, end: 5 },
|
|
||||||
{ type: 'number', value: '2', start: 6, end: 7 },
|
|
||||||
{ type: 'number', value: '3', start: 10, end: 11 },
|
|
||||||
{ type: 'operator', value: '+', start: 8, end: 9 },
|
|
||||||
{ type: 'brace', value: ')', start: 12, end: 13 },
|
|
||||||
{ type: 'operator', value: '*', start: 2, end: 3 },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,253 +0,0 @@
|
|||||||
import {
|
|
||||||
BinaryExpression,
|
|
||||||
Literal,
|
|
||||||
Identifier,
|
|
||||||
CallExpression,
|
|
||||||
} from './abstractSyntaxTreeTypes'
|
|
||||||
import {
|
|
||||||
findClosingBrace,
|
|
||||||
makeCallExpression,
|
|
||||||
isNotCodeToken,
|
|
||||||
} from './abstractSyntaxTree'
|
|
||||||
import { Token } from './tokeniser'
|
|
||||||
import { KCLSyntaxError } from './errors'
|
|
||||||
|
|
||||||
export function reversePolishNotation(
|
|
||||||
tokens: Token[],
|
|
||||||
previousPostfix: Token[] = [],
|
|
||||||
operators: Token[] = []
|
|
||||||
): Token[] {
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
return [...previousPostfix, ...operators.slice().reverse()] // reverse mutates, so slice/clone is needed
|
|
||||||
}
|
|
||||||
const currentToken = tokens[0]
|
|
||||||
if (
|
|
||||||
currentToken.type === 'word' &&
|
|
||||||
tokens?.[1]?.type === 'brace' &&
|
|
||||||
tokens?.[1]?.value === '('
|
|
||||||
) {
|
|
||||||
const closingBrace = findClosingBrace(tokens, 1)
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens.slice(closingBrace + 1),
|
|
||||||
[...previousPostfix, ...tokens.slice(0, closingBrace + 1)],
|
|
||||||
operators
|
|
||||||
)
|
|
||||||
} else if (
|
|
||||||
currentToken.type === 'number' ||
|
|
||||||
currentToken.type === 'word' ||
|
|
||||||
currentToken.type === 'string'
|
|
||||||
) {
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens.slice(1),
|
|
||||||
[...previousPostfix, currentToken],
|
|
||||||
operators
|
|
||||||
)
|
|
||||||
} else if (['+', '-', '*', '/', '%'].includes(currentToken.value)) {
|
|
||||||
if (
|
|
||||||
operators.length > 0 &&
|
|
||||||
_precedence(operators[operators.length - 1]) >= _precedence(currentToken)
|
|
||||||
) {
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens,
|
|
||||||
[...previousPostfix, operators[operators.length - 1]],
|
|
||||||
operators.slice(0, -1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return reversePolishNotation(tokens.slice(1), previousPostfix, [
|
|
||||||
...operators,
|
|
||||||
currentToken,
|
|
||||||
])
|
|
||||||
} else if (currentToken.value === '(') {
|
|
||||||
// push current token to both stacks as it is a legitimate operator
|
|
||||||
// but later we'll need to pop other operators off the stack until we find the matching ')'
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens.slice(1),
|
|
||||||
[...previousPostfix, currentToken],
|
|
||||||
[...operators, currentToken]
|
|
||||||
)
|
|
||||||
} else if (currentToken.value === ')') {
|
|
||||||
if (operators[operators.length - 1]?.value !== '(') {
|
|
||||||
// pop operators off the stack and push them to postFix until we find the matching '('
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens,
|
|
||||||
[...previousPostfix, operators[operators.length - 1]],
|
|
||||||
operators.slice(0, -1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return reversePolishNotation(
|
|
||||||
tokens.slice(1),
|
|
||||||
[...previousPostfix, currentToken],
|
|
||||||
operators.slice(0, -1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isNotCodeToken(currentToken)) {
|
|
||||||
return reversePolishNotation(tokens.slice(1), previousPostfix, operators)
|
|
||||||
}
|
|
||||||
throw new KCLSyntaxError('Unknown token', [
|
|
||||||
[currentToken.start, currentToken.end],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParenthesisToken {
|
|
||||||
type: 'parenthesis'
|
|
||||||
value: '(' | ')'
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtendedBinaryExpression extends BinaryExpression {
|
|
||||||
startExtended?: number
|
|
||||||
endExtended?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildTree = (
|
|
||||||
reversePolishNotationTokens: Token[],
|
|
||||||
stack: (
|
|
||||||
| ExtendedBinaryExpression
|
|
||||||
| Literal
|
|
||||||
| Identifier
|
|
||||||
| ParenthesisToken
|
|
||||||
| CallExpression
|
|
||||||
)[] = []
|
|
||||||
): BinaryExpression => {
|
|
||||||
if (reversePolishNotationTokens.length === 0) {
|
|
||||||
return stack[0] as BinaryExpression
|
|
||||||
}
|
|
||||||
const currentToken = reversePolishNotationTokens[0]
|
|
||||||
if (currentToken.type === 'number' || currentToken.type === 'string') {
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(1), [
|
|
||||||
...stack,
|
|
||||||
{
|
|
||||||
type: 'Literal',
|
|
||||||
value:
|
|
||||||
currentToken.type === 'number'
|
|
||||||
? Number(currentToken.value)
|
|
||||||
: currentToken.value.slice(1, -1),
|
|
||||||
raw: currentToken.value,
|
|
||||||
start: currentToken.start,
|
|
||||||
end: currentToken.end,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} else if (currentToken.type === 'word') {
|
|
||||||
if (
|
|
||||||
reversePolishNotationTokens?.[1]?.type === 'brace' &&
|
|
||||||
reversePolishNotationTokens?.[1]?.value === '('
|
|
||||||
) {
|
|
||||||
const closingBrace = findClosingBrace(reversePolishNotationTokens, 1)
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(closingBrace + 1), [
|
|
||||||
...stack,
|
|
||||||
makeCallExpression(reversePolishNotationTokens, 0).expression,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(1), [
|
|
||||||
...stack,
|
|
||||||
{
|
|
||||||
type: 'Identifier',
|
|
||||||
name: currentToken.value,
|
|
||||||
start: currentToken.start,
|
|
||||||
end: currentToken.end,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} else if (currentToken.type === 'brace' && currentToken.value === '(') {
|
|
||||||
const paranToken: ParenthesisToken = {
|
|
||||||
type: 'parenthesis',
|
|
||||||
value: '(',
|
|
||||||
start: currentToken.start,
|
|
||||||
end: currentToken.end,
|
|
||||||
}
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(1), [
|
|
||||||
...stack,
|
|
||||||
paranToken,
|
|
||||||
])
|
|
||||||
} else if (currentToken.type === 'brace' && currentToken.value === ')') {
|
|
||||||
const innerNode = stack[stack.length - 1]
|
|
||||||
|
|
||||||
const paran = stack[stack.length - 2]
|
|
||||||
|
|
||||||
const binExp: ExtendedBinaryExpression = {
|
|
||||||
...innerNode,
|
|
||||||
startExtended: paran.start,
|
|
||||||
endExtended: currentToken.end,
|
|
||||||
} as ExtendedBinaryExpression
|
|
||||||
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(1), [
|
|
||||||
...stack.slice(0, -2),
|
|
||||||
binExp,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = { ...stack[stack.length - 2] }
|
|
||||||
let start = left.start
|
|
||||||
if (left.type === 'BinaryExpression') {
|
|
||||||
start = left?.startExtended || left.start
|
|
||||||
delete left.startExtended
|
|
||||||
delete left.endExtended
|
|
||||||
}
|
|
||||||
|
|
||||||
const right = { ...stack[stack.length - 1] }
|
|
||||||
let end = right.end
|
|
||||||
if (right.type === 'BinaryExpression') {
|
|
||||||
end = right?.endExtended || right.end
|
|
||||||
delete right.startExtended
|
|
||||||
delete right.endExtended
|
|
||||||
}
|
|
||||||
|
|
||||||
const binExp: BinaryExpression = {
|
|
||||||
type: 'BinaryExpression',
|
|
||||||
operator: currentToken.value,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
left: left as any,
|
|
||||||
right: right as any,
|
|
||||||
}
|
|
||||||
return buildTree(reversePolishNotationTokens.slice(1), [
|
|
||||||
...stack.slice(0, -2),
|
|
||||||
binExp,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseExpression(tokens: Token[]): BinaryExpression {
|
|
||||||
const treeWithMaybeBadTopLevelStartEnd = buildTree(
|
|
||||||
reversePolishNotation(tokens)
|
|
||||||
)
|
|
||||||
const left = treeWithMaybeBadTopLevelStartEnd?.left as any
|
|
||||||
const start = left?.startExtended || treeWithMaybeBadTopLevelStartEnd?.start
|
|
||||||
if (left == undefined || left == null) {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
'syntax',
|
|
||||||
tokens.map((token) => [token.start, token.end])
|
|
||||||
) // Add text
|
|
||||||
}
|
|
||||||
delete left.startExtended
|
|
||||||
delete left.endExtended
|
|
||||||
|
|
||||||
const right = treeWithMaybeBadTopLevelStartEnd?.right as any
|
|
||||||
const end = right?.endExtended || treeWithMaybeBadTopLevelStartEnd?.end
|
|
||||||
delete right.startExtended
|
|
||||||
delete right.endExtended
|
|
||||||
|
|
||||||
const tree: BinaryExpression = {
|
|
||||||
...treeWithMaybeBadTopLevelStartEnd,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
}
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
function _precedence(operator: Token): number {
|
|
||||||
return precedence(operator.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function precedence(operator: string): number {
|
|
||||||
// might be useful for reference to make it match
|
|
||||||
// another commonly used lang https://www.w3schools.com/js/js_precedence.asp
|
|
||||||
if (['+', '-'].includes(operator)) {
|
|
||||||
return 11
|
|
||||||
} else if (['*', '/', '%'].includes(operator)) {
|
|
||||||
return 12
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,13 @@
|
|||||||
import { Diagnostic } from '@codemirror/lint'
|
import { Diagnostic } from '@codemirror/lint'
|
||||||
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
|
|
||||||
|
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||||
export class KCLError {
|
export class KCLError {
|
||||||
kind: string | undefined
|
kind: ExtractKind<RustKclError> | 'name'
|
||||||
sourceRanges: [number, number][]
|
sourceRanges: [number, number][]
|
||||||
msg: string
|
msg: string
|
||||||
constructor(
|
constructor(
|
||||||
kind: string | undefined,
|
kind: ExtractKind<RustKclError> | 'name',
|
||||||
msg: string,
|
msg: string,
|
||||||
sourceRanges: [number, number][]
|
sourceRanges: [number, number][]
|
||||||
) {
|
) {
|
||||||
@ -39,11 +41,18 @@ export class KCLTypeError extends KCLError {
|
|||||||
|
|
||||||
export class KCLUnimplementedError extends KCLError {
|
export class KCLUnimplementedError extends KCLError {
|
||||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||||
super('unimplemented feature', msg, sourceRanges)
|
super('unimplemented', msg, sourceRanges)
|
||||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class KCLUnexpectedError extends KCLError {
|
||||||
|
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||||
|
super('unexpected', msg, sourceRanges)
|
||||||
|
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class KCLValueAlreadyDefined extends KCLError {
|
export class KCLValueAlreadyDefined extends KCLError {
|
||||||
constructor(key: string, sourceRanges: [number, number][]) {
|
constructor(key: string, sourceRanges: [number, number][]) {
|
||||||
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
|
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { lexer } from './tokeniser'
|
import { ProgramMemory } from './executor'
|
||||||
import { ProgramMemory, Path, SketchGroup } from './executor'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { KCLUndefinedValueError } from './errors'
|
import { KCLError } from './errors'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
|
||||||
@ -31,29 +30,6 @@ const newVar = myVar + 1`
|
|||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
expect(root.myVar.value).toBe('a str another str')
|
expect(root.myVar.value).toBe('a str another str')
|
||||||
})
|
})
|
||||||
it('test with function call', async () => {
|
|
||||||
const code = `
|
|
||||||
const myVar = "hello"
|
|
||||||
log(5, myVar)`
|
|
||||||
const programMemoryOverride: ProgramMemory['root'] = {
|
|
||||||
log: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: vi.fn(),
|
|
||||||
__meta: [
|
|
||||||
{
|
|
||||||
sourceRange: [0, 0],
|
|
||||||
pathToNode: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const { root } = await enginelessExecutor(abstractSyntaxTree(lexer(code)), {
|
|
||||||
root: programMemoryOverride,
|
|
||||||
pendingMemory: {},
|
|
||||||
})
|
|
||||||
expect(root.myVar.value).toBe('hello')
|
|
||||||
expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello')
|
|
||||||
})
|
|
||||||
it('fn funcN = () => {} execute', async () => {
|
it('fn funcN = () => {} execute', async () => {
|
||||||
const { root } = await exe(
|
const { root } = await exe(
|
||||||
[
|
[
|
||||||
@ -85,8 +61,7 @@ show(mySketch)
|
|||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [43, 80],
|
sourceRange: [43, 80],
|
||||||
id: '37333036-3033-4432-b530-643030303837',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
name: 'myPath',
|
name: 'myPath',
|
||||||
},
|
},
|
||||||
@ -94,10 +69,10 @@ show(mySketch)
|
|||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to: [2, 3],
|
to: [2, 3],
|
||||||
from: [0, 2],
|
from: [0, 2],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [86, 102],
|
sourceRange: [86, 102],
|
||||||
id: '32343136-3330-4134-a462-376437386365',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -106,8 +81,7 @@ show(mySketch)
|
|||||||
from: [2, 3],
|
from: [2, 3],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [108, 151],
|
sourceRange: [108, 151],
|
||||||
id: '32306132-6130-4138-b832-636363326330',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
name: 'rightPath',
|
name: 'rightPath',
|
||||||
},
|
},
|
||||||
@ -171,13 +145,12 @@ show(mySketch)
|
|||||||
expect(root.mySk1).toEqual({
|
expect(root.mySk1).toEqual({
|
||||||
type: 'sketchGroup',
|
type: 'sketchGroup',
|
||||||
start: {
|
start: {
|
||||||
type: 'base',
|
|
||||||
to: [0, 0],
|
to: [0, 0],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: '37663863-3664-4366-a637-623739336334',
|
id: expect.any(String),
|
||||||
sourceRange: [14, 34],
|
sourceRange: [14, 34],
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
@ -185,10 +158,10 @@ show(mySketch)
|
|||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to: [1, 1],
|
to: [1, 1],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [40, 56],
|
sourceRange: [40, 56],
|
||||||
id: '34356231-3362-4363-b935-393033353034',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -197,8 +170,7 @@ show(mySketch)
|
|||||||
from: [1, 1],
|
from: [1, 1],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [62, 100],
|
sourceRange: [62, 100],
|
||||||
id: '39623339-3538-4366-b633-356630326639',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
name: 'myPath',
|
name: 'myPath',
|
||||||
},
|
},
|
||||||
@ -206,17 +178,17 @@ show(mySketch)
|
|||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to: [1, 1],
|
to: [1, 1],
|
||||||
from: [0, 1],
|
from: [0, 1],
|
||||||
|
name: '',
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
sourceRange: [106, 122],
|
sourceRange: [106, 122],
|
||||||
id: '30636135-6232-4335-b665-366562303161',
|
id: expect.any(String),
|
||||||
pathToNode: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0, 1],
|
rotation: [0, 0, 0, 1],
|
||||||
id: '30376661-3039-4965-b532-653665313731',
|
id: expect.any(String),
|
||||||
__meta: [{ sourceRange: [14, 34], pathToNode: [] }],
|
__meta: [{ sourceRange: [14, 34] }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('execute array expression', async () => {
|
it('execute array expression', async () => {
|
||||||
@ -231,13 +203,6 @@ show(mySketch)
|
|||||||
value: 3,
|
value: 3,
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
pathToNode: [
|
|
||||||
['body', ''],
|
|
||||||
[0, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
],
|
|
||||||
sourceRange: [14, 15],
|
sourceRange: [14, 15],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -247,25 +212,8 @@ show(mySketch)
|
|||||||
value: [1, '2', 3, 9],
|
value: [1, '2', 3, 9],
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
pathToNode: [
|
|
||||||
['body', ''],
|
|
||||||
[1, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
],
|
|
||||||
sourceRange: [27, 49],
|
sourceRange: [27, 49],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pathToNode: [
|
|
||||||
['body', ''],
|
|
||||||
[0, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
],
|
|
||||||
sourceRange: [14, 15],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -281,13 +229,6 @@ show(mySketch)
|
|||||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
pathToNode: [
|
|
||||||
['body', ''],
|
|
||||||
[1, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
],
|
|
||||||
sourceRange: [27, 83],
|
sourceRange: [27, 83],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -303,13 +244,6 @@ show(mySketch)
|
|||||||
value: '123',
|
value: '123',
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
pathToNode: [
|
|
||||||
['body', ''],
|
|
||||||
[1, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
],
|
|
||||||
sourceRange: [41, 50],
|
sourceRange: [41, 50],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -452,19 +386,19 @@ const theExtrude = startSketchAt([0, 0])
|
|||||||
|> extrude(4, %)
|
|> extrude(4, %)
|
||||||
show(theExtrude)`
|
show(theExtrude)`
|
||||||
await expect(exe(code)).rejects.toEqual(
|
await expect(exe(code)).rejects.toEqual(
|
||||||
new KCLUndefinedValueError('Memory item myVarZ not found', [[100, 106]])
|
new KCLError(
|
||||||
|
'undefined_value',
|
||||||
|
'memory item key `myVarZ` is not defined',
|
||||||
|
[[100, 106]]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
async function exe(
|
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
||||||
code: string,
|
const ast = parser_wasm(code)
|
||||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} }
|
|
||||||
) {
|
|
||||||
const tokens = lexer(code)
|
|
||||||
const ast = abstractSyntaxTree(tokens)
|
|
||||||
|
|
||||||
const result = await enginelessExecutor(ast, programMemory)
|
const result = await enginelessExecutor(ast, programMemory)
|
||||||
return result
|
return result
|
||||||
|
@ -1,35 +1,19 @@
|
|||||||
import {
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
Program,
|
|
||||||
BinaryPart,
|
|
||||||
BinaryExpression,
|
|
||||||
PipeExpression,
|
|
||||||
ObjectExpression,
|
|
||||||
MemberExpression,
|
|
||||||
Identifier,
|
|
||||||
CallExpression,
|
|
||||||
ArrayExpression,
|
|
||||||
UnaryExpression,
|
|
||||||
} from './abstractSyntaxTreeTypes'
|
|
||||||
import { InternalFnNames } from './std/stdTypes'
|
|
||||||
import { internalFns } from './std/std'
|
|
||||||
import {
|
|
||||||
KCLUndefinedValueError,
|
|
||||||
KCLValueAlreadyDefined,
|
|
||||||
KCLSyntaxError,
|
|
||||||
KCLSemanticError,
|
|
||||||
KCLTypeError,
|
|
||||||
} from './errors'
|
|
||||||
import {
|
import {
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
ArtifactMap,
|
ArtifactMap,
|
||||||
SourceRangeMap,
|
SourceRangeMap,
|
||||||
} from './std/engineConnection'
|
} from './std/engineConnection'
|
||||||
|
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||||
|
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
import { KCLError } from './errors'
|
||||||
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
|
import { rangeTypeFix } from './abstractSyntaxTree'
|
||||||
|
|
||||||
export type SourceRange = [number, number]
|
export type SourceRange = [number, number]
|
||||||
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
sourceRange: SourceRange
|
sourceRange: SourceRange
|
||||||
pathToNode: PathToNode
|
|
||||||
}
|
}
|
||||||
export type Position = [number, number, number]
|
export type Position = [number, number, number]
|
||||||
export type Rotation = [number, number, number, number]
|
export type Rotation = [number, number, number, number]
|
||||||
@ -41,7 +25,6 @@ interface BasePath {
|
|||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: string
|
id: string
|
||||||
sourceRange: SourceRange
|
sourceRange: SourceRange
|
||||||
pathToNode: PathToNode
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +51,7 @@ export interface AngledLineTo extends BasePath {
|
|||||||
interface GeoMeta {
|
interface GeoMeta {
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
id: string
|
id: string
|
||||||
refId?: string
|
|
||||||
sourceRange: SourceRange
|
sourceRange: SourceRange
|
||||||
pathToNode: PathToNode
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,69 +99,16 @@ type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
|||||||
interface Memory {
|
interface Memory {
|
||||||
[key: string]: MemoryItem
|
[key: string]: MemoryItem
|
||||||
}
|
}
|
||||||
interface PendingMemory {
|
|
||||||
[key: string]: Promise<MemoryItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProgramMemory {
|
export interface ProgramMemory {
|
||||||
root: Memory
|
root: Memory
|
||||||
pendingMemory: Partial<PendingMemory>
|
return?: ProgramReturn
|
||||||
return?: Identifier[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const addItemToMemory = (
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
key: string,
|
|
||||||
sourceRange: [[number, number]],
|
|
||||||
value: MemoryItem | Promise<MemoryItem>
|
|
||||||
) => {
|
|
||||||
const _programMemory = programMemory
|
|
||||||
if (_programMemory.root[key] || _programMemory.pendingMemory[key]) {
|
|
||||||
throw new KCLValueAlreadyDefined(key, sourceRange)
|
|
||||||
}
|
|
||||||
if (value instanceof Promise) {
|
|
||||||
_programMemory.pendingMemory[key] = value
|
|
||||||
value.then((resolvedValue) => {
|
|
||||||
_programMemory.root[key] = resolvedValue
|
|
||||||
delete _programMemory.pendingMemory[key]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_programMemory.root[key] = value
|
|
||||||
}
|
|
||||||
return _programMemory
|
|
||||||
}
|
|
||||||
|
|
||||||
const promisifyMemoryItem = async (obj: MemoryItem) => {
|
|
||||||
if (obj.value instanceof Promise) {
|
|
||||||
const resolvedGuy = await obj.value
|
|
||||||
return {
|
|
||||||
...obj,
|
|
||||||
value: resolvedGuy,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMemoryItem = async (
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
key: string,
|
|
||||||
sourceRanges: [number, number][]
|
|
||||||
): Promise<MemoryItem> => {
|
|
||||||
if (programMemory.root[key]) {
|
|
||||||
return programMemory.root[key]
|
|
||||||
}
|
|
||||||
if (programMemory.pendingMemory[key]) {
|
|
||||||
return programMemory.pendingMemory[key] as Promise<MemoryItem>
|
|
||||||
}
|
|
||||||
throw new KCLUndefinedValueError(`Memory item ${key} not found`, sourceRanges)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const executor = async (
|
export const executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
|
programMemory: ProgramMemory = { root: {} },
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
|
|
||||||
previousPathToNode: PathToNode = [],
|
|
||||||
// work around while the gemotry is still be stored on the frontend
|
// work around while the gemotry is still be stored on the frontend
|
||||||
// will be removed when the stream UI is added.
|
// will be removed when the stream UI is added.
|
||||||
tempMapCallback: (a: {
|
tempMapCallback: (a: {
|
||||||
@ -192,9 +120,7 @@ export const executor = async (
|
|||||||
const _programMemory = await _executor(
|
const _programMemory = await _executor(
|
||||||
node,
|
node,
|
||||||
programMemory,
|
programMemory,
|
||||||
engineCommandManager,
|
engineCommandManager
|
||||||
options,
|
|
||||||
previousPathToNode
|
|
||||||
)
|
)
|
||||||
const { artifactMap, sourceRangeMap } =
|
const { artifactMap, sourceRangeMap } =
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
@ -206,840 +132,25 @@ export const executor = async (
|
|||||||
|
|
||||||
export const _executor = async (
|
export const _executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} },
|
programMemory: ProgramMemory = { root: {} },
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager
|
||||||
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' },
|
|
||||||
previousPathToNode: PathToNode = []
|
|
||||||
): Promise<ProgramMemory> => {
|
): Promise<ProgramMemory> => {
|
||||||
let _programMemory: ProgramMemory = {
|
try {
|
||||||
root: {
|
const memory: ProgramMemory = await execute_wasm(
|
||||||
...programMemory.root,
|
JSON.stringify(node),
|
||||||
},
|
JSON.stringify(programMemory),
|
||||||
pendingMemory: {
|
|
||||||
...programMemory.pendingMemory,
|
|
||||||
},
|
|
||||||
return: programMemory.return,
|
|
||||||
}
|
|
||||||
const { body } = node
|
|
||||||
const proms: Promise<any>[] = []
|
|
||||||
for (let bodyIndex = 0; bodyIndex < body.length; bodyIndex++) {
|
|
||||||
const statement = body[bodyIndex]
|
|
||||||
if (statement.type === 'VariableDeclaration') {
|
|
||||||
for (let index = 0; index < statement.declarations.length; index++) {
|
|
||||||
const declaration = statement.declarations[index]
|
|
||||||
const variableName = declaration.id.name
|
|
||||||
const pathToNode: PathToNode = [
|
|
||||||
...previousPathToNode,
|
|
||||||
['body', ''],
|
|
||||||
[bodyIndex, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[index, 'index'],
|
|
||||||
['init', 'VariableDeclaration'],
|
|
||||||
]
|
|
||||||
const sourceRange: SourceRange = [
|
|
||||||
declaration.init.start,
|
|
||||||
declaration.init.end,
|
|
||||||
]
|
|
||||||
const __meta: Metadata[] = [
|
|
||||||
{
|
|
||||||
pathToNode,
|
|
||||||
sourceRange,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (declaration.init.type === 'PipeExpression') {
|
|
||||||
const prom = getPipeExpressionResult(
|
|
||||||
declaration.init,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
const value = await prom
|
|
||||||
if (value?.type === 'sketchGroup' || value?.type === 'extrudeGroup') {
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
value
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value,
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (declaration.init.type === 'Identifier') {
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value: _programMemory.root[declaration.init.name].value,
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'Literal') {
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value: declaration.init.value,
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'BinaryExpression') {
|
|
||||||
const prom = getBinaryExpressionResult(
|
|
||||||
declaration.init,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
promisifyMemoryItem({
|
|
||||||
type: 'userVal',
|
|
||||||
value: prom,
|
|
||||||
__meta,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'UnaryExpression') {
|
|
||||||
const prom = getUnaryExpressionResult(
|
|
||||||
declaration.init,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
promisifyMemoryItem({
|
|
||||||
type: 'userVal',
|
|
||||||
value: prom,
|
|
||||||
__meta,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'ArrayExpression') {
|
|
||||||
const valueInfo: Promise<{ value: any; __meta?: Metadata }>[] =
|
|
||||||
declaration.init.elements.map(
|
|
||||||
async (element): Promise<{ value: any; __meta?: Metadata }> => {
|
|
||||||
if (element.type === 'Literal') {
|
|
||||||
return {
|
|
||||||
value: element.value,
|
|
||||||
}
|
|
||||||
} else if (element.type === 'BinaryExpression') {
|
|
||||||
const prom = getBinaryExpressionResult(
|
|
||||||
element,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
return {
|
|
||||||
value: await prom,
|
|
||||||
}
|
|
||||||
} else if (element.type === 'PipeExpression') {
|
|
||||||
const prom = getPipeExpressionResult(
|
|
||||||
element,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
pathToNode
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
return {
|
|
||||||
value: await prom,
|
|
||||||
}
|
|
||||||
} else if (element.type === 'Identifier') {
|
|
||||||
const node = await getMemoryItem(
|
|
||||||
_programMemory,
|
|
||||||
element.name,
|
|
||||||
[[element.start, element.end]]
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
value: node.value,
|
|
||||||
__meta: node.__meta[node.__meta.length - 1],
|
|
||||||
}
|
|
||||||
} else if (element.type === 'UnaryExpression') {
|
|
||||||
const prom = getUnaryExpressionResult(
|
|
||||||
element,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
return {
|
|
||||||
value: await prom,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
`Unexpected element type ${element.type} in array expression`,
|
|
||||||
// TODO: Refactor this whole block into a `switch` so that we have a specific
|
|
||||||
// type here and can put a sourceRange.
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const awaitedValueInfo = await Promise.all(valueInfo)
|
|
||||||
const meta = awaitedValueInfo
|
|
||||||
.filter(({ __meta }) => __meta)
|
|
||||||
.map(({ __meta }) => __meta) as Metadata[]
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value: awaitedValueInfo.map(({ value }) => value),
|
|
||||||
__meta: [...__meta, ...meta],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'ObjectExpression') {
|
|
||||||
const prom = executeObjectExpression(
|
|
||||||
_programMemory,
|
|
||||||
declaration.init,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
promisifyMemoryItem({
|
|
||||||
type: 'userVal',
|
|
||||||
value: prom,
|
|
||||||
__meta,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'FunctionExpression') {
|
|
||||||
const fnInit = declaration.init
|
|
||||||
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
declaration.id.name,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value: async (...args: any[]) => {
|
|
||||||
let fnMemory: ProgramMemory = {
|
|
||||||
root: {
|
|
||||||
..._programMemory.root,
|
|
||||||
},
|
|
||||||
pendingMemory: {
|
|
||||||
..._programMemory.pendingMemory,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (args.length > fnInit.params.length) {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
`Too many arguments passed to function ${declaration.id.name}`,
|
|
||||||
[[declaration.start, declaration.end]]
|
|
||||||
)
|
|
||||||
} else if (args.length < fnInit.params.length) {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
`Too few arguments passed to function ${declaration.id.name}`,
|
|
||||||
[[declaration.start, declaration.end]]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fnInit.params.forEach((param, index) => {
|
|
||||||
fnMemory = addItemToMemory(
|
|
||||||
fnMemory,
|
|
||||||
param.name,
|
|
||||||
[sourceRange],
|
|
||||||
{
|
|
||||||
type: 'userVal',
|
|
||||||
value: args[index],
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const prom = _executor(
|
|
||||||
fnInit.body,
|
|
||||||
fnMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
{
|
|
||||||
bodyType: 'block',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
const result = (await prom).return
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'MemberExpression') {
|
|
||||||
await Promise.all([...proms]) // TODO wait for previous promises, does that makes sense?
|
|
||||||
const prom = getMemberExpressionResult(
|
|
||||||
declaration.init,
|
|
||||||
_programMemory
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
promisifyMemoryItem({
|
|
||||||
type: 'userVal',
|
|
||||||
value: prom,
|
|
||||||
__meta,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (declaration.init.type === 'CallExpression') {
|
|
||||||
const prom = executeCallExpression(
|
|
||||||
_programMemory,
|
|
||||||
declaration.init,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory = addItemToMemory(
|
|
||||||
_programMemory,
|
|
||||||
variableName,
|
|
||||||
[sourceRange],
|
|
||||||
prom.then((a) => {
|
|
||||||
return a?.type === 'sketchGroup' || a?.type === 'extrudeGroup'
|
|
||||||
? a
|
|
||||||
: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: a,
|
|
||||||
__meta,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
'Unsupported declaration type: ' + declaration.init.type,
|
|
||||||
[[declaration.start, declaration.end]]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (statement.type === 'ExpressionStatement') {
|
|
||||||
const expression = statement.expression
|
|
||||||
if (expression.type === 'CallExpression') {
|
|
||||||
const functionName = expression.callee.name
|
|
||||||
const args = expression.arguments.map((arg) => {
|
|
||||||
if (arg.type === 'Literal') {
|
|
||||||
return arg.value
|
|
||||||
} else if (arg.type === 'Identifier') {
|
|
||||||
return _programMemory.root[arg.name]?.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if ('show' === functionName) {
|
|
||||||
if (options.bodyType !== 'root') {
|
|
||||||
throw new KCLSemanticError(
|
|
||||||
`Cannot call ${functionName} outside of a root`,
|
|
||||||
[[statement.start, statement.end]]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_programMemory.return = expression.arguments as any // todo memory redo
|
|
||||||
} else {
|
|
||||||
if (_programMemory.root[functionName] == undefined) {
|
|
||||||
throw new KCLSemanticError(`No such name ${functionName} defined`, [
|
|
||||||
[statement.start, statement.end],
|
|
||||||
])
|
|
||||||
}
|
|
||||||
_programMemory.root[functionName].value(...args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (statement.type === 'ReturnStatement') {
|
|
||||||
if (statement.argument.type === 'BinaryExpression') {
|
|
||||||
const prom = getBinaryExpressionResult(
|
|
||||||
statement.argument,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
_programMemory.return = await prom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(proms)
|
|
||||||
return _programMemory
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMemberExpressionResult(
|
|
||||||
expression: MemberExpression,
|
|
||||||
programMemory: ProgramMemory
|
|
||||||
) {
|
|
||||||
const propertyName = (
|
|
||||||
expression.property.type === 'Identifier'
|
|
||||||
? expression.property.name
|
|
||||||
: expression.property.value
|
|
||||||
) as any
|
|
||||||
const object: any =
|
|
||||||
expression.object.type === 'MemberExpression'
|
|
||||||
? getMemberExpressionResult(expression.object, programMemory)
|
|
||||||
: programMemory.root[expression.object.name]?.value
|
|
||||||
return object?.[propertyName]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBinaryExpressionResult(
|
|
||||||
expression: BinaryExpression,
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const _pipeInfo = {
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
const left = await getBinaryPartResult(
|
|
||||||
expression.left,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
const right = await getBinaryPartResult(
|
|
||||||
expression.right,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
if (expression.operator === '+') return left + right
|
|
||||||
if (expression.operator === '-') return left - right
|
|
||||||
if (expression.operator === '*') return left * right
|
|
||||||
if (expression.operator === '/') return left / right
|
|
||||||
if (expression.operator === '%') return left % right
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBinaryPartResult(
|
|
||||||
part: BinaryPart,
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
): Promise<any> {
|
|
||||||
const _pipeInfo = {
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
if (part.type === 'Literal') {
|
|
||||||
return part.value
|
|
||||||
} else if (part.type === 'Identifier') {
|
|
||||||
return programMemory.root[part.name].value
|
|
||||||
} else if (part.type === 'BinaryExpression') {
|
|
||||||
const prom = getBinaryExpressionResult(
|
|
||||||
part,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
const result = await prom
|
|
||||||
return result
|
|
||||||
} else if (part.type === 'CallExpression') {
|
|
||||||
const result = await executeCallExpression(
|
|
||||||
programMemory,
|
|
||||||
part,
|
|
||||||
engineCommandManager,
|
|
||||||
[],
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUnaryExpressionResult(
|
|
||||||
expression: UnaryExpression,
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return -(await getBinaryPartResult(
|
|
||||||
expression.argument,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
{
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPipeExpressionResult(
|
|
||||||
expression: PipeExpression,
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
previousPathToNode: PathToNode = []
|
|
||||||
) {
|
|
||||||
const executedBody = await executePipeBody(
|
|
||||||
expression.body,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode
|
|
||||||
)
|
|
||||||
const result = executedBody[executedBody.length - 1]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executePipeBody(
|
|
||||||
body: PipeExpression['body'],
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
previousPathToNode: PathToNode = [],
|
|
||||||
expressionIndex = 0,
|
|
||||||
previousResults: any[] = []
|
|
||||||
): Promise<any[]> {
|
|
||||||
if (expressionIndex === body.length) {
|
|
||||||
return previousResults
|
|
||||||
}
|
|
||||||
const expression = body[expressionIndex]
|
|
||||||
if (expression.type === 'BinaryExpression') {
|
|
||||||
const result = await getBinaryExpressionResult(
|
|
||||||
expression,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
return executePipeBody(
|
return memory
|
||||||
body,
|
} catch (e: any) {
|
||||||
programMemory,
|
const parsed: RustKclError = JSON.parse(e.toString())
|
||||||
engineCommandManager,
|
const kclError = new KCLError(
|
||||||
previousPathToNode,
|
parsed.kind,
|
||||||
expressionIndex + 1,
|
parsed.msg,
|
||||||
[...previousResults, result]
|
rangeTypeFix(parsed.sourceRanges)
|
||||||
)
|
)
|
||||||
} else if (expression.type === 'CallExpression') {
|
|
||||||
return await executeCallExpression(
|
|
||||||
programMemory,
|
|
||||||
expression,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode,
|
|
||||||
{
|
|
||||||
isInPipe: true,
|
|
||||||
previousResults,
|
|
||||||
expressionIndex,
|
|
||||||
body,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new KCLSyntaxError('Invalid pipe expression', [
|
console.log(kclError)
|
||||||
[expression.start, expression.end],
|
throw kclError
|
||||||
])
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function executeObjectExpression(
|
|
||||||
_programMemory: ProgramMemory,
|
|
||||||
objExp: ObjectExpression,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const _pipeInfo = {
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
const obj: { [key: string]: any } = {}
|
|
||||||
const proms: Promise<any>[] = []
|
|
||||||
objExp.properties.forEach(async (property) => {
|
|
||||||
if (property.type === 'ObjectProperty') {
|
|
||||||
if (property.value.type === 'Literal') {
|
|
||||||
obj[property.key.name] = property.value.value
|
|
||||||
} else if (property.value.type === 'BinaryExpression') {
|
|
||||||
const prom = getBinaryExpressionResult(
|
|
||||||
property.value,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
obj[property.key.name] = await prom
|
|
||||||
} else if (property.value.type === 'PipeExpression') {
|
|
||||||
const prom = getPipeExpressionResult(
|
|
||||||
property.value,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
obj[property.key.name] = await prom
|
|
||||||
} else if (property.value.type === 'Identifier') {
|
|
||||||
obj[property.key.name] = (
|
|
||||||
await getMemoryItem(_programMemory, property.value.name, [
|
|
||||||
[property.value.start, property.value.end],
|
|
||||||
])
|
|
||||||
).value
|
|
||||||
} else if (property.value.type === 'ObjectExpression') {
|
|
||||||
const prom = executeObjectExpression(
|
|
||||||
_programMemory,
|
|
||||||
property.value,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
obj[property.key.name] = await prom
|
|
||||||
} else if (property.value.type === 'ArrayExpression') {
|
|
||||||
const prom = executeArrayExpression(
|
|
||||||
_programMemory,
|
|
||||||
property.value,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
obj[property.key.name] = await prom
|
|
||||||
} else if (property.value.type === 'CallExpression') {
|
|
||||||
const prom = executeCallExpression(
|
|
||||||
_programMemory,
|
|
||||||
property.value,
|
|
||||||
engineCommandManager,
|
|
||||||
[],
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
const result = await prom
|
|
||||||
obj[property.key.name] = result
|
|
||||||
} else if (property.value.type === 'UnaryExpression') {
|
|
||||||
const prom = getUnaryExpressionResult(
|
|
||||||
property.value,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
proms.push(prom)
|
|
||||||
obj[property.key.name] = await prom
|
|
||||||
} else {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
`Unexpected property type ${property.value.type} in object expression`,
|
|
||||||
[[property.value.start, property.value.end]]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new KCLSyntaxError(
|
|
||||||
`Unexpected property type ${property.type} in object expression`,
|
|
||||||
[[property.value.start, property.value.end]]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await Promise.all(proms)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeArrayExpression(
|
|
||||||
_programMemory: ProgramMemory,
|
|
||||||
arrExp: ArrayExpression,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const _pipeInfo = {
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
return await Promise.all(
|
|
||||||
arrExp.elements.map((el) => {
|
|
||||||
if (el.type === 'Literal') {
|
|
||||||
return el.value
|
|
||||||
} else if (el.type === 'Identifier') {
|
|
||||||
return _programMemory.root?.[el.name]?.value
|
|
||||||
} else if (el.type === 'BinaryExpression') {
|
|
||||||
return getBinaryExpressionResult(
|
|
||||||
el,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
} else if (el.type === 'ObjectExpression') {
|
|
||||||
return executeObjectExpression(_programMemory, el, engineCommandManager)
|
|
||||||
} else if (el.type === 'CallExpression') {
|
|
||||||
const result: any = executeCallExpression(
|
|
||||||
_programMemory,
|
|
||||||
el,
|
|
||||||
engineCommandManager,
|
|
||||||
[],
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
} else if (el.type === 'UnaryExpression') {
|
|
||||||
return getUnaryExpressionResult(
|
|
||||||
el,
|
|
||||||
_programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
{
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw new KCLTypeError('Invalid argument type', [[el.start, el.end]])
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCallExpression(
|
|
||||||
programMemory: ProgramMemory,
|
|
||||||
expression: CallExpression,
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
previousPathToNode: PathToNode = [],
|
|
||||||
pipeInfo: {
|
|
||||||
isInPipe: boolean
|
|
||||||
previousResults: any[]
|
|
||||||
expressionIndex: number
|
|
||||||
body: PipeExpression['body']
|
|
||||||
sourceRangeOverride?: SourceRange
|
|
||||||
} = {
|
|
||||||
isInPipe: false,
|
|
||||||
previousResults: [],
|
|
||||||
expressionIndex: 0,
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
isInPipe,
|
|
||||||
previousResults,
|
|
||||||
expressionIndex,
|
|
||||||
body,
|
|
||||||
sourceRangeOverride,
|
|
||||||
} = pipeInfo
|
|
||||||
const functionName = expression?.callee?.name
|
|
||||||
const _pipeInfo = {
|
|
||||||
...pipeInfo,
|
|
||||||
isInPipe: false,
|
|
||||||
}
|
|
||||||
const fnArgs = await Promise.all(
|
|
||||||
expression?.arguments?.map(async (arg) => {
|
|
||||||
if (arg.type === 'Literal') {
|
|
||||||
return arg.value
|
|
||||||
} else if (arg.type === 'Identifier') {
|
|
||||||
await new Promise((r) => setTimeout(r)) // push into next even loop, but also probably should fix this
|
|
||||||
const temp = await getMemoryItem(programMemory, arg.name, [
|
|
||||||
[arg.start, arg.end],
|
|
||||||
])
|
|
||||||
return temp?.type === 'userVal' ? temp.value : temp
|
|
||||||
} else if (arg.type === 'PipeSubstitution') {
|
|
||||||
return previousResults[expressionIndex - 1]
|
|
||||||
} else if (arg.type === 'ArrayExpression') {
|
|
||||||
return await executeArrayExpression(
|
|
||||||
programMemory,
|
|
||||||
arg,
|
|
||||||
engineCommandManager,
|
|
||||||
pipeInfo
|
|
||||||
)
|
|
||||||
} else if (arg.type === 'CallExpression') {
|
|
||||||
const result: any = await executeCallExpression(
|
|
||||||
programMemory,
|
|
||||||
arg,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
} else if (arg.type === 'ObjectExpression') {
|
|
||||||
return await executeObjectExpression(
|
|
||||||
programMemory,
|
|
||||||
arg,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
} else if (arg.type === 'UnaryExpression') {
|
|
||||||
return getUnaryExpressionResult(
|
|
||||||
arg,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
} else if (arg.type === 'BinaryExpression') {
|
|
||||||
return getBinaryExpressionResult(
|
|
||||||
arg,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
_pipeInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw new KCLSyntaxError('Invalid argument type in function call', [
|
|
||||||
[arg.start, arg.end],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (functionName in internalFns) {
|
|
||||||
const fnNameWithSketchOrExtrude = functionName as InternalFnNames
|
|
||||||
const result = await internalFns[fnNameWithSketchOrExtrude](
|
|
||||||
{
|
|
||||||
programMemory,
|
|
||||||
sourceRange: sourceRangeOverride || [expression.start, expression.end],
|
|
||||||
engineCommandManager,
|
|
||||||
code: JSON.stringify(expression),
|
|
||||||
},
|
|
||||||
fnArgs[0],
|
|
||||||
fnArgs[1],
|
|
||||||
fnArgs[2]
|
|
||||||
)
|
|
||||||
return isInPipe
|
|
||||||
? await executePipeBody(
|
|
||||||
body,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode,
|
|
||||||
expressionIndex + 1,
|
|
||||||
[...previousResults, result]
|
|
||||||
)
|
|
||||||
: result
|
|
||||||
}
|
|
||||||
const result = await programMemory.root[functionName].value(...fnArgs)
|
|
||||||
return isInPipe
|
|
||||||
? await executePipeBody(
|
|
||||||
body,
|
|
||||||
programMemory,
|
|
||||||
engineCommandManager,
|
|
||||||
previousPathToNode,
|
|
||||||
expressionIndex + 1,
|
|
||||||
[...previousResults, result]
|
|
||||||
)
|
|
||||||
: result
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
||||||
import { lexer } from './tokeniser'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -21,7 +20,7 @@ const sk3 = startSketchAt([0, 0])
|
|||||||
lineToSubstringIndex + subStr.length,
|
lineToSubstringIndex + subStr.length,
|
||||||
]
|
]
|
||||||
|
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||||
const { node } = getNodeFromPath<any>(ast, nodePath)
|
const { node } = getNodeFromPath<any>(ast, nodePath)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
moveValueIntoNewVariable,
|
moveValueIntoNewVariable,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
import { recast } from './recast'
|
import { recast } from './recast'
|
||||||
import { lexer } from './tokeniser'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
|
|
||||||
@ -104,11 +103,10 @@ describe('Testing addSketchTo', () => {
|
|||||||
it('should add a sketch to a program', () => {
|
it('should add a sketch to a program', () => {
|
||||||
const result = addSketchTo(
|
const result = addSketchTo(
|
||||||
{
|
{
|
||||||
type: 'Program',
|
|
||||||
body: [],
|
body: [],
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
nonCodeMeta: { noneCodeNodes: {} },
|
nonCodeMeta: { noneCodeNodes: {}, start: null },
|
||||||
},
|
},
|
||||||
'yz'
|
'yz'
|
||||||
)
|
)
|
||||||
@ -127,7 +125,7 @@ function giveSketchFnCallTagTestHelper(
|
|||||||
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
|
// giveSketchFnCallTag inputs and outputs an ast, which is very verbose for testing
|
||||||
// this wrapper changes the input and output to code
|
// this wrapper changes the input and output to code
|
||||||
// making it more of an integration test, but easier to read the test intention is the goal
|
// making it more of an integration test, but easier to read the test intention is the goal
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const start = code.indexOf(searchStr)
|
const start = code.indexOf(searchStr)
|
||||||
const range: [number, number] = [start, start + searchStr.length]
|
const range: [number, number] = [start, start + searchStr.length]
|
||||||
const { modifiedAst, tag, isTagExisting } = giveSketchFnCallTag(ast, range)
|
const { modifiedAst, tag, isTagExisting } = giveSketchFnCallTag(ast, range)
|
||||||
@ -184,18 +182,18 @@ describe('Testing moveValueIntoNewVariable', () => {
|
|||||||
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
||||||
const abc = 3
|
const abc = 3
|
||||||
const identifierGuy = 5
|
const identifierGuy = 5
|
||||||
|
const yo = 5 + 6
|
||||||
const part001 = startSketchAt([-1.2, 4.83])
|
const part001 = startSketchAt([-1.2, 4.83])
|
||||||
|> line([2.8, 0], %)
|
|> line([2.8, 0], %)
|
||||||
|> angledLine([100 + 100, 3.09], %)
|
|> angledLine([100 + 100, 3.09], %)
|
||||||
|> angledLine([abc, 3.09], %)
|
|> angledLine([abc, 3.09], %)
|
||||||
|> angledLine([def('yo'), 3.09], %)
|
|> angledLine([def(yo), 3.09], %)
|
||||||
|> angledLine([ghi(%), 3.09], %)
|
|> angledLine([ghi(%), 3.09], %)
|
||||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
|> angledLine([jkl(yo) + 2, 3.09], %)
|
||||||
const yo = 5 + 6
|
|
||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('should move a binary expression into a new variable', async () => {
|
it('should move a binary expression into a new variable', async () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('100 + 100') + 1
|
const startIndex = code.indexOf('100 + 100') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -209,7 +207,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a value into a new variable', async () => {
|
it('should move a value into a new variable', async () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('2.8') + 1
|
const startIndex = code.indexOf('2.8') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -223,7 +221,7 @@ show(part001)`
|
|||||||
expect(newCode).toContain(`line([newVar, 0], %)`)
|
expect(newCode).toContain(`line([newVar, 0], %)`)
|
||||||
})
|
})
|
||||||
it('should move a callExpression into a new variable', async () => {
|
it('should move a callExpression into a new variable', async () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('def(')
|
const startIndex = code.indexOf('def(')
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -233,11 +231,11 @@ show(part001)`
|
|||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
const newCode = recast(modifiedAst)
|
const newCode = recast(modifiedAst)
|
||||||
expect(newCode).toContain(`const newVar = def('yo')`)
|
expect(newCode).toContain(`const newVar = def(yo)`)
|
||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a binary expression with call expression into a new variable', async () => {
|
it('should move a binary expression with call expression into a new variable', async () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('jkl(') + 1
|
const startIndex = code.indexOf('jkl(') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
@ -247,11 +245,11 @@ show(part001)`
|
|||||||
'newVar'
|
'newVar'
|
||||||
)
|
)
|
||||||
const newCode = recast(modifiedAst)
|
const newCode = recast(modifiedAst)
|
||||||
expect(newCode).toContain(`const newVar = jkl('yo') + 2`)
|
expect(newCode).toContain(`const newVar = jkl(yo) + 2`)
|
||||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('should move a identifier into a new variable', async () => {
|
it('should move a identifier into a new variable', async () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const startIndex = code.indexOf('identifierGuy +') + 1
|
const startIndex = code.indexOf('identifierGuy +') + 1
|
||||||
const { modifiedAst } = moveValueIntoNewVariable(
|
const { modifiedAst } = moveValueIntoNewVariable(
|
||||||
|
@ -452,7 +452,7 @@ export function createPipeExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
body,
|
body,
|
||||||
nonCodeMeta: { noneCodeNodes: {} },
|
nonCodeMeta: { noneCodeNodes: {}, start: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
isTypeInValue,
|
isTypeInValue,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { lexer } from './tokeniser'
|
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -37,7 +36,7 @@ const variableBelowShouldNotBeIncluded = 3
|
|||||||
|
|
||||||
show(part001)`
|
show(part001)`
|
||||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
|
|
||||||
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
|
||||||
@ -71,7 +70,7 @@ const yo = 5 + 6
|
|||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('find a safe binaryExpression', () => {
|
it('find a safe binaryExpression', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('100 + 100') + 2
|
const rangeStart = code.indexOf('100 + 100') + 2
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -85,7 +84,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find a safe Identifier', () => {
|
it('find a safe Identifier', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('abc')
|
const rangeStart = code.indexOf('abc')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -93,7 +92,7 @@ show(part001)`
|
|||||||
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
|
expect(code.slice(result.value.start, result.value.end)).toBe('abc')
|
||||||
})
|
})
|
||||||
it('find a safe CallExpression', () => {
|
it('find a safe CallExpression', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('def')
|
const rangeStart = code.indexOf('def')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -107,7 +106,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('ghi')
|
const rangeStart = code.indexOf('ghi')
|
||||||
const range: [number, number] = [rangeStart, rangeStart]
|
const range: [number, number] = [rangeStart, rangeStart]
|
||||||
const result = isNodeSafeToReplace(ast, range)
|
const result = isNodeSafeToReplace(ast, range)
|
||||||
@ -116,7 +115,7 @@ show(part001)`
|
|||||||
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
|
expect(code.slice(result.value.start, result.value.end)).toBe('ghi(%)')
|
||||||
})
|
})
|
||||||
it('find an UNsafe Identifier, as it is a callee', () => {
|
it('find an UNsafe Identifier, as it is a callee', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('ine([2.8,')
|
const rangeStart = code.indexOf('ine([2.8,')
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(false)
|
expect(result.isSafe).toBe(false)
|
||||||
@ -126,7 +125,7 @@ show(part001)`
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
it("find a safe BinaryExpression that's assigned to a variable", () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('5 + 6') + 1
|
const rangeStart = code.indexOf('5 + 6') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -140,7 +139,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`const yo = replaceName`)
|
expect(outCode).toContain(`const yo = replaceName`)
|
||||||
})
|
})
|
||||||
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
it('find a safe BinaryExpression that has a CallExpression within', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('jkl') + 1
|
const rangeStart = code.indexOf('jkl') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -156,7 +155,7 @@ show(part001)`
|
|||||||
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
expect(outCode).toContain(`angledLine([replaceName, 3.09], %)`)
|
||||||
})
|
})
|
||||||
it('find a safe BinaryExpression within a CallExpression', () => {
|
it('find a safe BinaryExpression within a CallExpression', () => {
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const rangeStart = code.indexOf('identifierGuy') + 1
|
const rangeStart = code.indexOf('identifierGuy') + 1
|
||||||
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart])
|
||||||
expect(result.isSafe).toBe(true)
|
expect(result.isSafe).toBe(true)
|
||||||
@ -204,7 +203,7 @@ show(part001)`
|
|||||||
it('finds the second line when cursor is put at the end', () => {
|
it('finds the second line when cursor is put at the end', () => {
|
||||||
const searchLn = `line([0.94, 2.61], %)`
|
const searchLn = `line([0.94, 2.61], %)`
|
||||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
['body', ''],
|
['body', ''],
|
||||||
@ -219,7 +218,7 @@ show(part001)`
|
|||||||
it('finds the last line when cursor is put at the end', () => {
|
it('finds the last line when cursor is put at the end', () => {
|
||||||
const searchLn = `line([-0.21, -1.4], %)`
|
const searchLn = `line([-0.21, -1.4], %)`
|
||||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
const expected = [
|
const expected = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { recast } from './recast'
|
import { recast } from './recast'
|
||||||
import { abstractSyntaxTree } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { Program } from './abstractSyntaxTreeTypes'
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
import { lexer, Token } from './tokeniser'
|
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
@ -342,11 +341,7 @@ describe('it recasts binary expression using brackets where needed', () => {
|
|||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
function code2ast(code: string): { ast: Program; tokens: Token[] } {
|
function code2ast(code: string): { ast: Program } {
|
||||||
const tokens = lexer(code)
|
const ast = parser_wasm(code)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
return { ast }
|
||||||
return {
|
|
||||||
ast,
|
|
||||||
tokens,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
import { Program } from './abstractSyntaxTreeTypes'
|
import { Program } from './abstractSyntaxTreeTypes'
|
||||||
import { recast_js } from '../wasm-lib/pkg/wasm_lib'
|
import { recast_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
|
||||||
export const recast = (ast: Program): string => recast_js(JSON.stringify(ast))
|
export const recast = (ast: Program): string => {
|
||||||
|
try {
|
||||||
|
const s: string = recast_wasm(JSON.stringify(ast))
|
||||||
|
return s
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: do something real with the error.
|
||||||
|
console.log('recast', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { InternalFn } from './stdTypes'
|
|
||||||
import {
|
|
||||||
ExtrudeGroup,
|
|
||||||
ExtrudeSurface,
|
|
||||||
SketchGroup,
|
|
||||||
Position,
|
|
||||||
Rotation,
|
|
||||||
} from '../executor'
|
|
||||||
import { clockwiseSign } from './std'
|
|
||||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
|
||||||
|
|
||||||
export const extrude: InternalFn = (
|
|
||||||
{ sourceRange, engineCommandManager, code },
|
|
||||||
length: number,
|
|
||||||
sketchVal: SketchGroup
|
|
||||||
): ExtrudeGroup => {
|
|
||||||
const sketch = sketchVal
|
|
||||||
const { position, rotation } = sketchVal
|
|
||||||
|
|
||||||
const id = generateUuidFromHashSeed(
|
|
||||||
JSON.stringify({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data: {
|
|
||||||
length,
|
|
||||||
sketchVal,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const extrudeSurfaces: ExtrudeSurface[] = []
|
|
||||||
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
|
|
||||||
engineCommandManager.sendModellingCommand({
|
|
||||||
id,
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
length,
|
|
||||||
extrusionDirection: extrusionDirection,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
range: sourceRange,
|
|
||||||
command: {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'extrude',
|
|
||||||
target: sketch.id,
|
|
||||||
distance: length,
|
|
||||||
cap: true,
|
|
||||||
},
|
|
||||||
cmd_id: id,
|
|
||||||
file_id: uuidv4(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'extrudeGroup',
|
|
||||||
id,
|
|
||||||
value: extrudeSurfaces, // TODO, this is just an empty array now, should be deleted.
|
|
||||||
height: length,
|
|
||||||
position,
|
|
||||||
rotation,
|
|
||||||
__meta: [
|
|
||||||
{
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sourceRange: sketchVal.__meta[0].sourceRange,
|
|
||||||
pathToNode: sketchVal.__meta[0].pathToNode,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getExtrudeWallTransform: InternalFn = (
|
|
||||||
_,
|
|
||||||
pathName: string,
|
|
||||||
extrudeGroup: ExtrudeGroup
|
|
||||||
): {
|
|
||||||
position: Position
|
|
||||||
quaternion: Rotation
|
|
||||||
} => {
|
|
||||||
const path = extrudeGroup?.value.find((path) => path.name === pathName)
|
|
||||||
if (!path) throw new Error(`Could not find path with name ${pathName}`)
|
|
||||||
return {
|
|
||||||
position: path.position,
|
|
||||||
quaternion: path.rotation,
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,8 +5,7 @@ import {
|
|||||||
getYComponent,
|
getYComponent,
|
||||||
getXComponent,
|
getXComponent,
|
||||||
} from './sketch'
|
} from './sketch'
|
||||||
import { lexer } from '../tokeniser'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
import { abstractSyntaxTree } from '../abstractSyntaxTree'
|
|
||||||
import { getNodePathFromSourceRange } from '../queryAst'
|
import { getNodePathFromSourceRange } from '../queryAst'
|
||||||
import { recast } from '../recast'
|
import { recast } from '../recast'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
@ -106,7 +105,7 @@ const mySketch001 = startSketchAt([0, 0])
|
|||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const code = genCode(lineToChange)
|
const code = genCode(lineToChange)
|
||||||
const expectedCode = genCode(lineAfterChange)
|
const expectedCode = genCode(lineAfterChange)
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
const { modifiedAst } = changeSketchArguments(
|
const { modifiedAst } = changeSketchArguments(
|
||||||
@ -135,7 +134,6 @@ show(mySketch001)`
|
|||||||
|
|
||||||
describe('testing addNewSketchLn', () => {
|
describe('testing addNewSketchLn', () => {
|
||||||
const lineToChange = 'lineTo([-1.59, -1.54], %)'
|
const lineToChange = 'lineTo([-1.59, -1.54], %)'
|
||||||
const lineAfterChange = 'lineTo([2, 3], %)'
|
|
||||||
test('addNewSketchLn', async () => {
|
test('addNewSketchLn', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = `
|
const code = `
|
||||||
@ -144,9 +142,10 @@ const mySketch001 = startSketchAt([0, 0])
|
|||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
|
expect(sourceStart).toBe(66)
|
||||||
const { modifiedAst } = addNewSketchLn({
|
const { modifiedAst } = addNewSketchLn({
|
||||||
node: ast,
|
node: ast,
|
||||||
programMemory,
|
programMemory,
|
||||||
@ -183,7 +182,7 @@ describe('testing addTagForSketchOnFace', () => {
|
|||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)`
|
show(mySketch001)`
|
||||||
const code = genCode(originalLine)
|
const code = genCode(originalLine)
|
||||||
const ast = abstractSyntaxTree(lexer(code))
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(originalLine)
|
const sourceStart = code.indexOf(originalLine)
|
||||||
const sourceRange: [number, number] = [
|
const sourceRange: [number, number] = [
|
||||||
|
@ -22,14 +22,8 @@ import {
|
|||||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
import {
|
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||||
SketchLineHelper,
|
|
||||||
ModifyAstBase,
|
|
||||||
InternalFn,
|
|
||||||
TransformCallback,
|
|
||||||
} from './stdTypes'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
@ -43,10 +37,7 @@ import {
|
|||||||
} from '../modifyAst'
|
} from '../modifyAst'
|
||||||
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
||||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import {
|
import { perpendicularDistance } from 'sketch-helpers'
|
||||||
intersectionWithParallelLine,
|
|
||||||
perpendicularDistance,
|
|
||||||
} from 'sketch-helpers'
|
|
||||||
|
|
||||||
export type Coords2d = [number, number]
|
export type Coords2d = [number, number]
|
||||||
|
|
||||||
@ -116,54 +107,6 @@ function makeId(seed: string | any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const lineTo: SketchLineHelper = {
|
export const lineTo: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
to: [number, number]
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
): SketchGroup => {
|
|
||||||
if (!previousSketch)
|
|
||||||
throw new Error('lineTo must be called after startSketchAt')
|
|
||||||
const sketchGroup = { ...previousSketch }
|
|
||||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
|
||||||
const to = 'to' in data ? data.to : data
|
|
||||||
|
|
||||||
const lineData: LineData = {
|
|
||||||
from: [...from, 0],
|
|
||||||
to: [...to, 0],
|
|
||||||
}
|
|
||||||
const id = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
// engineCommandManager.sendModellingCommand({
|
|
||||||
// id,
|
|
||||||
// params: [lineData, previousSketch],
|
|
||||||
// range: sourceRange,
|
|
||||||
// })
|
|
||||||
const currentPath: Path = {
|
|
||||||
type: 'toPoint',
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
__geoMeta: {
|
|
||||||
sourceRange,
|
|
||||||
id,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if ('tag' in data) {
|
|
||||||
currentPath.name = data.tag
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...sketchGroup,
|
|
||||||
value: [...sketchGroup.value, currentPath],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -231,78 +174,6 @@ export const lineTo: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const line: SketchLineHelper = {
|
export const line: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| 'default'
|
|
||||||
| {
|
|
||||||
to: [number, number] | 'default'
|
|
||||||
// name?: string
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
): SketchGroup => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const sketchGroup = { ...previousSketch }
|
|
||||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
|
||||||
let args: [number, number] = [0.2, 1]
|
|
||||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
|
||||||
args = data.to
|
|
||||||
} else if (data !== 'default' && !('to' in data)) {
|
|
||||||
args = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const to: [number, number] = [from[0] + args[0], from[1] + args[1]]
|
|
||||||
const lineData: LineData = {
|
|
||||||
from: [...from, 0],
|
|
||||||
to: [...to, 0],
|
|
||||||
}
|
|
||||||
const id = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendModellingCommand({
|
|
||||||
id,
|
|
||||||
params: [lineData, previousSketch],
|
|
||||||
range: sourceRange,
|
|
||||||
command: {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'extend_path',
|
|
||||||
path: sketchGroup.id,
|
|
||||||
segment: {
|
|
||||||
type: 'line',
|
|
||||||
end: {
|
|
||||||
x: lineData.to[0],
|
|
||||||
y: lineData.to[1],
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cmd_id: id,
|
|
||||||
file_id: uuidv4(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const currentPath: Path = {
|
|
||||||
type: 'toPoint',
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
__geoMeta: {
|
|
||||||
id,
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (data !== 'default' && 'tag' in data) {
|
|
||||||
currentPath.name = data.tag
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...sketchGroup,
|
|
||||||
value: [...sketchGroup.value, currentPath],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
previousProgramMemory,
|
previousProgramMemory,
|
||||||
@ -396,25 +267,6 @@ export const line: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const xLineTo: SketchLineHelper = {
|
export const xLineTo: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
meta,
|
|
||||||
data:
|
|
||||||
| number
|
|
||||||
| {
|
|
||||||
to: number
|
|
||||||
// name?: string
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('bad bad bad')
|
|
||||||
const from = getCoordsFromPaths(
|
|
||||||
previousSketch,
|
|
||||||
previousSketch.value.length - 1
|
|
||||||
)
|
|
||||||
const [xVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
|
||||||
return lineTo.fn(meta, { to: [xVal, from[1]], tag }, previousSketch)
|
|
||||||
},
|
|
||||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||||
@ -463,25 +315,6 @@ export const xLineTo: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const yLineTo: SketchLineHelper = {
|
export const yLineTo: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
meta,
|
|
||||||
data:
|
|
||||||
| number
|
|
||||||
| {
|
|
||||||
to: number
|
|
||||||
// name?: string
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('bad bad bad')
|
|
||||||
const from = getCoordsFromPaths(
|
|
||||||
previousSketch,
|
|
||||||
previousSketch.value.length - 1
|
|
||||||
)
|
|
||||||
const [yVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
|
||||||
return lineTo.fn(meta, { to: [from[0], yVal], tag }, previousSketch)
|
|
||||||
},
|
|
||||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||||
@ -530,21 +363,6 @@ export const yLineTo: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const xLine: SketchLineHelper = {
|
export const xLine: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
meta,
|
|
||||||
data:
|
|
||||||
| number
|
|
||||||
| {
|
|
||||||
length: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('bad bad bad')
|
|
||||||
const [xVal, tag] =
|
|
||||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
|
||||||
return line.fn(meta, { to: [xVal, 0], tag }, previousSketch)
|
|
||||||
},
|
|
||||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||||
@ -595,22 +413,6 @@ export const xLine: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const yLine: SketchLineHelper = {
|
export const yLine: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
meta,
|
|
||||||
data:
|
|
||||||
| number
|
|
||||||
| {
|
|
||||||
length: number
|
|
||||||
// name?: string
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('bad bad bad')
|
|
||||||
const [yVal, tag] =
|
|
||||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
|
||||||
return line.fn(meta, { to: [0, yVal], tag }, previousSketch)
|
|
||||||
},
|
|
||||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||||
@ -655,57 +457,6 @@ export const yLine: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLine: SketchLineHelper = {
|
export const angledLine: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
angle: number
|
|
||||||
length: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const sketchGroup = { ...previousSketch }
|
|
||||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
|
||||||
const [angle, length] = 'angle' in data ? [data.angle, data.length] : data
|
|
||||||
const to: [number, number] = [
|
|
||||||
from[0] + length * Math.cos((angle * Math.PI) / 180),
|
|
||||||
from[1] + length * Math.sin((angle * Math.PI) / 180),
|
|
||||||
]
|
|
||||||
const lineData: LineData = {
|
|
||||||
from: [...from, 0],
|
|
||||||
to: [...to, 0],
|
|
||||||
}
|
|
||||||
const id = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
// engineCommandManager.sendModellingCommand({
|
|
||||||
// id,
|
|
||||||
// params: [lineData, previousSketch],
|
|
||||||
// range: sourceRange,
|
|
||||||
// })
|
|
||||||
const currentPath: Path = {
|
|
||||||
type: 'toPoint',
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
__geoMeta: {
|
|
||||||
id,
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if ('tag' in data) {
|
|
||||||
currentPath.name = data.tag
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...sketchGroup,
|
|
||||||
value: [...sketchGroup.value, currentPath],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -773,26 +524,6 @@ export const angledLine: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLineOfXLength: SketchLineHelper = {
|
export const angledLineOfXLength: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
angle: number
|
|
||||||
length: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const [angle, length, tag] =
|
|
||||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
|
||||||
return line.fn(
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
{ to: getYComponent(angle, length), tag },
|
|
||||||
previousSketch
|
|
||||||
)
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
previousProgramMemory,
|
previousProgramMemory,
|
||||||
@ -866,26 +597,6 @@ export const angledLineOfXLength: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLineOfYLength: SketchLineHelper = {
|
export const angledLineOfYLength: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
angle: number
|
|
||||||
length: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const [angle, length, tag] =
|
|
||||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
|
||||||
return line.fn(
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
{ to: getXComponent(angle, length), tag },
|
|
||||||
previousSketch
|
|
||||||
)
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
previousProgramMemory,
|
previousProgramMemory,
|
||||||
@ -960,33 +671,6 @@ export const angledLineOfYLength: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLineToX: SketchLineHelper = {
|
export const angledLineToX: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
angle: number
|
|
||||||
to: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const from = getCoordsFromPaths(
|
|
||||||
previousSketch,
|
|
||||||
previousSketch.value.length - 1
|
|
||||||
)
|
|
||||||
const [angle, xTo, tag] =
|
|
||||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
|
||||||
const xComponent = xTo - from[0]
|
|
||||||
const yComponent = xComponent * Math.tan((angle * Math.PI) / 180)
|
|
||||||
const yTo = from[1] + yComponent
|
|
||||||
return lineTo.fn(
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
{ to: [xTo, yTo], tag },
|
|
||||||
previousSketch
|
|
||||||
)
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -1056,33 +740,6 @@ export const angledLineToX: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLineToY: SketchLineHelper = {
|
export const angledLineToY: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| {
|
|
||||||
angle: number
|
|
||||||
to: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const from = getCoordsFromPaths(
|
|
||||||
previousSketch,
|
|
||||||
previousSketch.value.length - 1
|
|
||||||
)
|
|
||||||
const [angle, yTo, tag] =
|
|
||||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
|
||||||
const yComponent = yTo - from[1]
|
|
||||||
const xComponent = yComponent / Math.tan((angle * Math.PI) / 180)
|
|
||||||
const xTo = from[0] + xComponent
|
|
||||||
return lineTo.fn(
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
{ to: [xTo, yTo], tag },
|
|
||||||
previousSketch
|
|
||||||
)
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -1153,37 +810,6 @@ export const angledLineToY: SketchLineHelper = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const angledLineThatIntersects: SketchLineHelper = {
|
export const angledLineThatIntersects: SketchLineHelper = {
|
||||||
fn: (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data: {
|
|
||||||
angle: number
|
|
||||||
intersectTag: string
|
|
||||||
offset?: number
|
|
||||||
tag?: string
|
|
||||||
},
|
|
||||||
previousSketch: SketchGroup
|
|
||||||
) => {
|
|
||||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
|
||||||
const intersectPath = previousSketch.value.find(
|
|
||||||
({ name }) => name === data.intersectTag
|
|
||||||
)
|
|
||||||
if (!intersectPath) throw new Error('intersectTag must match a line')
|
|
||||||
const from = getCoordsFromPaths(
|
|
||||||
previousSketch,
|
|
||||||
previousSketch.value.length - 1
|
|
||||||
)
|
|
||||||
const to = intersectionWithParallelLine({
|
|
||||||
line1: [intersectPath.from, intersectPath.to],
|
|
||||||
line1Offset: data.offset || 0,
|
|
||||||
line2Point: from,
|
|
||||||
line2Angle: data.angle,
|
|
||||||
})
|
|
||||||
return lineTo.fn(
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
{ to, tag: data.tag },
|
|
||||||
previousSketch
|
|
||||||
)
|
|
||||||
},
|
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -1546,145 +1172,6 @@ function addTagWithTo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const close: InternalFn = (
|
|
||||||
{ sourceRange, engineCommandManager, code },
|
|
||||||
sketchGroup: SketchGroup
|
|
||||||
): SketchGroup => {
|
|
||||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
|
||||||
const to = sketchGroup.start
|
|
||||||
? sketchGroup.start.from
|
|
||||||
: getCoordsFromPaths(sketchGroup, 0)
|
|
||||||
|
|
||||||
const lineData: LineData = {
|
|
||||||
from: [...from, 0],
|
|
||||||
to: [...to, 0],
|
|
||||||
}
|
|
||||||
const id = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data: sketchGroup,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendModellingCommand({
|
|
||||||
id,
|
|
||||||
params: [lineData],
|
|
||||||
range: sourceRange,
|
|
||||||
command: {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'close_path',
|
|
||||||
path_id: sketchGroup.id,
|
|
||||||
},
|
|
||||||
cmd_id: id,
|
|
||||||
file_id: uuidv4(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentPath: Path = {
|
|
||||||
type: 'toPoint',
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
__geoMeta: {
|
|
||||||
id,
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const newValue = [...sketchGroup.value]
|
|
||||||
newValue.push(currentPath)
|
|
||||||
return {
|
|
||||||
...sketchGroup,
|
|
||||||
value: newValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startSketchAt: InternalFn = (
|
|
||||||
{ sourceRange, programMemory, engineCommandManager, code },
|
|
||||||
data:
|
|
||||||
| [number, number]
|
|
||||||
| 'default'
|
|
||||||
| {
|
|
||||||
to: [number, number] | 'default'
|
|
||||||
// name?: string
|
|
||||||
tag?: string
|
|
||||||
}
|
|
||||||
): SketchGroup => {
|
|
||||||
let to: [number, number] = [0, 0]
|
|
||||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
|
||||||
to = data.to
|
|
||||||
} else if (data !== 'default' && !('to' in data)) {
|
|
||||||
to = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineData: { to: [number, number, number] } = {
|
|
||||||
to: [...to, 0],
|
|
||||||
}
|
|
||||||
const id = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
const pathId = makeId({
|
|
||||||
code,
|
|
||||||
sourceRange,
|
|
||||||
data,
|
|
||||||
isPath: true,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendModellingCommand({
|
|
||||||
id: pathId,
|
|
||||||
params: [lineData],
|
|
||||||
range: sourceRange,
|
|
||||||
command: {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'start_path',
|
|
||||||
},
|
|
||||||
cmd_id: pathId,
|
|
||||||
file_id: uuidv4(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'move_path_pen',
|
|
||||||
path: pathId,
|
|
||||||
to: {
|
|
||||||
x: lineData.to[0],
|
|
||||||
y: lineData.to[1],
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cmd_id: id,
|
|
||||||
file_id: uuidv4(),
|
|
||||||
})
|
|
||||||
const currentPath: Path = {
|
|
||||||
type: 'base',
|
|
||||||
to,
|
|
||||||
from: to,
|
|
||||||
__geoMeta: {
|
|
||||||
id,
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (data !== 'default' && 'tag' in data) {
|
|
||||||
currentPath.name = data.tag
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'sketchGroup',
|
|
||||||
start: currentPath,
|
|
||||||
value: [],
|
|
||||||
position: [0, 0, 0],
|
|
||||||
rotation: [0, 0, 0, 1],
|
|
||||||
id: pathId,
|
|
||||||
__meta: [
|
|
||||||
{
|
|
||||||
sourceRange,
|
|
||||||
pathToNode: [], // TODO
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getYComponent(
|
export function getYComponent(
|
||||||
angleDegree: number,
|
angleDegree: number,
|
||||||
xComponent: number
|
xComponent: number
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { abstractSyntaxTree } from '../abstractSyntaxTree'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
import { SketchGroup } from '../executor'
|
import { SketchGroup } from '../executor'
|
||||||
import { lexer } from '../tokeniser'
|
|
||||||
import {
|
import {
|
||||||
ConstraintType,
|
ConstraintType,
|
||||||
getTransformInfos,
|
getTransformInfos,
|
||||||
@ -32,8 +31,7 @@ async function testingSwapSketchFnCall({
|
|||||||
type: 'default',
|
type: 'default',
|
||||||
range: [startIndex, startIndex + callToSwap.length],
|
range: [startIndex, startIndex + callToSwap.length],
|
||||||
}
|
}
|
||||||
const tokens = lexer(inputCode)
|
const ast = parser_wasm(inputCode)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const selections = {
|
const selections = {
|
||||||
codeBasedSelections: [range],
|
codeBasedSelections: [range],
|
||||||
@ -383,9 +381,7 @@ const part001 = startSketchAt([0, 0.04]) // segment-in-start
|
|||||||
|> xLine(3.54, %)
|
|> xLine(3.54, %)
|
||||||
show(part001)`
|
show(part001)`
|
||||||
it('normal case works', async () => {
|
it('normal case works', async () => {
|
||||||
const programMemory = await enginelessExecutor(
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
abstractSyntaxTree(lexer(code))
|
|
||||||
)
|
|
||||||
const index = code.indexOf('// normal-segment') - 7
|
const index = code.indexOf('// normal-segment') - 7
|
||||||
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
||||||
programMemory.root['part001'] as SketchGroup,
|
programMemory.root['part001'] as SketchGroup,
|
||||||
@ -395,17 +391,16 @@ show(part001)`
|
|||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to: [5.62, 1.79],
|
to: [5.62, 1.79],
|
||||||
from: [3.48, 0.44],
|
from: [3.48, 0.44],
|
||||||
|
name: '',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('verify it works when the segment is in the `start` property', async () => {
|
it('verify it works when the segment is in the `start` property', async () => {
|
||||||
const programMemory = await enginelessExecutor(
|
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||||
abstractSyntaxTree(lexer(code))
|
|
||||||
)
|
|
||||||
const index = code.indexOf('// segment-in-start') - 7
|
const index = code.indexOf('// segment-in-start') - 7
|
||||||
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
const { __geoMeta, ...segment } = getSketchSegmentFromSourceRange(
|
||||||
programMemory.root['part001'] as SketchGroup,
|
programMemory.root['part001'] as SketchGroup,
|
||||||
[index, index]
|
[index, index]
|
||||||
).segment
|
).segment
|
||||||
expect(segment).toEqual({ type: 'base', to: [0, 0.04], from: [0, 0.04] })
|
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|