Compare commits
135 Commits
Author | SHA1 | Date | |
---|---|---|---|
888104080e | |||
b6769889e3 | |||
a32258dac4 | |||
18dbbad244 | |||
b67c16cc9d | |||
ad482641ef | |||
9ee24845a1 | |||
e69d263252 | |||
111738f38e | |||
e34501cc5a | |||
c767c1c3a6 | |||
e399a8f938 | |||
59d5f2524a | |||
b47ebd14d2 | |||
e74bcd0695 | |||
22161ec386 | |||
ada46c4317 | |||
6675fa8d1e | |||
075d2debce | |||
488e41ac0e | |||
8147f5f1eb | |||
bc7e9d9789 | |||
8d493d6517 | |||
9fa98d6f3f | |||
24a31c94e7 | |||
76e3207251 | |||
e2237fa9f6 | |||
ae4aa82129 | |||
14b287a746 | |||
dd1b7631fa | |||
f98f782b40 | |||
01f5ecdc36 | |||
5297d3e142 | |||
f71f44968b | |||
7b79998c40 | |||
4632d407c1 | |||
58d7e59ca4 | |||
f592d8db84 | |||
31eca3728e | |||
c5d8779af4 | |||
cf686bdeb0 | |||
ae7143a94f | |||
f2b24849b3 | |||
35d6530406 | |||
01208221c7 | |||
fbbed3fbfb | |||
ce51f26701 | |||
caddac5059 | |||
54751aa7bb | |||
7b7d5e5f5e | |||
f7971bddef | |||
e4f2e66029 | |||
663c396128 | |||
8db86a6783 | |||
d7ad7c749e | |||
6e3c642d22 | |||
4d7433ff3a | |||
4e93146559 | |||
731a9bfbdb | |||
cdb4c36cf5 | |||
66ba60dc8e | |||
8fcc8cdd17 | |||
bba9bdc563 | |||
760a180f56 | |||
0eeff8cb45 | |||
3c76721159 | |||
6ac79ae645 | |||
90d7c33c92 | |||
e02bc76bdb | |||
0466f04d82 | |||
f8ed830b60 | |||
b7ca91bf6d | |||
2261f92b0b | |||
bbe9e621b1 | |||
bf087d760b | |||
a4353c63fd | |||
c438d11c3d | |||
43284e33c8 | |||
77dce7f0dd | |||
d559862051 | |||
7382ed87ba | |||
3324ed31de | |||
ba9dbc2205 | |||
b0028d4874 | |||
9e6be9651c | |||
b145ab0106 | |||
84e0fbb70f | |||
990605bbea | |||
d075c4ad13 | |||
a3f41f5519 | |||
cb173e2850 | |||
87cd3b67f4 | |||
fe3ee3806e | |||
c9ed6c724c | |||
a5fa259d55 | |||
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
8071eb6f8a | |||
11f789e980 | |||
3f82522fe9 | |||
c5cb0e2fd4 | |||
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c | |||
3da6fc3b7e | |||
34dd15ead7 | |||
b3d441e9d6 | |||
4b3dc3756c | |||
10027b98b5 | |||
da17dad63b | |||
fba6c422a8 | |||
0b4b93932d | |||
f42900ec46 | |||
eeca624ba6 | |||
84d08bad16 | |||
1181f33e9d | |||
797e200d08 | |||
d2f231066b |
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
|
||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||
|
2
.github/workflows/cargo-build.yml
vendored
2
.github/workflows/cargo-build.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
15
.github/workflows/cargo-clippy.yml
vendored
15
.github/workflows/cargo-clippy.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -40,7 +40,18 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests -- -D warnings
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
|
37
.github/workflows/cargo-criterion.yml
vendored
Normal file
37
.github/workflows/cargo-criterion.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-criterion.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-criterion.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
name: cargo criterion
|
||||
jobs:
|
||||
cargocriterion:
|
||||
name: cargo criterion
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cargo install cargo-criterion
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Benchmark kcl library
|
||||
shell: bash
|
||||
run: |-
|
||||
cd src/wasm-lib/kcl; cargo criterion
|
||||
|
2
.github/workflows/cargo-fmt.yml
vendored
2
.github/workflows/cargo-fmt.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
14
.github/workflows/cargo-test.yml
vendored
14
.github/workflows/cargo-test.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -41,11 +41,21 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo test --all
|
||||
cargo nextest run --workspace --no-fail-fast -P ci
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
||||
|
157
.github/workflows/ci.yml
vendored
157
.github/workflows/ci.yml
vendored
@ -9,39 +9,53 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
|
||||
check-format:
|
||||
runs-on: 'ubuntu-20.04'
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
|
||||
- run: yarn fmt-check
|
||||
|
||||
check-types:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn build:wasm
|
||||
- run: yarn tsc
|
||||
|
||||
build-test-web:
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- run: yarn build:wasm
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn tsc
|
||||
- run: yarn build:wasm
|
||||
|
||||
- run: yarn simpleserver:ci
|
||||
|
||||
@ -49,21 +63,17 @@ jobs:
|
||||
|
||||
- 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]
|
||||
needs: [check-format, build-test-web, check-types]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-20.04, windows-latest]
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
@ -87,6 +97,10 @@ jobs:
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: wasm prep
|
||||
shell: bash
|
||||
run: |
|
||||
@ -95,31 +109,56 @@ jobs:
|
||||
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)
|
||||
- name: install apple silicon target mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
- name: Prepare Windows certificate and variables
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
|
||||
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
|
||||
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
|
||||
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
|
||||
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
|
||||
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Setup Windows certicate with SSM KSP
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
smksp_registrar.exe list
|
||||
smctl.exe keypair ls
|
||||
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||
smksp_cert_sync.exe
|
||||
shell: cmd
|
||||
|
||||
- name: Build and sign the app for the current platform
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
with:
|
||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: src-tauri/target/release/bundle/*/*
|
||||
|
||||
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
|
||||
|
||||
publish-apps-release:
|
||||
runs-on: ubuntu-20.04
|
||||
@ -127,33 +166,41 @@ jobs:
|
||||
needs: [build-test-web, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
PUB_DATE: ${{ github.event.release.created_at }}
|
||||
NOTES: ${{ github.event.release.body }}
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact
|
||||
ls -l artifact/*
|
||||
ls -l artifact/*/*itty*
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
||||
WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
|
||||
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
||||
--arg linux_sig "$LINUX_SIG" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||
--arg windows_sig "$WINDOWS_SIG" \
|
||||
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": $linux_sig,
|
||||
"url": $linux_url
|
||||
@ -166,6 +213,34 @@ jobs:
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
},
|
||||
"appimage-x86_64": {
|
||||
"url": $linux_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v1.1.1'
|
||||
with:
|
||||
@ -180,7 +255,7 @@ jobs:
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/*'
|
||||
glob: '*/*itty*'
|
||||
parent: false
|
||||
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||
|
||||
@ -190,7 +265,13 @@ jobs:
|
||||
path: last_update.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload release files to Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifact/*/*
|
||||
files: artifact/*/*itty*
|
||||
|
2
.github/workflows/update-dev-branch.yml
vendored
2
.github/workflows/update-dev-branch.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.0
|
||||
- uses: actions/checkout@v4
|
||||
- shell: bash
|
||||
run: |
|
||||
# checkout our branch
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,6 +22,8 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
src/wasm-lib/.idea
|
||||
|
||||
# rust
|
||||
src/wasm-lib/target
|
||||
src/wasm-lib/bindings
|
||||
|
@ -5,3 +5,5 @@ coverage
|
||||
# Ignore Rust projects:
|
||||
*.rs
|
||||
target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
|
121
README.md
121
README.md
@ -1,48 +1,72 @@
|
||||
## Kurt demo project
|
||||

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

|
||||
|
||||
|
||||
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
|
||||
|
||||
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
|
||||
|
||||
## Before submitting a PR
|
||||
|
||||
Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
|
||||
- You have separated out refactoring commits from feature commits as much as possible
|
||||
- You have run all of the following commands locally:
|
||||
- `yarn fmt`
|
||||
- `yarn tsc`
|
||||
- `yarn test`
|
||||
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
|
||||
|
||||
## Release a new version
|
||||
|
||||
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
|
||||
@ -79,6 +128,7 @@ Note that these became separate apps on Macos, so make sure you open the right o
|
||||
```bash
|
||||
VERSION=x.y.z yarn run bump-jsons
|
||||
```
|
||||
|
||||
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||
|
||||
2. Merge the PR
|
||||
@ -86,3 +136,24 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA
|
||||
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
|
||||
|
||||
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
|
||||
|
||||
## Fuzzing the parser
|
||||
|
||||
Make sure you install cargo fuzz:
|
||||
|
||||
```bash
|
||||
$ cargo install cargo-fuzz
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cd src/wasm-lib/kcl
|
||||
|
||||
# list the fuzz targets
|
||||
$ cargo fuzz list
|
||||
|
||||
# run the parser fuzzer
|
||||
$ cargo +nightly fuzz run parser
|
||||
```
|
||||
|
||||
For more information on fuzzing you can check out
|
||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
||||
|
@ -1,4 +1,60 @@
|
||||
[
|
||||
{
|
||||
"name": "abs",
|
||||
"summary": "Computes the absolute value of a number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "acos",
|
||||
"summary": "Computes the arccosine of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "angleToMatchLengthX",
|
||||
"summary": "Returns the angle to match the given length for x.",
|
||||
@ -7473,6 +7529,62 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "asin",
|
||||
"summary": "Computes the arcsine of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "atan",
|
||||
"summary": "Computes the arctangent of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "bezierCurve",
|
||||
"summary": "Draw a bezier curve.",
|
||||
@ -8446,6 +8558,34 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "ceil",
|
||||
"summary": "Computes the smallest integer greater than or equal to a number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "close",
|
||||
"summary": "Close the current sketch.",
|
||||
@ -9322,6 +9462,52 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "cos",
|
||||
"summary": "Computes the sine of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "e",
|
||||
"summary": "Return the value of Euler’s number `e`.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "extrude",
|
||||
"summary": "Extrudes by a given amount.",
|
||||
@ -9916,6 +10102,34 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "floor",
|
||||
"summary": "Computes the largest integer less than or equal to a number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "getExtrudeWallTransform",
|
||||
"summary": "Returns the extrude wall transform.",
|
||||
@ -11173,9 +11387,6 @@
|
||||
},
|
||||
"to": {
|
||||
"description": "The to point.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A point.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
@ -11183,12 +11394,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -11201,10 +11406,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -13001,8 +13202,129 @@
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "min",
|
||||
"summary": "Returns the minimum of the given arguments.",
|
||||
"name": "ln",
|
||||
"summary": "Computes the natural logarithm of the number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "log",
|
||||
"summary": "Computes the logarithm of the number with respect to an arbitrary base.",
|
||||
"description": "The result might not be correctly rounded owing to implementation details; `log2()` can produce more accurate results for base 2, and `log10()` can produce more accurate results for base 10.",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "base",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "log10",
|
||||
"summary": "Computes the base 10 logarithm of the number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "log2",
|
||||
"summary": "Computes the base 2 logarithm of the number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "max",
|
||||
"summary": "Computes the maximum of the given arguments.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
@ -13031,6 +13353,92 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "min",
|
||||
"summary": "Computes the minimum of the given arguments.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "args",
|
||||
"type": "[number]",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "pi",
|
||||
"summary": "Return the value of `pi`. Archimedes’ constant (π).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "pow",
|
||||
"summary": "Computes the number to a power.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "pow",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "segAng",
|
||||
"summary": "Returns the angle of the segment.",
|
||||
@ -15315,6 +15723,62 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "sin",
|
||||
"summary": "Computes the sine of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "sqrt",
|
||||
"summary": "Computes the square root of a number.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "startSketchAt",
|
||||
"summary": "Start a sketch at a given point.",
|
||||
@ -15341,9 +15805,6 @@
|
||||
},
|
||||
"to": {
|
||||
"description": "The to point.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A point.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
@ -15351,12 +15812,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -15369,10 +15824,6 @@
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"description": "A string like `default`.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -15815,6 +16266,52 @@
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "tan",
|
||||
"summary": "Computes the tangent of a number (in radians).",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "num",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "tau",
|
||||
"summary": "Return the value of `tau`. The full circle constant (τ). Equal to 2π.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "xLine",
|
||||
"summary": "Draw a line on the x-axis.",
|
@ -5,6 +5,8 @@
|
||||
## Table of Contents
|
||||
|
||||
* [Functions](#functions)
|
||||
* [`abs`](#abs)
|
||||
* [`acos`](#acos)
|
||||
* [`angleToMatchLengthX`](#angleToMatchLengthX)
|
||||
* [`angleToMatchLengthY`](#angleToMatchLengthY)
|
||||
* [`angledLine`](#angledLine)
|
||||
@ -14,9 +16,15 @@
|
||||
* [`angledLineToX`](#angledLineToX)
|
||||
* [`angledLineToY`](#angledLineToY)
|
||||
* [`arc`](#arc)
|
||||
* [`asin`](#asin)
|
||||
* [`atan`](#atan)
|
||||
* [`bezierCurve`](#bezierCurve)
|
||||
* [`ceil`](#ceil)
|
||||
* [`close`](#close)
|
||||
* [`cos`](#cos)
|
||||
* [`e`](#e)
|
||||
* [`extrude`](#extrude)
|
||||
* [`floor`](#floor)
|
||||
* [`getExtrudeWallTransform`](#getExtrudeWallTransform)
|
||||
* [`lastSegX`](#lastSegX)
|
||||
* [`lastSegY`](#lastSegY)
|
||||
@ -25,13 +33,24 @@
|
||||
* [`legLen`](#legLen)
|
||||
* [`line`](#line)
|
||||
* [`lineTo`](#lineTo)
|
||||
* [`ln`](#ln)
|
||||
* [`log`](#log)
|
||||
* [`log10`](#log10)
|
||||
* [`log2`](#log2)
|
||||
* [`max`](#max)
|
||||
* [`min`](#min)
|
||||
* [`pi`](#pi)
|
||||
* [`pow`](#pow)
|
||||
* [`segAng`](#segAng)
|
||||
* [`segEndX`](#segEndX)
|
||||
* [`segEndY`](#segEndY)
|
||||
* [`segLen`](#segLen)
|
||||
* [`show`](#show)
|
||||
* [`sin`](#sin)
|
||||
* [`sqrt`](#sqrt)
|
||||
* [`startSketchAt`](#startSketchAt)
|
||||
* [`tan`](#tan)
|
||||
* [`tau`](#tau)
|
||||
* [`xLine`](#xLine)
|
||||
* [`xLineTo`](#xLineTo)
|
||||
* [`yLine`](#yLine)
|
||||
@ -40,6 +59,46 @@
|
||||
|
||||
## Functions
|
||||
|
||||
### abs
|
||||
|
||||
Computes the absolute value of a number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
abs(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### acos
|
||||
|
||||
Computes the arccosine of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
acos(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### angleToMatchLengthX
|
||||
|
||||
Returns the angle to match the given length for x.
|
||||
@ -1328,6 +1387,46 @@ arc(data: ArcData, sketch_group: SketchGroup) -> SketchGroup
|
||||
|
||||
|
||||
|
||||
### asin
|
||||
|
||||
Computes the arcsine of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
asin(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### atan
|
||||
|
||||
Computes the arctangent of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
atan(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### bezierCurve
|
||||
|
||||
Draw a bezier curve.
|
||||
@ -1493,6 +1592,26 @@ bezierCurve(data: BezierData, sketch_group: SketchGroup) -> SketchGroup
|
||||
|
||||
|
||||
|
||||
### ceil
|
||||
|
||||
Computes the smallest integer greater than or equal to a number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
ceil(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### close
|
||||
|
||||
Close the current sketch.
|
||||
@ -1637,6 +1756,45 @@ close(sketch_group: SketchGroup) -> SketchGroup
|
||||
|
||||
|
||||
|
||||
### cos
|
||||
|
||||
Computes the sine of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
cos(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### e
|
||||
|
||||
Return the value of Euler’s number `e`.
|
||||
|
||||
|
||||
|
||||
```
|
||||
e() -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### extrude
|
||||
|
||||
Extrudes by a given amount.
|
||||
@ -1746,6 +1904,26 @@ extrude(length: number, sketch_group: SketchGroup) -> ExtrudeGroup
|
||||
|
||||
|
||||
|
||||
### floor
|
||||
|
||||
Computes the largest integer less than or equal to a number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
floor(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### getExtrudeWallTransform
|
||||
|
||||
Returns the extrude wall transform.
|
||||
@ -2044,11 +2222,9 @@ line(data: LineData, sketch_group: SketchGroup) -> SketchGroup
|
||||
// The tag.
|
||||
tag: string,
|
||||
// The to point.
|
||||
to: [number] |
|
||||
string,
|
||||
to: [number],
|
||||
} |
|
||||
[number] |
|
||||
string
|
||||
[number]
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
|
||||
```
|
||||
@ -2336,9 +2512,110 @@ lineTo(data: LineToData, sketch_group: SketchGroup) -> SketchGroup
|
||||
|
||||
|
||||
|
||||
### ln
|
||||
|
||||
Computes the natural logarithm of the number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
ln(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### log
|
||||
|
||||
Computes the logarithm of the number with respect to an arbitrary base.
|
||||
|
||||
The result might not be correctly rounded owing to implementation details; `log2()` can produce more accurate results for base 2, and `log10()` can produce more accurate results for base 10.
|
||||
|
||||
```
|
||||
log(num: number, base: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
* `base`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### log10
|
||||
|
||||
Computes the base 10 logarithm of the number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
log10(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### log2
|
||||
|
||||
Computes the base 2 logarithm of the number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
log2(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### max
|
||||
|
||||
Computes the maximum of the given arguments.
|
||||
|
||||
|
||||
|
||||
```
|
||||
max(args: [number]) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `args`: `[number]`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### min
|
||||
|
||||
Returns the minimum of the given arguments.
|
||||
Computes the minimum of the given arguments.
|
||||
|
||||
|
||||
|
||||
@ -2356,6 +2633,46 @@ min(args: [number]) -> number
|
||||
|
||||
|
||||
|
||||
### pi
|
||||
|
||||
Return the value of `pi`. Archimedes’ constant (π).
|
||||
|
||||
|
||||
|
||||
```
|
||||
pi() -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### pow
|
||||
|
||||
Computes the number to a power.
|
||||
|
||||
|
||||
|
||||
```
|
||||
pow(num: number, pow: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
* `pow`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### segAng
|
||||
|
||||
Returns the angle of the segment.
|
||||
@ -2766,6 +3083,46 @@ show(sketch: SketchGroup)
|
||||
|
||||
|
||||
|
||||
### sin
|
||||
|
||||
Computes the sine of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
sin(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### sqrt
|
||||
|
||||
Computes the square root of a number.
|
||||
|
||||
|
||||
|
||||
```
|
||||
sqrt(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### startSketchAt
|
||||
|
||||
Start a sketch at a given point.
|
||||
@ -2784,11 +3141,9 @@ startSketchAt(data: LineData) -> SketchGroup
|
||||
// The tag.
|
||||
tag: string,
|
||||
// The to point.
|
||||
to: [number] |
|
||||
string,
|
||||
to: [number],
|
||||
} |
|
||||
[number] |
|
||||
string
|
||||
[number]
|
||||
```
|
||||
|
||||
#### Returns
|
||||
@ -2859,6 +3214,45 @@ string
|
||||
|
||||
|
||||
|
||||
### tan
|
||||
|
||||
Computes the tangent of a number (in radians).
|
||||
|
||||
|
||||
|
||||
```
|
||||
tan(num: number) -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
* `num`: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### tau
|
||||
|
||||
Return the value of `tau`. The full circle constant (τ). Equal to 2π.
|
||||
|
||||
|
||||
|
||||
```
|
||||
tau() -> number
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
|
||||
#### Returns
|
||||
|
||||
* `number`
|
||||
|
||||
|
||||
|
||||
### xLine
|
||||
|
||||
Draw a line on the x-axis.
|
75
docs/kcl/types.md
Normal file
75
docs/kcl/types.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Types
|
||||
|
||||
`KCL` defines the following types and keywords the language.
|
||||
|
||||
All these types can be nested in various forms where nesting applies. Like
|
||||
arrays can hold objects and vice versa.
|
||||
|
||||
## Boolean
|
||||
|
||||
`true` or `false` work when defining values.
|
||||
|
||||
## Variable declaration
|
||||
|
||||
Variables are defined with the `let` keyword like so:
|
||||
|
||||
```
|
||||
let myBool = false
|
||||
```
|
||||
|
||||
## Array
|
||||
|
||||
An array is defined with `[]` braces. What is inside the brackets can
|
||||
be of any type. For example, the following is completely valid:
|
||||
|
||||
```
|
||||
let myArray = ["thing", 2, false]
|
||||
```
|
||||
|
||||
If you want to get a value from an array you can use the index like so:
|
||||
`myArray[0]`.
|
||||
|
||||
|
||||
## Object
|
||||
|
||||
An object is defined with `{}` braces. Here is an example object:
|
||||
|
||||
```
|
||||
let myObj = {a: 0, b: "thing"}
|
||||
```
|
||||
|
||||
We support two different ways of getting properties from objects, you can call
|
||||
`myObj.a` or `myObj["a"]` both work.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
We also have support for defining your own functions. Functions can take in any
|
||||
type of argument. Below is an example of the syntax:
|
||||
|
||||
```
|
||||
fn myFn = (x) => {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
|
||||
## Binary expressions
|
||||
|
||||
You can also do math! Let's show an example below:
|
||||
|
||||
```
|
||||
let myMathExpression = 3 + 1 * 2 / 3 - 7
|
||||
```
|
||||
|
||||
You can nest expressions in parenthesis as well:
|
||||
|
||||
```
|
||||
let myMathExpression = 3 + (1 * 2 / (3 - 7))
|
||||
```
|
||||
|
||||
Please if you find any issues using any of the above expressions or syntax
|
||||
please file an issue with the `ast` label on the [modeling-app
|
||||
repo](https://github.com/KittyCAD/modeling-app/issues/new).
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.3.1",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
@ -10,10 +10,11 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.35",
|
||||
"@kittycad/lib": "^0.0.38",
|
||||
"@lezer/javascript": "^1.4.7",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@sentry/react": "^7.65.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
@ -61,16 +62,18 @@
|
||||
"build:local": "vite build",
|
||||
"build:both": "vite build",
|
||||
"build:both:local": "yarn build:wasm && vite build",
|
||||
"pretest": "yarn remove-importmeta",
|
||||
"test": "vitest --mode development",
|
||||
"test:nowatch": "vitest run --mode development",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
||||
"test:cov": "vitest run --coverage --mode development",
|
||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "http-server ./public --cors -p 3000",
|
||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"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\"",
|
||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"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"
|
||||
|
42
public/expectations.md
Normal file
42
public/expectations.md
Normal file
@ -0,0 +1,42 @@
|
||||
## Alpha Users Expectations
|
||||
|
||||
### Welcome
|
||||
|
||||
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
|
||||
|
||||
### KittyCAD Modeling App (KCMA)
|
||||
|
||||
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
|
||||
|
||||
### Why Code?
|
||||
|
||||
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isn’t as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
|
||||
|
||||
- Automation and parametric design
|
||||
- Customization and flexibility
|
||||
- Algorithmic and generative design
|
||||
- Reproducibility
|
||||
- Easier integration with other tools
|
||||
|
||||
### Before You Use KCMA
|
||||
|
||||
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
||||
|
||||
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
|
||||
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
|
||||
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure it’s not a duplicate or it’s feasible for the current state of the app.
|
||||
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
|
||||
- **High:** The bug is blocking you from continuing.
|
||||
- Example: Every time I click the extrude button with two faces selected, the app crashes.
|
||||
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
|
||||
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
|
||||
- **Low:** The bug is annoying but doesn’t affect workflow or block you from continuing (usually you can say “It would be nice if ___, but it’s not needed”)
|
||||
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
|
||||
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
|
||||
|
||||
### Discord
|
||||
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
|
||||
|
||||
### Thank You!
|
||||
|
||||
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!
|
46
public/kcma-logomark-dark.svg
Normal file
46
public/kcma-logomark-dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
BIN
public/kcma-logomark.png
Normal file
BIN
public/kcma-logomark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
46
public/kcma-logomark.svg
Normal file
46
public/kcma-logomark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
BIN
public/onboarding-bracket-dark.png
Normal file
BIN
public/onboarding-bracket-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
BIN
public/onboarding-bracket.png
Normal file
BIN
public/onboarding-bracket.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
26
public/roadmap.md
Normal file
26
public/roadmap.md
Normal file
@ -0,0 +1,26 @@
|
||||
## KittyCAD Modeling App Roadmap
|
||||
|
||||
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.
|
||||
|
||||
### Current Priority List
|
||||
|
||||
1. [Sketch on Face](https://github.com/KittyCAD/modeling-app/discussions/477)
|
||||
2. [Revolve](https://github.com/KittyCAD/modeling-app/discussions/496)
|
||||
3. [Fillet](https://github.com/KittyCAD/modeling-app/discussions/501)
|
||||
4. [Linear Pattern](https://github.com/KittyCAD/modeling-app/discussions/256)
|
||||
5. [Circular Pattern](https://github.com/KittyCAD/modeling-app/discussions/257)
|
||||
6. [Mirror-Sketch](https://github.com/KittyCAD/modeling-app/discussions/507)
|
||||
7. [Chamfer](https://github.com/KittyCAD/modeling-app/discussions/502)
|
||||
8. [Sweep](https://github.com/KittyCAD/modeling-app/discussions/498)
|
||||
9. [Draft](https://github.com/KittyCAD/modeling-app/discussions/495)
|
||||
10. [Shell](https://github.com/KittyCAD/modeling-app/discussions/503)
|
||||
11. [Union](https://github.com/KittyCAD/modeling-app/discussions/509)
|
||||
12. [Mirror-Model](https://github.com/KittyCAD/modeling-app/discussions/508)
|
||||
13. [Subtract](https://github.com/KittyCAD/modeling-app/discussions/510)
|
||||
14. [Intersect](https://github.com/KittyCAD/modeling-app/discussions/511)
|
||||
15. [Offset](https://github.com/KittyCAD/modeling-app/discussions/512)
|
||||
16. [Thicken](https://github.com/KittyCAD/modeling-app/discussions/499)
|
||||
17. [Import](https://github.com/KittyCAD/modeling-app/discussions/478)
|
||||
18. [Assemblies](https://github.com/KittyCAD/modeling-app/discussions/494)
|
||||
19. [External Thread](https://github.com/KittyCAD/modeling-app/discussions/505)
|
||||
|
971
src-tauri/Cargo.lock
generated
971
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -16,13 +16,14 @@ tauri-build = { version = "1.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
oauth2 = "4.4.1"
|
||||
kittycad = "0.2.25"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
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"] }
|
||||
toml = "0.6.0"
|
||||
tauri = { version = "1.4.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tokio = { version = "1.32.0", features = ["time"] }
|
||||
toml = "0.8.0"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
@ -6,6 +6,7 @@ use std::io::Read;
|
||||
use anyhow::Result;
|
||||
use oauth2::TokenResponse;
|
||||
use tauri::{InvokeError, Manager};
|
||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||
|
||||
/// This command returns the a json string parse from a toml file at the path.
|
||||
#[tauri::command]
|
||||
@ -85,6 +86,47 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
///This command returns the KittyCAD user info given a token.
|
||||
/// The string returned from this method is the user info as a json string.
|
||||
#[tauri::command]
|
||||
async fn get_user(
|
||||
token: Option<String>,
|
||||
hostname: &str,
|
||||
) -> Result<kittycad::types::User, InvokeError> {
|
||||
// Use the host passed in if it's set.
|
||||
// Otherwise, use the default host.
|
||||
let host = if hostname.is_empty() {
|
||||
DEFAULT_HOST.to_string()
|
||||
} else {
|
||||
hostname.to_string()
|
||||
};
|
||||
|
||||
// Change the baseURL to the one we want.
|
||||
let mut baseurl = host.to_string();
|
||||
if !host.starts_with("http://") && !host.starts_with("https://") {
|
||||
baseurl = format!("https://{host}");
|
||||
if host.starts_with("localhost") {
|
||||
baseurl = format!("http://{host}")
|
||||
}
|
||||
}
|
||||
println!("Getting user info...");
|
||||
|
||||
// use kittycad library to fetch the user info from /user/me
|
||||
let mut client = kittycad::Client::new(token.unwrap());
|
||||
|
||||
if baseurl != DEFAULT_HOST {
|
||||
client.set_base_url(&baseurl);
|
||||
}
|
||||
|
||||
let user_info: kittycad::types::User = client
|
||||
.users()
|
||||
.get_self()
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
|
||||
Ok(user_info)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
@ -97,7 +139,12 @@ fn main() {
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_user,
|
||||
login,
|
||||
read_toml,
|
||||
read_txt_file
|
||||
])
|
||||
.plugin(tauri_plugin_fs_extra::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.3.1"
|
||||
"version": "0.9.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@ -36,6 +36,9 @@
|
||||
"https://api.dev.kittycad.io/*"
|
||||
]
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
@ -71,9 +74,9 @@
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
"timestampUrl": "http://timestamp.digicert.com"
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
|
515
src/App.tsx
515
src/App.tsx
@ -1,39 +1,16 @@
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { asyncParser } from './lang/abstractSyntaxTree'
|
||||
import { _executor } from './lang/executor'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { ViewUpdate, EditorView } from '@codemirror/view'
|
||||
import {
|
||||
lineHighlightField,
|
||||
addLineHighlight,
|
||||
} from './editor/highlightextension'
|
||||
import { PaneType, Selections, useStore } from './useStore'
|
||||
import Server from './editor/lsp/server'
|
||||
import Client from './editor/lsp/client'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
import { Logs, KCLErrors } from './components/Logs'
|
||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||
import { MemoryPanel } from './components/MemoryPanel'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
import { FromServer, IntoServer } from './editor/lsp/codec'
|
||||
import {
|
||||
EngineCommand,
|
||||
EngineCommandManager,
|
||||
} from './lang/std/engineConnection'
|
||||
import { isOverlap, throttle } from './lib/utils'
|
||||
import { EngineCommand } from './lang/std/engineConnection'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
faCode,
|
||||
@ -41,96 +18,47 @@ import {
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TEST } from './env'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
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 { useLoaderData } from 'react-router-dom'
|
||||
import { IndexLoaderData } from './Router'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
|
||||
export function App() {
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
const pathParams = useParams()
|
||||
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useHotKeyListener()
|
||||
const {
|
||||
editorView,
|
||||
setEditorView,
|
||||
setSelectionRanges,
|
||||
selectionRanges,
|
||||
addLog,
|
||||
addKCLError,
|
||||
code,
|
||||
setCode,
|
||||
setAst,
|
||||
setError,
|
||||
setProgramMemory,
|
||||
resetLogs,
|
||||
resetKCLErrors,
|
||||
selectionRangeTypeMap,
|
||||
setArtifactMap,
|
||||
engineCommandManager,
|
||||
setEngineCommandManager,
|
||||
setHighlightRange,
|
||||
setCursor2,
|
||||
sourceRangeMap,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
isStreamReady,
|
||||
isLSPServerReady,
|
||||
setIsLSPServerReady,
|
||||
isMouseDownInStream,
|
||||
formatCode,
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
setStreamDimensions,
|
||||
streamDimensions,
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
executeAst,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
setEditorView: s.setEditorView,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
selectionRanges: s.selectionRanges,
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
addLog: s.addLog,
|
||||
code: s.code,
|
||||
setCode: s.setCode,
|
||||
setAst: s.setAst,
|
||||
setError: s.setError,
|
||||
setProgramMemory: s.setProgramMemory,
|
||||
resetLogs: s.resetLogs,
|
||||
resetKCLErrors: s.resetKCLErrors,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
isShiftDown: s.isShiftDown,
|
||||
setCursor: s.setCursor,
|
||||
setCursor2: s.setCursor2,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
setMediaStream: s.setMediaStream,
|
||||
isStreamReady: s.isStreamReady,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
formatCode: s.formatCode,
|
||||
addKCLError: s.addKCLError,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
streamDimensions: s.streamDimensions,
|
||||
executeAst: s.executeAst,
|
||||
}))
|
||||
|
||||
const {
|
||||
@ -138,7 +66,7 @@ export function App() {
|
||||
context: { token },
|
||||
},
|
||||
settings: {
|
||||
context: { showDebugPanel, theme, onboardingStatus },
|
||||
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
@ -157,9 +85,54 @@ export function App() {
|
||||
useHotkeys('shift + l', () => togglePane('logs'))
|
||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
useHotkeys('esc', () => {
|
||||
if (guiMode.mode === 'sketch') {
|
||||
if (guiMode.sketchMode === 'selectFace') return
|
||||
if (guiMode.sketchMode === 'sketchEdit') {
|
||||
// TODO: share this with Toolbar's "Exit sketch" button
|
||||
// exiting sketch should be done consistently across all exits
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
// this is necessary to get the UI back into a consistent
|
||||
// state right now, hopefully won't need to rerender
|
||||
// when exiting sketch mode in the future
|
||||
executeAst()
|
||||
} else {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool: 'select',
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
pathToNode: guiMode.pathToNode,
|
||||
pathId: guiMode.pathId,
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setGuiMode({ mode: 'default' })
|
||||
}
|
||||
})
|
||||
|
||||
const paneOpacity =
|
||||
onboardingStatus === onboardingPaths.CAMERA
|
||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||
(p) => p === onboardingStatus
|
||||
)
|
||||
? 'opacity-20'
|
||||
: didDragInStream
|
||||
? 'opacity-40'
|
||||
@ -179,236 +152,35 @@ export function App() {
|
||||
}
|
||||
}, [loadedCode, setCode])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
setCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
const streamHeight = streamRef?.current?.offsetHeight
|
||||
|
||||
const width = streamWidth ? streamWidth : 0
|
||||
const quadWidth = Math.round(width / 4) * 4
|
||||
const height = streamHeight ? streamHeight : 0
|
||||
const quadHeight = Math.round(height / 4) * 4
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
streamHeight: quadHeight,
|
||||
})
|
||||
if (!width || !height) return
|
||||
const eng = new EngineCommandManager({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
token,
|
||||
})
|
||||
setEngineCommandManager(eng)
|
||||
return () => {
|
||||
eng?.tearDown()
|
||||
}
|
||||
}, [quadWidth, quadHeight])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStreamReady) return
|
||||
if (!engineCommandManager) return
|
||||
let unsubFn: any[] = []
|
||||
const asyncWrap = async () => {
|
||||
try {
|
||||
if (!code) {
|
||||
setAst(null)
|
||||
return
|
||||
}
|
||||
const _ast = await asyncParser(code)
|
||||
setAst(_ast)
|
||||
resetLogs()
|
||||
resetKCLErrors()
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
const programMemory = await _executor(
|
||||
_ast,
|
||||
{
|
||||
root: {
|
||||
_0: {
|
||||
type: 'userVal',
|
||||
value: 0,
|
||||
__meta: [],
|
||||
},
|
||||
_90: {
|
||||
type: 'userVal',
|
||||
value: 90,
|
||||
__meta: [],
|
||||
},
|
||||
_180: {
|
||||
type: 'userVal',
|
||||
value: 180,
|
||||
__meta: [],
|
||||
},
|
||||
_270: {
|
||||
type: 'userVal',
|
||||
value: 270,
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
engineCommandManager
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
|
||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
setHighlightRange([0, 0])
|
||||
} else {
|
||||
const sourceRange = sourceRangeMap[data.entity_id]
|
||||
setHighlightRange(sourceRange)
|
||||
}
|
||||
},
|
||||
})
|
||||
const unSubClick = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
setCursor2()
|
||||
return
|
||||
}
|
||||
const sourceRange = sourceRangeMap[data.entity_id]
|
||||
setCursor2({ range: sourceRange, type: 'default' })
|
||||
},
|
||||
})
|
||||
unsubFn.push(unSubHover, unSubClick)
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setError()
|
||||
} catch (e: any) {
|
||||
if (e instanceof KCLError) {
|
||||
addKCLError(e)
|
||||
} else {
|
||||
setError('problem')
|
||||
console.log(e)
|
||||
addLog(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
asyncWrap()
|
||||
return () => {
|
||||
unsubFn.forEach((fn) => fn())
|
||||
}
|
||||
}, [code, isStreamReady, engineCommandManager])
|
||||
useSetupEngineManager(streamRef, token)
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager?.sendSceneCommand(message)
|
||||
}, 16)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
shiftKey,
|
||||
currentTarget,
|
||||
nativeEvent,
|
||||
}) => {
|
||||
nativeEvent.preventDefault()
|
||||
if (isMouseDownInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.nativeEvent.preventDefault()
|
||||
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
el: currentTarget,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: e.currentTarget,
|
||||
...streamDimensions,
|
||||
})
|
||||
|
||||
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
|
||||
if (isMouseDownInStream) {
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction,
|
||||
type: 'mouse_move',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
@ -420,57 +192,45 @@ export function App() {
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
|
||||
const eWithButton = { ...e, button: buttonDownInStream }
|
||||
|
||||
if (interactionGuards.pan.callback(eWithButton)) {
|
||||
interaction = 'pan'
|
||||
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||
interaction = 'rotate'
|
||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
console.log('none')
|
||||
return
|
||||
}
|
||||
|
||||
const extraExtensions = useMemo(() => {
|
||||
if (TEST) return []
|
||||
return [
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]
|
||||
}, [])
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
// Below is the client and server promise.
|
||||
const { lspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
lspServer.start()
|
||||
setIsLSPServerReady(true)
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsLSPServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup becuase it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isLSPServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: lspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [lspClient, isLSPServerReady])
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -482,7 +242,7 @@ export function App() {
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
project={project}
|
||||
enableMenu={true}
|
||||
@ -491,17 +251,17 @@ export function App() {
|
||||
<Resizable
|
||||
className={
|
||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||
(isMouseDownInStream || onboardingStatus === 'camera'
|
||||
(buttonDownInStream || onboardingStatus === 'camera'
|
||||
? ' pointer-events-none '
|
||||
: ' ') +
|
||||
paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '400px',
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={600}
|
||||
maxWidth={800}
|
||||
minHeight={'auto'}
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
@ -509,40 +269,15 @@ export function App() {
|
||||
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
||||
}}
|
||||
>
|
||||
<div className="h-full flex flex-col justify-between">
|
||||
<div id="code-pane" className="h-full flex flex-col justify-between">
|
||||
<CollapsiblePanel
|
||||
title="Code"
|
||||
icon={faCode}
|
||||
className="open:!mb-2 overflow-x-hidden"
|
||||
className="open:!mb-2"
|
||||
open={openPanes.includes('code')}
|
||||
menu={<CodeMenu />}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
<button
|
||||
// disabled={!shouldFormat}
|
||||
onClick={formatCode}
|
||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
||||
>
|
||||
format
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="overflow-x-hidden h-full"
|
||||
>
|
||||
<CodeMirror
|
||||
className="h-full overflow-hidden-x"
|
||||
value={code}
|
||||
extensions={
|
||||
kclLSP
|
||||
? [kclLSP, lineHighlightField, ...extraExtensions]
|
||||
: [lineHighlightField, ...extraExtensions]
|
||||
}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={editorTheme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
<TextEditor theme={editorTheme} />
|
||||
</CollapsiblePanel>
|
||||
<section className="flex flex-col">
|
||||
<MemoryPanel
|
||||
@ -573,7 +308,7 @@ export function App() {
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
|
@ -6,9 +6,9 @@ export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const {
|
||||
auth: { state },
|
||||
} = useGlobalStateContext()
|
||||
const isLoggedIn = state.matches('checkIfLoggedIn')
|
||||
const isLoggingIn = state.matches('checkIfLoggedIn')
|
||||
|
||||
return isLoggedIn ? (
|
||||
return isLoggingIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
) : (
|
||||
<>{children}</>
|
||||
|
@ -130,6 +130,7 @@ const router = createBrowserRouter(
|
||||
path: paths.INDEX,
|
||||
loader: () =>
|
||||
isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'),
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: paths.FILE + '/:id',
|
||||
@ -140,7 +141,6 @@ const router = createBrowserRouter(
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
id: paths.FILE,
|
||||
loader: async ({
|
||||
request,
|
||||
|
@ -47,6 +47,52 @@
|
||||
@apply hover:bg-cool-20;
|
||||
}
|
||||
|
||||
.toolbarButtons::-webkit-scrollbar {
|
||||
@apply h-0.5;
|
||||
}
|
||||
|
||||
.toolbarButtons {
|
||||
@apply flex items-center overflow-x-auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.toolbarButtons button {
|
||||
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@apply gap-1.5 p-0.5 pr-1;
|
||||
@apply rounded-sm;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
|
||||
}
|
||||
.toolbarButtons button:hover {
|
||||
@apply text-cool-90 bg-cool-10;
|
||||
}
|
||||
:global(.sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-90 bg-fern-10;
|
||||
}
|
||||
.toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-70 bg-chalkboard-30;
|
||||
}
|
||||
.toolbarButtons button:disabled:hover {
|
||||
@apply !bg-inherit !text-inherit cursor-not-allowed;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarButtons button {
|
||||
@apply text-chalkboard-20 border-chalkboard-50;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:hover {
|
||||
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
|
||||
}
|
||||
:global(.dark .sketch) .toolbarButtons button:hover {
|
||||
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
|
||||
}
|
||||
:global(.dark) .toolbarButtons button:disabled {
|
||||
@apply text-chalkboard-40 bg-chalkboard-80;
|
||||
}
|
||||
|
||||
:global(.dark) .popoverToggle {
|
||||
@apply hover:bg-cool-90;
|
||||
}
|
||||
|
141
src/Toolbar.tsx
141
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
||||
import { useStore, toolTips } from './useStore'
|
||||
import { useStore, toolTips, ToolTip } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
@ -16,6 +15,32 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAppMode } from 'hooks/useAppMode'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
|
||||
export const sketchButtonClassnames = {
|
||||
background:
|
||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
|
||||
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
|
||||
}
|
||||
|
||||
const sketchFnLabels: Record<ToolTip | 'sketch_line' | 'move', string> = {
|
||||
sketch_line: 'Line',
|
||||
line: 'Line',
|
||||
move: 'Move',
|
||||
angledLine: 'Angled Line',
|
||||
angledLineThatIntersects: 'Angled Line That Intersects',
|
||||
angledLineOfXLength: 'Angled Line Of X Length',
|
||||
angledLineOfYLength: 'Angled Line Of Y Length',
|
||||
angledLineToX: 'Angled Line To X',
|
||||
angledLineToY: 'Angled Line To Y',
|
||||
lineTo: 'Line to Point',
|
||||
xLine: 'Horizontal Line',
|
||||
yLine: 'Vertical Line',
|
||||
xLineTo: 'Horizontal Line to Point',
|
||||
yLineTo: 'Vertical Line to Point',
|
||||
}
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -25,6 +50,8 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
executeAst,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
@ -32,15 +59,18 @@ export const Toolbar = () => {
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
executeAst: s.executeAst,
|
||||
}))
|
||||
useAppMode()
|
||||
|
||||
useEffect(() => {
|
||||
console.log('guiMode', guiMode)
|
||||
}, [guiMode])
|
||||
|
||||
function ToolbarButtons() {
|
||||
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
|
||||
return (
|
||||
<>
|
||||
<span className={styles.toolbarButtons + ' ' + className}>
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -49,7 +79,9 @@ export const Toolbar = () => {
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Start Sketch
|
||||
</button>
|
||||
)}
|
||||
@ -66,24 +98,33 @@ export const Toolbar = () => {
|
||||
pathToNode,
|
||||
programMemory
|
||||
)
|
||||
updateAst(modifiedAst)
|
||||
updateAst(modifiedAst, true)
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
SketchOnFace
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Sketch on Face
|
||||
</button>
|
||||
)}
|
||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
sketchMode: 'enterSketchEdit',
|
||||
pathToNode: pathToNode,
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathId: guiMode.pathId,
|
||||
})
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
@ -100,10 +141,12 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
ExtrudeSketch
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -117,23 +160,49 @@ export const Toolbar = () => {
|
||||
pathToNode,
|
||||
false
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
ExtrudeSketch (w/o pipe)
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude as new
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
|
||||
setGuiMode({ mode: 'default' })
|
||||
executeAst()
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon
|
||||
icon="exit"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
@ -144,7 +213,18 @@ export const Toolbar = () => {
|
||||
return (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
@ -154,17 +234,30 @@ export const Toolbar = () => {
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
pathId: guiMode.pathId,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
className={
|
||||
'group ' +
|
||||
(guiMode.sketchMode === sketchFnName
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
<ActionIcon
|
||||
icon={sketchFnName.includes('line') ? 'line' : 'move'}
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{sketchFnLabels[sketchFnName]}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<ConvertToVariable />
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
@ -182,7 +275,7 @@ export const Toolbar = () => {
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@ -192,7 +285,7 @@ export const 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">
|
||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<Popover.Button
|
||||
@ -233,7 +326,7 @@ export const Toolbar = () => {
|
||||
</Popover.Button>
|
||||
</section>
|
||||
<section>
|
||||
<ToolbarButtons />
|
||||
<ToolbarButtons className="flex-wrap" />
|
||||
</section>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { CustomIcon, CustomIconName } from './CustomIcon'
|
||||
|
||||
const iconSizes = {
|
||||
sm: 12,
|
||||
@ -13,7 +14,7 @@ const iconSizes = {
|
||||
}
|
||||
|
||||
export interface ActionIconProps extends React.PropsWithChildren {
|
||||
icon?: SolidIconDefinition | BrandIconDefinition
|
||||
icon?: SolidIconDefinition | BrandIconDefinition | CustomIconName
|
||||
className?: string
|
||||
bgClassName?: string
|
||||
iconClassName?: string
|
||||
@ -28,25 +29,39 @@ export const ActionIcon = ({
|
||||
size = 'md',
|
||||
children,
|
||||
}: ActionIconProps) => {
|
||||
// By default, we reverse the icon color and background color in dark mode
|
||||
const computedIconClassName =
|
||||
iconClassName ||
|
||||
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
|
||||
|
||||
const computedBgClassName =
|
||||
bgClassName ||
|
||||
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`p-${
|
||||
size === 'xl' ? '2' : '1'
|
||||
} w-fit inline-grid place-content-center ${className} ` +
|
||||
(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')
|
||||
computedBgClassName
|
||||
}
|
||||
>
|
||||
{children || (
|
||||
{children ? (
|
||||
children
|
||||
) : typeof icon === 'string' ? (
|
||||
<CustomIcon
|
||||
name={icon}
|
||||
width={iconSizes[size]}
|
||||
height={iconSizes[size]}
|
||||
className={computedIconClassName}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
width={iconSizes[size]}
|
||||
height={iconSizes[size]}
|
||||
className={
|
||||
iconClassName ||
|
||||
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||
}
|
||||
className={computedIconClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -28,7 +29,7 @@ export const AppHeader = ({
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
(showToolbar ? 'grid ' : 'flex justify-between ') +
|
||||
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
|
||||
styles.header +
|
||||
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||
className
|
||||
@ -37,13 +38,14 @@ export const AppHeader = ({
|
||||
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
|
||||
{/* Toolbar if the context deems it */}
|
||||
{showToolbar && (
|
||||
<div className="max-w-4xl">
|
||||
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||
<Toolbar />
|
||||
</div>
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
|
182
src/components/AstExplorer.tsx
Normal file
182
src/components/AstExplorer.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
||||
ast: s.ast,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
selectionRanges: s.selectionRanges,
|
||||
}))
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range
|
||||
)
|
||||
const node = getNodeFromPath(ast, pathToNode).node
|
||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '300px' }}>
|
||||
<div className="">
|
||||
filter out keys:<div className="w-2 inline-block"></div>
|
||||
{['start', 'end', 'type'].map((key) => {
|
||||
return (
|
||||
<label key={key} className="inline-flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox"
|
||||
checked={filterKeys.includes(key)}
|
||||
onChange={(e) => {
|
||||
if (filterKeys.includes(key)) {
|
||||
setFilterKeys(filterKeys.filter((k) => k !== key))
|
||||
} else {
|
||||
setFilterKeys([...filterKeys, key])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mr-2">{key}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="h-full relative"
|
||||
onMouseLeave={(e) => {
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
>
|
||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DisplayBody({
|
||||
body,
|
||||
filterKeys,
|
||||
node,
|
||||
}: {
|
||||
body: { start: number; end: number; [key: string]: any }[]
|
||||
filterKeys: string[]
|
||||
node: any
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{body.map((b, index) => {
|
||||
return (
|
||||
<div className="my-2" key={index}>
|
||||
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DisplayObj({
|
||||
obj,
|
||||
filterKeys,
|
||||
node,
|
||||
}: {
|
||||
obj: { start: number; end: number; [key: string]: any }
|
||||
filterKeys: string[]
|
||||
node: any
|
||||
}) {
|
||||
const { setHighlightRange, setCursor2 } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
setCursor2: s.setCursor2,
|
||||
}))
|
||||
const ref = useRef<HTMLPreElement>(null)
|
||||
const [hasCursor, setHasCursor] = useState(false)
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
useEffect(() => {
|
||||
if (
|
||||
node?.start === obj?.start &&
|
||||
node?.end === obj?.end &&
|
||||
node.type === obj?.type
|
||||
) {
|
||||
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
|
||||
setHasCursor(true)
|
||||
} else {
|
||||
setHasCursor(false)
|
||||
}
|
||||
}, [node.start, node.end, node.type])
|
||||
return (
|
||||
<pre
|
||||
ref={ref}
|
||||
className={`ml-2 border-l border-violet-600 pl-1 ${
|
||||
hasCursor ? 'bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
setHighlightRange([obj?.start || 0, obj.end])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
setHighlightRange([obj?.start || 0, obj.end])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<button
|
||||
className="m-0 p-0 border-0"
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
>
|
||||
{'>'}type: {obj.type}
|
||||
</button>
|
||||
) : (
|
||||
<span className="flex">
|
||||
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
|
||||
<ul className="inline-block">
|
||||
{Object.entries(obj).map(([key, value]) => {
|
||||
if (filterKeys.includes(key)) {
|
||||
return null
|
||||
} else if (Array.isArray(value)) {
|
||||
return (
|
||||
<li key={key}>
|
||||
{`${key}: [`}
|
||||
<DisplayBody
|
||||
body={value}
|
||||
filterKeys={filterKeys}
|
||||
node={node}
|
||||
/>
|
||||
{']'}
|
||||
</li>
|
||||
)
|
||||
} else if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value?.end
|
||||
) {
|
||||
return (
|
||||
<li key={key}>
|
||||
{key}:
|
||||
<DisplayObj
|
||||
obj={value}
|
||||
filterKeys={filterKeys}
|
||||
node={node}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
} else if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number'
|
||||
) {
|
||||
return (
|
||||
<li key={key}>
|
||||
{key}: {value}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</span>
|
||||
)}
|
||||
</pre>
|
||||
)
|
||||
}
|
@ -144,7 +144,7 @@ export function useCalc({
|
||||
try {
|
||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||
const ast = parser_wasm(code)
|
||||
const _programMem: any = { root: {} }
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
|
||||
isNewVariableNameUnique,
|
||||
setNewVariableName,
|
||||
shouldCreateVariable,
|
||||
setShouldCreateVariable,
|
||||
setShouldCreateVariable = () => {},
|
||||
showCheckbox = true,
|
||||
}: {
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableName: string
|
||||
setNewVariableName: (a: string) => void
|
||||
shouldCreateVariable: boolean
|
||||
setShouldCreateVariable: (a: boolean) => void
|
||||
shouldCreateVariable?: boolean
|
||||
setShouldCreateVariable?: (a: boolean) => void
|
||||
showCheckbox?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
||||
>
|
||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex flex-1">
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
{showCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
|
||||
checked={shouldCreateVariable}
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
|
||||
disabled={!shouldCreateVariable}
|
||||
name="create-new-variable"
|
||||
id="create-new-variable"
|
||||
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
|
||||
autoFocus={true}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
|
||||
!shouldCreateVariable ? 'opacity-50' : ''
|
||||
}`}
|
||||
value={newVariableName}
|
||||
|
19
src/components/CodeMenu.module.css
Normal file
19
src/components/CodeMenu.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
.button {
|
||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
||||
@apply transition-colors ease-out;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply text-chalkboard-30;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||
}
|
||||
|
||||
.button small {
|
||||
@apply text-chalkboard-60;
|
||||
}
|
||||
|
||||
:global(.dark) .button small {
|
||||
@apply text-chalkboard-40;
|
||||
}
|
82
src/components/CodeMenu.tsx
Normal file
82
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faEllipsis,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { useStore } from 'useStore'
|
||||
import styles from './CodeMenu.module.css'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { editorShortcutMeta } from './TextEditor'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
const { formatCode } = useStore((s) => ({
|
||||
formatCode: s.formatCode,
|
||||
}))
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<div
|
||||
className="relative"
|
||||
onClick={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (e.eventPhase === 3 && target.closest('a') === null) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Button className="p-0 border-none relative">
|
||||
<ActionIcon
|
||||
icon={faEllipsis}
|
||||
bgClassName={
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||
}
|
||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||
<Menu.Item>
|
||||
<button onClick={() => formatCode()} className={styles.button}>
|
||||
<span>Format code</span>
|
||||
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={handleConvertToVarClick}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
<small>{editorShortcutMeta.convertToVariable.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item>
|
||||
<a
|
||||
className={styles.button}
|
||||
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>Read the KCL docs</span>
|
||||
<small>
|
||||
On GitHub
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1 align-text-top"
|
||||
width={12}
|
||||
/>
|
||||
</small>
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
.panel {
|
||||
@apply relative overflow-auto z-0;
|
||||
@apply relative z-0;
|
||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 z-10 cursor-pointer;
|
||||
@apply flex items-center gap-2 w-full p-2;
|
||||
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-20;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
||||
title: string
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
@ -18,9 +19,11 @@ export const PanelHeader = ({
|
||||
title,
|
||||
icon,
|
||||
iconClassNames,
|
||||
menu,
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<div className="flex gap-2 align-center flex-1">
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
bgClassName={
|
||||
@ -33,6 +36,10 @@ export const PanelHeader = ({
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||
{menu}
|
||||
</div>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
||||
children,
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
||||
{...props}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
>
|
||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
||||
<PanelHeader
|
||||
title={title}
|
||||
icon={icon}
|
||||
iconClassNames={iconClassNames}
|
||||
menu={menu}
|
||||
/>
|
||||
{children}
|
||||
</details>
|
||||
)
|
||||
|
@ -62,7 +62,7 @@ export const CommandBarProvider = ({
|
||||
|
||||
const CommandBar = () => {
|
||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||
useHotkeys('meta+k', () => {
|
||||
useHotkeys(['meta+k', 'meta+/'], () => {
|
||||
if (commands.length === 0) return
|
||||
setCommandBarOpen(!commandBarOpen)
|
||||
})
|
||||
@ -196,7 +196,7 @@ const CommandBar = () => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
@ -207,7 +207,7 @@ const CommandBar = () => {
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
@ -221,10 +221,10 @@ const CommandBar = () => {
|
||||
<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"
|
||||
className="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||
as="div"
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||
<div>
|
||||
{inSubCommand && (
|
||||
@ -235,7 +235,7 @@ const CommandBar = () => {
|
||||
)}
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="bg-transparent focus:outline-none w-full"
|
||||
className="w-full bg-transparent focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
setCommandBarOpen(false)
|
||||
@ -264,12 +264,12 @@ const CommandBar = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
||||
<Combobox.Options static className="overflow-y-auto max-h-96">
|
||||
{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"
|
||||
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
|
||||
>
|
||||
<p>{commandResult.item.name}</p>
|
||||
{(commandResult.item as SubCommand).description && (
|
||||
|
161
src/components/CustomIcon.tsx
Normal file
161
src/components/CustomIcon.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
export type CustomIconName =
|
||||
| 'equal'
|
||||
| 'exit'
|
||||
| 'extrude'
|
||||
| 'horizontal'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'parallel'
|
||||
| 'sketch'
|
||||
| 'vertical'
|
||||
|
||||
export const CustomIcon = ({
|
||||
name,
|
||||
...props
|
||||
}: {
|
||||
name: CustomIconName
|
||||
} & React.SVGProps<SVGSVGElement>) => {
|
||||
switch (name) {
|
||||
case 'equal':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 8.78V7H14.52V8.78H5ZM5 13.02V11.24H14.52V13.02H5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'exit':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'extrude':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 3L10.3536 3.35355L12.3536 5.35355L11.6465 6.06066L10.5 4.91421V11.5854C11.0826 11.7913 11.5 12.3469 11.5 13C11.5 13.8284 10.8284 14.5 10 14.5C9.17157 14.5 8.5 13.8284 8.5 13C8.5 12.3469 8.91741 11.7913 9.5 11.5854V4.91421L8.35356 6.06066L7.64645 5.35355L9.64645 3.35355L10 3ZM1.95887 12.3282L8 8.63644V9.80838L2.91773 12.9142L10 17.2423L17.0823 12.9142L12 9.80838V8.63644L18.0411 12.3282L19 12.9142L19 14.9683H18V13.5253L10.5 18.1087V19.9683H9.5V18.1087L2 13.5253V14.9683H1L1 12.9142L1.95887 12.3282Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'horizontal':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 9.5H16V11.5H4V9.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'line':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.5 6C16.3284 6 17 5.32843 17 4.5C17 3.67157 16.3284 3 15.5 3C14.6716 3 14 3.67157 14 4.5C14 4.73107 14.0522 4.94993 14.1456 5.14543L5.14543 14.1456C4.94993 14.0522 4.73107 14 4.5 14C3.67157 14 3 14.6716 3 15.5C3 16.3284 3.67157 17 4.5 17C5.32843 17 6 16.3284 6 15.5C6 15.2679 5.94729 15.0482 5.8532 14.852L14.852 5.8532C15.0482 5.94729 15.2679 6 15.5 6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'move':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 2.29289L10.3536 2.64645L12.3536 4.64645L11.6465 5.35355L10.5 4.20711V8V9.50001H12L15.7929 9.50001L14.6465 8.35356L15.3536 7.64645L17.3536 9.64645L17.7071 10L17.3536 10.3536L15.3536 12.3536L14.6465 11.6465L15.7929 10.5H12H10.5V12V15.7929L11.6465 14.6464L12.3536 15.3536L10.3536 17.3536L10 17.7071L9.64645 17.3536L7.64645 15.3536L8.35356 14.6464L9.50001 15.7929V12V10.5H8.00001H4.20712L5.35357 11.6465L4.64646 12.3536L2.64646 10.3536L2.29291 10L2.64646 9.64645L4.64646 7.64645L5.35357 8.35356L4.20712 9.50001H8.00001H9.50001V8V4.20711L8.35356 5.35355L7.64645 4.64645L9.64645 2.64645L10 2.29289Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'parallel':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 16V4H6V16H8ZM14 16V4H12V16H14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'sketch':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.8037 13.4035L15.5509 14.1635L16.3682 16.8386L13.5521 16.1346L12.8186 15.3885L14.8037 13.4035ZM14.1025 12.6903L12.1175 14.6754L3.48609 5.89624C2.94588 5.34678 2.94963 4.46456 3.49448 3.91971C4.04591 3.36828 4.94112 3.37208 5.48786 3.92817L14.1025 12.6903ZM6.20094 3.22709L16.4357 13.6371L17.5003 17.1216L17.8412 18.2376L16.7091 17.9546L13.0364 17.0364L2.77301 6.59732C1.84793 5.6564 1.85434 4.14564 2.78737 3.2126C3.73167 2.2683 5.26468 2.27481 6.20094 3.22709Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'vertical':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11 4V16H9V4H11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import { useState } from 'react'
|
||||
import { ActionButton } from '../components/ActionButton'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { AstExplorer } from './AstExplorer'
|
||||
|
||||
type SketchModeCmd = Extract<
|
||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||
@ -29,7 +30,11 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
{...props}
|
||||
className={'!absolute !h-auto bottom-5 right-5 ' + className}
|
||||
className={
|
||||
'!absolute overflow-hidden !h-auto bottom-5 right-5 ' + className
|
||||
}
|
||||
// header height, top-5, and bottom-5
|
||||
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
||||
>
|
||||
<section className="p-4 flex flex-col gap-4">
|
||||
<Xyz
|
||||
@ -94,6 +99,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
>
|
||||
Send sketch mode command
|
||||
</ActionButton>
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
</section>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
|
@ -40,12 +40,12 @@ const DownloadAppBanner = () => {
|
||||
</code>
|
||||
, and isn't backed up anywhere! Visit{' '}
|
||||
<a
|
||||
href="https://github.com/KittyCAD/modeling-app/releases"
|
||||
href="https://kittycad.io/modeling-app/download"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
|
||||
>
|
||||
our GitHub repository
|
||||
our website
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</p>
|
||||
|
@ -1,8 +1,62 @@
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import {
|
||||
faBug,
|
||||
faHome,
|
||||
faRefresh,
|
||||
faTrash,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
export const ErrorPage = () => {
|
||||
let error = useRouteError()
|
||||
|
||||
console.error('error', error)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
<p className="text-2xl font-bold">Page not found</p>
|
||||
<section className="max-w-full xl:max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl mb-8 font-bold">
|
||||
An unexpected error occurred
|
||||
</h1>
|
||||
{isRouteErrorResponse(error) && (
|
||||
<p className="mb-8">
|
||||
{error.status}: {error.data}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-between gap-2 mt-6">
|
||||
{isTauri() && (
|
||||
<ActionButton Element="link" to={'/'} icon={{ icon: faHome }}>
|
||||
Go Home
|
||||
</ActionButton>
|
||||
)}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faRefresh }}
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Reload
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faTrash }}
|
||||
onClick={() => {
|
||||
window.localStorage.clear()
|
||||
}}
|
||||
>
|
||||
Clear storage
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="link"
|
||||
icon={{ icon: faBug }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
to="https://discord.com/channels/915388055236509727/1138967922614743060"
|
||||
>
|
||||
Report Bug
|
||||
</ActionButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -24,6 +24,9 @@ import {
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -108,6 +111,7 @@ export const GlobalStateProvider = ({
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(paths.SIGN_IN)
|
||||
|
||||
logout()
|
||||
},
|
||||
goToIndexPage: () => {
|
||||
@ -149,10 +153,12 @@ export const GlobalStateProvider = ({
|
||||
export default GlobalStateProvider
|
||||
|
||||
export function logout() {
|
||||
const url = withBaseUrl('/logout')
|
||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||
return fetch(url, {
|
||||
return (
|
||||
!isTauri() &&
|
||||
fetch(withBaseUrl('/logout'), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ describe('processMemory', () => {
|
||||
// Enable rotations #152
|
||||
const code = `
|
||||
const myVar = 5
|
||||
const myFn = (a) => {
|
||||
fn myFn = (a) => {
|
||||
return a - 2
|
||||
}
|
||||
const otherVar = myFn(5)
|
||||
@ -29,6 +29,7 @@ describe('processMemory', () => {
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
|
@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory } from '../lang/executor'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
@ -24,7 +24,11 @@ export const MemoryPanel = ({
|
||||
<CollapsiblePanel {...props}>
|
||||
<div className="h-full relative">
|
||||
<div className="absolute inset-0 flex flex-col items-start">
|
||||
<div className=" h-full console-tile w-full">
|
||||
<div
|
||||
className="overflow-y-auto h-full console-tile w-full"
|
||||
style={{ marginBottom: 36 }}
|
||||
>
|
||||
{/* 36px is the height of PanelHeader */}
|
||||
<ReactJson
|
||||
src={ProcessedMemory}
|
||||
collapsed={1}
|
||||
@ -49,8 +53,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
Object.keys(programMemory.root).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NetworkHealthIndicator,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('NetworkHealthIndicator tests', () => {
|
||||
test('Renders the network indicator', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
)
|
||||
})
|
||||
|
||||
test('Responds to network changes', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
)
|
||||
})
|
||||
})
|
112
src/components/NetworkHealthIndicator.tsx
Normal file
112
src/components/NetworkHealthIndicator.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamation,
|
||||
faWifi,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
|
||||
export const NETWORK_CONTENT = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
|
||||
useEffect(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'p-0 border-none relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
bgClassName={
|
||||
hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faCheck}
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
<span
|
||||
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||
data-testid="network-bad"
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { useStore } from '../useStore'
|
||||
|
||||
export const OpenFileButton = () => {
|
||||
const { setCode } = useStore((s) => ({
|
||||
setCode: s.setCode,
|
||||
}))
|
||||
const handleClick = async () => {
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'CAD',
|
||||
extensions: ['toml'],
|
||||
},
|
||||
],
|
||||
})
|
||||
if (Array.isArray(selected)) {
|
||||
// User selected multiple files
|
||||
// We should not get here, since multiple is false.
|
||||
} else if (selected === null) {
|
||||
// User cancelled the selection
|
||||
// Do nothing.
|
||||
} else {
|
||||
// User selected a single file
|
||||
// We want to invoke our command to read the file.
|
||||
const json: string = await invoke('read_toml', { path: selected })
|
||||
const packageDetails = JSON.parse(json).package
|
||||
if (packageDetails.main) {
|
||||
const absPath = [
|
||||
...selected.split('/').slice(0, -1),
|
||||
packageDetails.main,
|
||||
].join('/')
|
||||
const file: string = await invoke('read_txt_file', { path: absPath })
|
||||
setCode(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return <button onClick={() => handleClick()}>Open File</button>
|
||||
}
|
@ -16,8 +16,8 @@ const ProjectSidebarMenu = ({
|
||||
}) => {
|
||||
return renderAsLink ? (
|
||||
<Link
|
||||
to={'../'}
|
||||
className="flex items-center gap-4 my-2"
|
||||
to={paths.HOME}
|
||||
className="h-9 max-h-min min-w-max 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-link"
|
||||
>
|
||||
<img
|
||||
@ -26,7 +26,7 @@ const ProjectSidebarMenu = ({
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<span
|
||||
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max"
|
||||
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block"
|
||||
data-testid="project-sidebar-link-name"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
@ -35,15 +35,15 @@ const ProjectSidebarMenu = ({
|
||||
) : (
|
||||
<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"
|
||||
className="h-9 max-h-min min-w-max 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"
|
||||
className="h-full w-auto"
|
||||
/>
|
||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max">
|
||||
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap hidden lg:block">
|
||||
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export const SetVarNameModal = ({
|
||||
isOpen,
|
||||
@ -19,21 +22,23 @@ export const SetVarNameModal = ({
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onReject}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
onClose={onReject}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
enterFrom="opacity-0 translate-y-4"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="ease-in duration-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -43,43 +48,39 @@ export const SetVarNameModal = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
||||
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
toast.success(`Added variable ${newVariableName}`)
|
||||
}}
|
||||
>
|
||||
Set {valueName}
|
||||
</Dialog.Title>
|
||||
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
setShouldCreateVariable={() => {}}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||
!isNewVariableNameUnique
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
}
|
||||
icon={{ icon: faPlus }}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
|
@ -7,29 +7,62 @@ import {
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
compareVec2Epsilon,
|
||||
} from 'lang/std/sketch'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||
import { modify_ast_for_sketch } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from 'lang/errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { rangeTypeFix } from 'lang/abstractSyntaxTree'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const {
|
||||
mediaStream,
|
||||
engineCommandManager,
|
||||
setIsMouseDownInStream,
|
||||
setButtonDownInStream,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
||||
setButtonDownInStream: s.setButtonDownInStream,
|
||||
fileId: s.fileId,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { cameraControls },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -42,24 +75,52 @@ export const Stream = ({ className = '' }) => {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
}, [mediaStream, engineCommandManager])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
}) => {
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!videoRef.current) return
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: videoRef.current,
|
||||
...streamDimensions,
|
||||
})
|
||||
console.log('click', x, y)
|
||||
|
||||
const newId = uuidv4()
|
||||
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
if (
|
||||
interactionGuards.pan.callback(e) ||
|
||||
interactionGuards.pan.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'pan'
|
||||
} else if (
|
||||
interactionGuards.rotate.callback(e) ||
|
||||
interactionGuards.rotate.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'rotate'
|
||||
} else if (
|
||||
interactionGuards.zoom.dragCallback(e) ||
|
||||
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'zoom'
|
||||
}
|
||||
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -69,12 +130,15 @@ export const Stream = ({ className = '' }) => {
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(true)
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||
e.preventDefault()
|
||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -91,6 +155,7 @@ export const Stream = ({ className = '' }) => {
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -101,7 +166,7 @@ export const Stream = ({ className = '' }) => {
|
||||
const newCmdId = uuidv4()
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
const command: Models['WebSocketRequest_type'] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_end',
|
||||
@ -109,9 +174,8 @@ export const Stream = ({ className = '' }) => {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(false)
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -123,7 +187,218 @@ export const Stream = ({ className = '' }) => {
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}
|
||||
|
||||
if (!didDragInStream && guiMode.mode === 'default') {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
(!didDragInStream &&
|
||||
guiMode.mode === 'sketch' &&
|
||||
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||
(guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('move' as any)
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
}
|
||||
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
|
||||
if (!(guiMode.mode === 'sketch')) return
|
||||
|
||||
if (guiMode.sketchMode === 'selectFace') return
|
||||
|
||||
// Check if the sketch group already exists.
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
const variableName = varDec?.id?.name
|
||||
const sketchGroup = programMemory.root[variableName]
|
||||
const isEditingExistingSketch =
|
||||
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
|
||||
let sketchGroupId = ''
|
||||
if (sketchGroup && sketchGroup.type === 'SketchGroup') {
|
||||
sketchGroupId = sketchGroup.id
|
||||
}
|
||||
|
||||
if (
|
||||
guiMode.sketchMode === ('move' as any as 'line') &&
|
||||
command.cmd.type === 'handle_mouse_drag_end'
|
||||
) {
|
||||
// Let's get the updated ast.
|
||||
if (sketchGroupId === '') return
|
||||
|
||||
console.log('guiMode.pathId', guiMode.pathId)
|
||||
|
||||
// We have a problem if we do not have an id for the sketch group.
|
||||
if (
|
||||
guiMode.pathId === undefined ||
|
||||
guiMode.pathId === null ||
|
||||
guiMode.pathId === ''
|
||||
)
|
||||
return
|
||||
|
||||
let engineId = guiMode.pathId
|
||||
|
||||
try {
|
||||
const updatedAst: Program = await modify_ast_for_sketch(
|
||||
engineCommandManager,
|
||||
JSON.stringify(ast),
|
||||
variableName,
|
||||
engineId
|
||||
)
|
||||
|
||||
updateAst(updatedAst, false)
|
||||
} catch (e: any) {
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
throw kclError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
||||
|
||||
if (!(guiMode.sketchMode === ('sketch_line' as any as 'line'))) return
|
||||
|
||||
if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
guiMode.waitingFirstClick &&
|
||||
!isEditingExistingSketch
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
// We need to update the guiMode with the right pathId so that we can
|
||||
// move lines later and send the right sketch id to the engine.
|
||||
for (const [id, artifact] of Object.entries(
|
||||
engineCommandManager.artifactMap
|
||||
)) {
|
||||
if (artifact.commandType === 'start_path') {
|
||||
guiMode.pathId = id
|
||||
}
|
||||
}
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst, false)
|
||||
} else if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
(!guiMode.waitingFirstClick || isEditingExistingSketch)
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
|
||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketchGroup = programMemory.root[variableName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||
const initialCoords = sketchGroup.value[0].from
|
||||
|
||||
const isClose = compareVec2Epsilon(initialCoords, [
|
||||
coords[1].x,
|
||||
coords[1].y,
|
||||
])
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
_modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
updateAst(_modifiedAst, false)
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory,
|
||||
pathToNode: guiMode.pathToNode,
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
updateAst(_modifiedAst, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
setDidDragInStream(false)
|
||||
setClickCoords(undefined)
|
||||
}
|
||||
|
||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!clickCoords) return
|
||||
|
||||
const delta =
|
||||
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
|
||||
0.5
|
||||
|
||||
if (delta > 5 && !didDragInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -139,7 +414,9 @@ export const Stream = ({ className = '' }) => {
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
className="w-full h-full"
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
|
296
src/components/TextEditor.tsx
Normal file
296
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,296 @@
|
||||
import ReactCodeMirror, {
|
||||
Extension,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@uiw/react-codemirror'
|
||||
import { FromServer, IntoServer } from 'editor/lsp/codec'
|
||||
import Server from '../editor/lsp/server'
|
||||
import Client from '../editor/lsp/client'
|
||||
import { TEST } from 'env'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Selections, useStore } from 'useStore'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
EditorView,
|
||||
addLineHighlight,
|
||||
lineHighlightField,
|
||||
} from 'editor/highlightextension'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
codeMirror: 'Alt-Shift-f',
|
||||
display: 'Alt + Shift + F',
|
||||
},
|
||||
convertToVariable: {
|
||||
codeMirror: 'Ctrl-Shift-c',
|
||||
display: 'Ctrl + Shift + C',
|
||||
},
|
||||
}
|
||||
|
||||
export const TextEditor = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const pathParams = useParams()
|
||||
const {
|
||||
code,
|
||||
deferredSetCode,
|
||||
editorView,
|
||||
engineCommandManager,
|
||||
formatCode,
|
||||
isLSPServerReady,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setSelectionRanges,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
editorView: s.editorView,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
formatCode: s.formatCode,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
}))
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { textWrapping },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
// Below is the client and server promise.
|
||||
const { lspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
lspServer.start()
|
||||
setIsLSPServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsLSPServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup becuase it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isLSPServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: lspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [lspClient, isLSPServerReady])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
deferredSetCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(
|
||||
engineCommandManager?.sourceRangeMap || {}
|
||||
).filter(([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
})
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
const extensions = [
|
||||
lineHighlightField,
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Meta-k',
|
||||
run: () => {
|
||||
setCommandBarOpen(true)
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.formatCode.codeMirror,
|
||||
run: () => {
|
||||
formatCode()
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.convertToVariable.codeMirror,
|
||||
run: () => {
|
||||
if (convertEnabled) {
|
||||
convertCallback()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]),
|
||||
] as Extension[]
|
||||
|
||||
if (kclLSP) extensions.push(kclLSP)
|
||||
|
||||
// These extensions have proven to mess with vitest
|
||||
if (!TEST) {
|
||||
extensions.push(
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
interact({
|
||||
rules: [
|
||||
// a rule for a number dragger
|
||||
{
|
||||
// the regexp matching the value
|
||||
regexp: /-?\b\d+\.?\d*\b/g,
|
||||
// set cursor to "ew-resize" on hover
|
||||
cursor: 'ew-resize',
|
||||
// change number value based on mouse X movement on drag
|
||||
onDrag: (text, setText, e) => {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey
|
||||
? 0.01
|
||||
: e.metaKey
|
||||
? 0.1
|
||||
: e.shiftKey
|
||||
? 10
|
||||
: 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
|
||||
const newVal = roundOff(
|
||||
Number(text) + delta,
|
||||
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||
)
|
||||
|
||||
if (isNaN(newVal)) return
|
||||
setText(newVal.toString())
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
|
||||
}
|
||||
|
||||
return extensions
|
||||
}, [kclLSP, textWrapping])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="full-height-subtract"
|
||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
||||
>
|
||||
<ReactCodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={editorExtensions}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from '../../useStore'
|
||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
|
||||
import { SetVarNameModal } from '../SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export const ConvertToVariable = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
>
|
||||
ConvertToVariable
|
||||
</button>
|
||||
)
|
||||
}
|
@ -12,6 +12,8 @@ import {
|
||||
getTransformInfos,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
|
||||
export const EqualAngle = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
@ -82,14 +84,22 @@ export const EqualAngle = () => {
|
||||
transformInfos,
|
||||
programMemory,
|
||||
})
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableEqual}
|
||||
title="yo dawg"
|
||||
title="Parallel (or equal angle)"
|
||||
className="group"
|
||||
>
|
||||
parallel
|
||||
<ActionIcon
|
||||
icon="parallel"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Parallel
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
getTransformInfos,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
|
||||
export const EqualLength = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
@ -82,14 +84,22 @@ export const EqualLength = () => {
|
||||
transformInfos,
|
||||
programMemory,
|
||||
})
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableEqual}
|
||||
title="yo dawg"
|
||||
className="group"
|
||||
title="Equal Length"
|
||||
>
|
||||
EqualLength
|
||||
<ActionIcon
|
||||
icon="equal"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Equal Length
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
|
||||
export const HorzVert = ({
|
||||
horOrVert,
|
||||
@ -61,14 +63,22 @@ export const HorzVert = ({
|
||||
programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableHorz}
|
||||
title="yo dawg"
|
||||
className="group"
|
||||
title={horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||
>
|
||||
{horOrVert === 'horizontal' ? 'Horz' : 'Vert'}
|
||||
<ActionIcon
|
||||
icon={horOrVert === 'horizontal' ? 'horizontal' : 'vertical'}
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{horOrVert === 'horizontal' ? 'Horizontal' : 'Vertical'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ export const Intersect = () => {
|
||||
initialVariableName: 'offset',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -182,14 +182,15 @@ export const Intersect = () => {
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, {
|
||||
updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title="Set Perpendicular Distance"
|
||||
>
|
||||
perpendicularDistance
|
||||
Set Perpendicular Distance
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -65,14 +65,14 @@ export const RemoveConstrainingValues = () => {
|
||||
programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
disabled={!enableHorz}
|
||||
title="yo dawg"
|
||||
title="Remove Constraining Values"
|
||||
>
|
||||
RemoveConstrainingValues
|
||||
Remove Constraining Values
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -22,11 +22,16 @@ import { updateCursors } from '../../lang/util'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
|
||||
export const SetAbsDistance = ({
|
||||
buttonType,
|
||||
}: {
|
||||
buttonType: 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||
}) => {
|
||||
type ButtonType = 'xAbs' | 'yAbs' | 'snapToYAxis' | 'snapToXAxis'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
xAbs: 'Set distance from X Axis',
|
||||
yAbs: 'Set distance from Y Axis',
|
||||
snapToYAxis: 'Snap To Y Axis',
|
||||
snapToXAxis: 'Snap To X Axis',
|
||||
}
|
||||
|
||||
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
@ -124,7 +129,7 @@ export const SetAbsDistance = ({
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
updateAst(_modifiedAst, {
|
||||
updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
@ -132,8 +137,9 @@ export const SetAbsDistance = ({
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
title={buttonLabels[buttonType]}
|
||||
>
|
||||
{buttonType}
|
||||
{buttonLabels[buttonType]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ export const SetAngleBetween = () => {
|
||||
initialVariableName: 'angle',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -141,14 +141,15 @@ export const SetAngleBetween = () => {
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, {
|
||||
updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title="Set Angle Between"
|
||||
>
|
||||
angleBetween
|
||||
Set Angle Between
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -21,17 +21,28 @@ import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
export const SetHorzVertDistance = ({
|
||||
buttonType,
|
||||
}: {
|
||||
buttonType:
|
||||
type ButtonType =
|
||||
| 'setHorzDistance'
|
||||
| 'setVertDistance'
|
||||
| 'alignEndsHorizontally'
|
||||
| 'alignEndsVertically'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
setHorzDistance: 'Set Horizontal Distance',
|
||||
setVertDistance: 'Set Vertical Distance',
|
||||
alignEndsHorizontally: 'Align Ends Horizontally',
|
||||
alignEndsVertically: 'Align Ends Vertically',
|
||||
}
|
||||
|
||||
export const SetHorzVertDistance = ({
|
||||
buttonType,
|
||||
}: {
|
||||
buttonType: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
@ -137,7 +148,7 @@ export const SetHorzVertDistance = ({
|
||||
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any))
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, {
|
||||
updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -163,14 +174,15 @@ export const SetHorzVertDistance = ({
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, {
|
||||
updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!enable}
|
||||
title={buttonLabels[buttonType]}
|
||||
>
|
||||
{buttonType}
|
||||
{buttonLabels[buttonType]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -23,10 +23,17 @@ import { updateCursors } from '../../lang/util'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
|
||||
type ButtonType = 'setAngle' | 'setLength'
|
||||
|
||||
const buttonLabels: Record<ButtonType, string> = {
|
||||
setAngle: 'Set Angle',
|
||||
setLength: 'Set Length',
|
||||
}
|
||||
|
||||
export const SetAngleLength = ({
|
||||
angleOrLength,
|
||||
}: {
|
||||
angleOrLength: 'setAngle' | 'setLength'
|
||||
angleOrLength: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
@ -136,7 +143,7 @@ export const SetAngleLength = ({
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
updateAst(_modifiedAst, {
|
||||
updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
@ -144,8 +151,9 @@ export const SetAngleLength = ({
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
title={buttonLabels[angleOrLength]}
|
||||
>
|
||||
{angleOrLength}
|
||||
{buttonLabels[angleOrLength]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { paths } from '../Router'
|
||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||
@ -12,6 +12,7 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
type User = Models['User_type']
|
||||
|
||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const location = useLocation()
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
@ -38,7 +39,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
<Popover className="relative">
|
||||
{user?.image && !imageLoadFailed ? (
|
||||
<Popover.Button
|
||||
className="border-0 rounded-full w-fit p-0 focus:outline-none group"
|
||||
className="border-0 rounded-full w-fit min-w-max p-0 focus:outline-none group"
|
||||
data-testid="user-sidebar-toggle"
|
||||
>
|
||||
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 group-focus:border-liquid-50 overflow-hidden">
|
||||
@ -126,7 +127,11 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||
navigate(
|
||||
(location.pathname.endsWith('/')
|
||||
? location.pathname.slice(0, -1)
|
||||
: location.pathname) + paths.SETTINGS
|
||||
)
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
|
@ -208,7 +208,13 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
filterText: filterText ?? label,
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = formatContents(documentation)
|
||||
completion.info = () => {
|
||||
const htmlString = formatContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
return { dom: htmlNode }
|
||||
}
|
||||
}
|
||||
|
||||
return completion
|
||||
|
@ -5,8 +5,6 @@ import init, {
|
||||
} from '../../wasm-lib/pkg/wasm_lib'
|
||||
import { FromServer, IntoServer } from './codec'
|
||||
|
||||
let server: null | Server
|
||||
|
||||
export default class Server {
|
||||
readonly initOutput: InitOutput
|
||||
readonly #intoServer: IntoServer
|
||||
@ -26,12 +24,8 @@ export default class Server {
|
||||
intoServer: IntoServer,
|
||||
fromServer: FromServer
|
||||
): Promise<Server> {
|
||||
if (null == server) {
|
||||
const initOutput = await init()
|
||||
server = new Server(initOutput, intoServer, fromServer)
|
||||
} else {
|
||||
console.warn('Server already initialized; ignoring')
|
||||
}
|
||||
const server = new Server(initOutput, intoServer, fromServer)
|
||||
return server
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
||||
.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_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
|
||||
|
297
src/hooks/useAppMode.ts
Normal file
297
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,297 @@
|
||||
// needed somewhere to dump this logic,
|
||||
// Once we have xState this should be removed
|
||||
|
||||
import { useStore, Selections } from 'useStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
interface DefaultPlanes {
|
||||
xy: string
|
||||
// TODO re-enable
|
||||
// yz: string
|
||||
// xz: string
|
||||
}
|
||||
|
||||
export function useAppMode() {
|
||||
const {
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
selectionRanges,
|
||||
engineCommandManager,
|
||||
selectionRangeTypeMap,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
}))
|
||||
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
|
||||
useEffect(() => {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'selectFace' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
const createAndShowPlanes = async () => {
|
||||
let localDefaultPlanes: DefaultPlanes
|
||||
if (!defaultPlanes) {
|
||||
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||
setDefaultPlanes(localDefaultPlanes)
|
||||
} else {
|
||||
localDefaultPlanes = defaultPlanes
|
||||
}
|
||||
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, false)
|
||||
}
|
||||
createAndShowPlanes()
|
||||
}
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'enterSketchEdit' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
const enableSketchMode = async () => {
|
||||
let localDefaultPlanes: DefaultPlanes
|
||||
if (!defaultPlanes) {
|
||||
localDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||
setDefaultPlanes(localDefaultPlanes)
|
||||
} else {
|
||||
localDefaultPlanes = defaultPlanes
|
||||
}
|
||||
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, true)
|
||||
// TODO figure out the plane to use based on the sketch
|
||||
// maybe it's easier to make a new plane than rely on the defaults
|
||||
await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: localDefaultPlanes.xy,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
})
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
)
|
||||
await Promise.all(proms)
|
||||
}
|
||||
enableSketchMode()
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
sketchMode: 'sketchEdit',
|
||||
})
|
||||
}
|
||||
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||
}
|
||||
if (guiMode.mode === 'default') {
|
||||
const pathId =
|
||||
engineCommandManager &&
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
if (pathId) {
|
||||
setGuiMode({
|
||||
mode: 'canEditSketch',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId,
|
||||
})
|
||||
}
|
||||
} else if (guiMode.mode === 'canEditSketch') {
|
||||
if (
|
||||
!engineCommandManager ||
|
||||
!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
) {
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
guiMode,
|
||||
guiMode.mode,
|
||||
engineCommandManager,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const unSub = engineCommandManager?.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async ({ data }) => {
|
||||
if (!data.entity_id) return
|
||||
if (!defaultPlanes) return
|
||||
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||
// user clicked something else in the scene
|
||||
return
|
||||
}
|
||||
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
|
||||
{
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: data.entity_id,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
}
|
||||
)
|
||||
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||
const sketchUuid = uuidv4()
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
})
|
||||
)
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
})
|
||||
)
|
||||
const res = await Promise.all(proms)
|
||||
console.log('res', res)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId: sketchUuid,
|
||||
})
|
||||
|
||||
console.log('sketchModeResponse', sketchModeResponse)
|
||||
},
|
||||
})
|
||||
return unSub
|
||||
}, [engineCommandManager, defaultPlanes])
|
||||
}
|
||||
|
||||
async function createPlane(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
{
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
}
|
||||
) {
|
||||
const planeId = uuidv4()
|
||||
await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
return planeId
|
||||
}
|
||||
|
||||
function setDefaultPlanesHidden(
|
||||
engineCommandManager: EngineCommandManager | undefined,
|
||||
defaultPlanes: DefaultPlanes,
|
||||
hidden: boolean
|
||||
) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: hidden,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function initDefaultPlanes(
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<DefaultPlanes> {
|
||||
const xy = await createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
})
|
||||
// TODO re-enable
|
||||
// const yz = createPlane(engineCommandManager, {
|
||||
// x_axis: { x: 0, y: 1, z: 0 },
|
||||
// y_axis: { x: 0, y: 0, z: 1 },
|
||||
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
// })
|
||||
// const xz = createPlane(engineCommandManager, {
|
||||
// x_axis: { x: 1, y: 0, z: 0 },
|
||||
// y_axis: { x: 0, y: 0, z: 1 },
|
||||
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
// })
|
||||
return { xy }
|
||||
}
|
||||
|
||||
function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlapingEntries = Object.entries(artifactMap || {}).filter(
|
||||
([id, artifact]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.range) &&
|
||||
Array.isArray(artifact?.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.commandType === 'start_path' ||
|
||||
artifact.commandType === 'extend_path' ||
|
||||
artifact.commandType === 'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: overlapingEntries.find(
|
||||
([, artifact]) => artifact.commandType === 'start_path'
|
||||
)?.[0] || false
|
||||
}
|
13
src/hooks/useDotDotSlash.ts
Normal file
13
src/hooks/useDotDotSlash.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export function useDotDotSlash(): (count?: number) => string {
|
||||
const location = useLocation()
|
||||
const dotDotSlash = (count = 1): string => {
|
||||
// since we can't use relative paths (../) for windows
|
||||
if (location.pathname === '/') return ''
|
||||
const path = location.pathname.slice(0, location.pathname.lastIndexOf('/'))
|
||||
if (count <= 1) return path
|
||||
return dotDotSlash(count - 1)
|
||||
}
|
||||
return dotDotSlash
|
||||
}
|
50
src/hooks/useEngineConnectionSubscriptions.ts
Normal file
50
src/hooks/useEngineConnectionSubscriptions.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export function useEngineConnectionSubscriptions() {
|
||||
const {
|
||||
engineCommandManager,
|
||||
setCursor2,
|
||||
setHighlightRange,
|
||||
highlightRange,
|
||||
} = useStore((s) => ({
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
setCursor2: s.setCursor2,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
highlightRange: s.highlightRange,
|
||||
}))
|
||||
useEffect(() => {
|
||||
if (!engineCommandManager) return
|
||||
|
||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange =
|
||||
engineCommandManager.sourceRangeMap[data.entity_id]
|
||||
setHighlightRange(sourceRange)
|
||||
} else if (
|
||||
!highlightRange ||
|
||||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
|
||||
) {
|
||||
setHighlightRange([0, 0])
|
||||
}
|
||||
},
|
||||
})
|
||||
const unSubClick = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
setCursor2()
|
||||
return
|
||||
}
|
||||
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
|
||||
setCursor2({ range: sourceRange, type: 'default' })
|
||||
},
|
||||
})
|
||||
return () => {
|
||||
unSubHover()
|
||||
unSubClick()
|
||||
}
|
||||
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
|
||||
}
|
53
src/hooks/useSetupEngineManager.ts
Normal file
53
src/hooks/useSetupEngineManager.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { _executor } from '../lang/executor'
|
||||
import { useStore } from '../useStore'
|
||||
import { EngineCommandManager } from '../lang/std/engineConnection'
|
||||
|
||||
export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
token?: string
|
||||
) {
|
||||
const {
|
||||
setEngineCommandManager,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
setStreamDimensions,
|
||||
executeCode,
|
||||
} = useStore((s) => ({
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
setMediaStream: s.setMediaStream,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
executeCode: s.executeCode,
|
||||
}))
|
||||
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
const streamHeight = streamRef?.current?.offsetHeight
|
||||
|
||||
const width = streamWidth ? streamWidth : 0
|
||||
const quadWidth = Math.round(width / 4) * 4
|
||||
const height = streamHeight ? streamHeight : 0
|
||||
const quadHeight = Math.round(height / 4) * 4
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
streamHeight: quadHeight,
|
||||
})
|
||||
if (!width || !height) return
|
||||
const eng = new EngineCommandManager({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
token,
|
||||
})
|
||||
setEngineCommandManager(eng)
|
||||
eng.waitForReady.then(() => {
|
||||
executeCode()
|
||||
})
|
||||
return () => {
|
||||
eng?.tearDown()
|
||||
}
|
||||
}, [quadWidth, quadHeight])
|
||||
}
|
56
src/hooks/useToolbarGuards.ts
Normal file
56
src/hooks/useToolbarGuards.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export function useConvertToVariable() {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enable, setEnabled] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnabled(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}
|
||||
|
||||
return { enable, handleClick }
|
||||
}
|
@ -31,6 +31,14 @@ body.dark {
|
||||
@apply text-chalkboard-10;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply bg-chalkboard-20;
|
||||
}
|
||||
|
||||
.dark select {
|
||||
@apply bg-chalkboard-90;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2 h-2 rounded-sm;
|
||||
@apply bg-chalkboard-20;
|
||||
@ -82,11 +90,22 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.full-height-subtract {
|
||||
--height-subtract: 2.25rem;
|
||||
height: 100%;
|
||||
max-height: calc(100% - var(--height-subtract));
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-editor {
|
||||
@apply bg-transparent;
|
||||
@apply h-full bg-transparent;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller::-webkit-scrollbar {
|
||||
@apply h-0;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-activeLine,
|
||||
@ -137,14 +156,39 @@ code {
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip {
|
||||
font-size: 80%;
|
||||
@apply text-xs shadow-md;
|
||||
@apply bg-chalkboard-10 text-chalkboard-80;
|
||||
@apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-tooltip {
|
||||
@apply bg-chalkboard-110 text-chalkboard-40;
|
||||
@apply border-chalkboard-70/20 border-l-liquid-70;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip-hover {
|
||||
@apply py-1 px-2 w-max max-w-md;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip-hover .documentation {
|
||||
padding: 5;
|
||||
#code-mirror-override .cm-completionInfo {
|
||||
@apply px-4 rounded-l-none;
|
||||
@apply bg-chalkboard-10 text-liquid-90;
|
||||
@apply border-liquid-40/30;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-completionInfo {
|
||||
@apply bg-liquid-120 text-liquid-50;
|
||||
@apply border-liquid-90/60;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip-autocomplete li {
|
||||
@apply px-2 py-1;
|
||||
}
|
||||
#code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
|
||||
@apply bg-liquid-10 text-liquid-110;
|
||||
}
|
||||
.dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
|
||||
@apply bg-liquid-100 text-liquid-20;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-content {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { KCLUnexpectedError } from './errors'
|
||||
import { KCLError } from './errors'
|
||||
import { initPromise } from './rust'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
@ -139,54 +139,6 @@ const newVar = myVar + 1
|
||||
},
|
||||
])
|
||||
})
|
||||
test('using std function "log"', () => {
|
||||
const code = `log(5, "hello", aIdentifier)`
|
||||
const { body } = parser_wasm(code)
|
||||
expect(body).toEqual([
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
start: 0,
|
||||
end: 28,
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 28,
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 3,
|
||||
name: 'log',
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
start: 4,
|
||||
end: 5,
|
||||
value: 5,
|
||||
raw: '5',
|
||||
},
|
||||
{
|
||||
type: 'Literal',
|
||||
start: 7,
|
||||
end: 14,
|
||||
value: 'hello',
|
||||
raw: '"hello"',
|
||||
},
|
||||
{
|
||||
type: 'Identifier',
|
||||
start: 16,
|
||||
end: 27,
|
||||
name: 'aIdentifier',
|
||||
},
|
||||
],
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing function declaration', () => {
|
||||
@ -1560,26 +1512,26 @@ const yo = { a: { b: { c: '123' } } }
|
||||
// this is a comment
|
||||
const key = 'c'`
|
||||
const nonCodeMetaInstance = {
|
||||
type: 'NoneCodeNode',
|
||||
type: 'NonCodeNode',
|
||||
start: code.indexOf('\n// this is a comment'),
|
||||
end: code.indexOf('const key'),
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'this is a comment',
|
||||
},
|
||||
}
|
||||
const { nonCodeMeta } = parser_wasm(code)
|
||||
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||
expect(nonCodeMeta.nonCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||
|
||||
// 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 { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
||||
codeWithExtraStartWhitespace
|
||||
)
|
||||
expect(nonCodeMeta2.noneCodeNodes[0].value).toStrictEqual(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].value).toStrictEqual(
|
||||
nonCodeMetaInstance.value
|
||||
)
|
||||
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].start).not.toBe(
|
||||
nonCodeMetaInstance.start
|
||||
)
|
||||
})
|
||||
@ -1596,13 +1548,13 @@ const key = 'c'`
|
||||
const { body } = parser_wasm(code)
|
||||
const indexOfSecondLineToExpression = 2
|
||||
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
||||
.noneCodeNodes
|
||||
.nonCodeNodes
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||
type: 'NoneCodeNode',
|
||||
type: 'NonCodeNode',
|
||||
start: 106,
|
||||
end: 166,
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'this is\n a comment\n spanning a few lines',
|
||||
},
|
||||
})
|
||||
@ -1619,13 +1571,13 @@ const key = 'c'`
|
||||
|
||||
const { body } = parser_wasm(code)
|
||||
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
||||
.noneCodeNodes
|
||||
.nonCodeNodes
|
||||
expect(sketchNonCodeMeta[3]).toEqual({
|
||||
type: 'NoneCodeNode',
|
||||
type: 'NonCodeNode',
|
||||
start: 125,
|
||||
end: 141,
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'a comment',
|
||||
},
|
||||
})
|
||||
@ -1744,6 +1696,12 @@ describe('parsing errors', () => {
|
||||
_theError = e
|
||||
}
|
||||
const theError = _theError as any
|
||||
expect(theError).toEqual(new KCLUnexpectedError('Brace', [[29, 30]]))
|
||||
expect(theError).toEqual(
|
||||
new KCLError(
|
||||
'unexpected',
|
||||
'Unexpected token Token { token_type: Brace, start: 29, end: 30, value: "}" }',
|
||||
[[29, 30]]
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -33,5 +33,5 @@ export type SyntaxType =
|
||||
| 'PipeExpression'
|
||||
| 'PipeSubstitution'
|
||||
| 'Literal'
|
||||
| 'NoneCodeNode'
|
||||
| 'NonCodeNode'
|
||||
| 'UnaryExpression'
|
||||
|
@ -21,7 +21,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -77,7 +77,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
|
||||
__meta: [{ sourceRange: [13, 34] }],
|
||||
},
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { ProgramMemory } from './executor'
|
||||
import { ProgramMemory, SketchGroup } from './executor'
|
||||
import { initPromise } from './rust'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { vi } from 'vitest'
|
||||
@ -117,10 +117,10 @@ show(mySketch)
|
||||
// ].join('\n')
|
||||
// const { root } = await exe(code)
|
||||
// expect(root.mySk1.value).toHaveLength(3)
|
||||
// expect(root?.rotated?.type).toBe('sketchGroup')
|
||||
// expect(root?.rotated?.type).toBe('SketchGroup')
|
||||
// if (
|
||||
// root?.mySk1?.type !== 'sketchGroup' ||
|
||||
// root?.rotated?.type !== 'sketchGroup'
|
||||
// root?.mySk1?.type !== 'SketchGroup' ||
|
||||
// root?.rotated?.type !== 'SketchGroup'
|
||||
// )
|
||||
// throw new Error('not a sketch group')
|
||||
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
||||
@ -143,7 +143,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.mySk1).toEqual({
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -199,7 +199,7 @@ show(mySketch)
|
||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||
expect(root).toEqual({
|
||||
three: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
@ -208,7 +208,7 @@ show(mySketch)
|
||||
],
|
||||
},
|
||||
yo: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
@ -225,7 +225,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.yo).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
@ -240,7 +240,7 @@ show(mySketch)
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
@ -338,7 +338,7 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// result of `-legLen(5, min(3, 999))` should be -4
|
||||
const yVal = sketch.value?.[0]?.to?.[1]
|
||||
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
|
||||
expect(yVal).toBe(-4)
|
||||
})
|
||||
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
||||
@ -356,8 +356,8 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
|
||||
expect(sketch.value?.[1]?.from).toEqual([3, 4])
|
||||
expect(sketch.value?.[1]?.to).toEqual([6, 0])
|
||||
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
|
||||
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
|
||||
const removedUnaryExp = code.replace(
|
||||
`-legLen(segLen('seg01', %), myVar)`,
|
||||
`legLen(segLen('seg01', %), myVar)`
|
||||
@ -366,7 +366,9 @@ describe('testing math operators', () => {
|
||||
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
||||
|
||||
// without the minus sign, the y value should be 8
|
||||
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
|
||||
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
|
||||
6, 8,
|
||||
])
|
||||
})
|
||||
it('with nested callExpression and binaryExpression', async () => {
|
||||
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
||||
@ -397,7 +399,10 @@ show(theExtrude)`
|
||||
|
||||
// helpers
|
||||
|
||||
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, return: null }
|
||||
) {
|
||||
const ast = parser_wasm(code)
|
||||
|
||||
const result = await enginelessExecutor(ast, programMemory)
|
||||
|
@ -5,96 +5,21 @@ import {
|
||||
SourceRangeMap,
|
||||
} from './std/engineConnection'
|
||||
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
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 PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
||||
export type Metadata = {
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
export type Position = [number, number, number]
|
||||
export type Rotation = [number, number, number, number]
|
||||
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
|
||||
export type { Position } from '../wasm-lib/kcl/bindings/Position'
|
||||
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
|
||||
export type { Path } from '../wasm-lib/kcl/bindings/Path'
|
||||
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||
|
||||
interface BasePath {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
name?: string
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export interface ToPoint extends BasePath {
|
||||
type: 'toPoint'
|
||||
}
|
||||
|
||||
export interface Base extends BasePath {
|
||||
type: 'base'
|
||||
}
|
||||
|
||||
export interface HorizontalLineTo extends BasePath {
|
||||
type: 'horizontalLineTo'
|
||||
x: number
|
||||
}
|
||||
|
||||
export interface AngledLineTo extends BasePath {
|
||||
type: 'angledLineTo'
|
||||
angle: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
interface GeoMeta {
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
|
||||
|
||||
export interface SketchGroup {
|
||||
type: 'sketchGroup'
|
||||
id: string
|
||||
value: Path[]
|
||||
start?: Base
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
interface ExtrudePlane {
|
||||
type: 'extrudePlane'
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type ExtrudeSurface = GeoMeta &
|
||||
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
|
||||
|
||||
export interface ExtrudeGroup {
|
||||
type: 'extrudeGroup'
|
||||
id: string
|
||||
value: ExtrudeSurface[]
|
||||
height: number
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
/** UserVal not produced by one of our internal functions */
|
||||
export interface UserVal {
|
||||
type: 'userVal'
|
||||
value: any
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
||||
export type PathToNode = [string | number, string][]
|
||||
|
||||
interface Memory {
|
||||
[key: string]: MemoryItem
|
||||
@ -102,12 +27,12 @@ interface Memory {
|
||||
|
||||
export interface ProgramMemory {
|
||||
root: Memory
|
||||
return?: ProgramReturn
|
||||
return: ProgramReturn | null
|
||||
}
|
||||
|
||||
export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
// work around while the gemotry is still be stored on the frontend
|
||||
// will be removed when the stream UI is added.
|
||||
@ -123,7 +48,7 @@ export const executor = async (
|
||||
engineCommandManager
|
||||
)
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
await engineCommandManager.waitForAllCommands(node, _programMemory)
|
||||
tempMapCallback({ artifactMap, sourceRangeMap })
|
||||
|
||||
engineCommandManager.endSession()
|
||||
@ -132,7 +57,7 @@ export const executor = async (
|
||||
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<ProgramMemory> => {
|
||||
try {
|
||||
|
@ -106,7 +106,7 @@ describe('Testing addSketchTo', () => {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: { noneCodeNodes: {}, start: null },
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||
},
|
||||
'yz'
|
||||
)
|
||||
@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
|
||||
expect(str).toBe(`const part001 = startSketchAt('default')
|
||||
|> ry(90, %)
|
||||
|> line('default', %)
|
||||
show(part001)`)
|
||||
show(part001)
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -175,11 +176,14 @@ show(part001)`
|
||||
})
|
||||
|
||||
describe('Testing moveValueIntoNewVariable', () => {
|
||||
const fn = (fnName: string) => `const ${fnName} = (x) => {
|
||||
const fn = (fnName: string) => `fn ${fnName} = (x) => {
|
||||
return x
|
||||
}
|
||||
`
|
||||
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
||||
const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
|
||||
fn ghi = (x) => {
|
||||
return 2
|
||||
}
|
||||
const abc = 3
|
||||
const identifierGuy = 5
|
||||
const yo = 5 + 6
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Selection, TooTip } from '../useStore'
|
||||
import { Selection, ToolTip } from '../useStore'
|
||||
import {
|
||||
Program,
|
||||
CallExpression,
|
||||
@ -27,6 +27,48 @@ import {
|
||||
getFirstArg,
|
||||
createFirstArg,
|
||||
} from './std/sketch'
|
||||
import { isLiteralArrayOrStatic } from './std/sketchcombos'
|
||||
|
||||
export function addStartSketch(
|
||||
node: Program,
|
||||
start: [number, number],
|
||||
end: [number, number]
|
||||
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
||||
const _node = { ...node }
|
||||
const _name = findUniqueName(node, 'part')
|
||||
|
||||
const startSketchAt = createCallExpression('startSketchAt', [
|
||||
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
|
||||
])
|
||||
const initialLineTo = createCallExpression('line', [
|
||||
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
const pipeBody = [startSketchAt, initialLineTo]
|
||||
|
||||
const variableDeclaration = createVariableDeclaration(
|
||||
_name,
|
||||
createPipeExpression(pipeBody)
|
||||
)
|
||||
|
||||
const newIndex = node.body.length
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[newIndex.toString(10), 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
id: _name,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
export function addSketchTo(
|
||||
node: Program,
|
||||
@ -151,7 +193,7 @@ export function mutateArrExp(
|
||||
): boolean {
|
||||
if (node.type === 'ArrayExpression') {
|
||||
node.elements.forEach((element, i) => {
|
||||
if (element.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(element)) {
|
||||
node.elements[i] = updateWith.elements[i]
|
||||
}
|
||||
})
|
||||
@ -169,8 +211,8 @@ export function mutateObjExpProp(
|
||||
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
|
||||
if (keyIndex !== -1) {
|
||||
if (
|
||||
updateWith.type === 'Literal' &&
|
||||
node.properties[keyIndex].value.type === 'Literal'
|
||||
isLiteralArrayOrStatic(updateWith) &&
|
||||
isLiteralArrayOrStatic(node.properties[keyIndex].value)
|
||||
) {
|
||||
node.properties[keyIndex].value = updateWith
|
||||
return true
|
||||
@ -180,7 +222,7 @@ export function mutateObjExpProp(
|
||||
) {
|
||||
const arrExp = node.properties[keyIndex].value as ArrayExpression
|
||||
arrExp.elements.forEach((element, i) => {
|
||||
if (element.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(element)) {
|
||||
arrExp.elements[i] = updateWith.elements[i]
|
||||
}
|
||||
})
|
||||
@ -263,7 +305,11 @@ export function extrudeSketch(
|
||||
}
|
||||
const name = findUniqueName(node, 'part')
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
let showCallIndex = getShowIndex(_node)
|
||||
if (showCallIndex == -1) {
|
||||
// We didn't find a show, so let's just append everything
|
||||
showCallIndex = _node.body.length
|
||||
}
|
||||
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
['body', ''],
|
||||
@ -491,7 +537,7 @@ export function createPipeExpression(
|
||||
start: 0,
|
||||
end: 0,
|
||||
body,
|
||||
nonCodeMeta: { noneCodeNodes: {}, start: null },
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: null },
|
||||
}
|
||||
}
|
||||
|
||||
@ -593,7 +639,7 @@ export function giveSketchFnCallTag(
|
||||
createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal
|
||||
const tagStr = String(tagValue.value)
|
||||
const newFirstArg = createFirstArg(
|
||||
primaryCallExp.callee.name as TooTip,
|
||||
primaryCallExp.callee.name as ToolTip,
|
||||
firstArg.val,
|
||||
tagValue
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
|
||||
import { Selection, TooTip } from '../useStore'
|
||||
import { Selection, ToolTip } from '../useStore'
|
||||
import {
|
||||
BinaryExpression,
|
||||
Program,
|
||||
@ -457,7 +457,7 @@ export function isLinesParallelAndConstrained(
|
||||
const secondaryFirstArg = getFirstArg(secondaryNode)
|
||||
const constraintType = getConstraintType(
|
||||
secondaryFirstArg.val,
|
||||
secondaryNode.callee.name as TooTip
|
||||
secondaryNode.callee.name as ToolTip
|
||||
)
|
||||
const constraintLevel = getConstraintLevelFromSourceRange(
|
||||
secondaryLine.range,
|
||||
|
@ -11,26 +11,27 @@ describe('recast', () => {
|
||||
const code = '1 + 2'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('variable declaration', () => {
|
||||
const code = 'const myVar = 5'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it("variable declaration that's binary with string", () => {
|
||||
const code = "const myVar = 5 + 'yo'"
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
|
||||
const { ast: ast2 } = code2ast(codeWithOtherQuotes)
|
||||
expect(recast(ast2)).toBe(codeWithOtherQuotes)
|
||||
expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
|
||||
})
|
||||
it('test assigning two variables, the second summing with the first', () => {
|
||||
const code = `const myVar = 5
|
||||
const newVar = myVar + 1`
|
||||
const newVar = myVar + 1
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -42,11 +43,12 @@ const newVar = myVar + 1`
|
||||
)
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('test with function call', () => {
|
||||
const code = `const myVar = "hello"
|
||||
log(5, myVar)`
|
||||
log(5, myVar)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -61,7 +63,7 @@ log(5, myVar)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('recast sketch declaration', () => {
|
||||
let code = `const mySketch = startSketchAt([0, 0])
|
||||
@ -70,10 +72,11 @@ log(5, myVar)`
|
||||
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|
||||
|> close(%)
|
||||
|
||||
show(mySketch)`
|
||||
show(mySketch)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('sketch piped into callExpression', () => {
|
||||
const code = [
|
||||
@ -85,7 +88,7 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast BinaryExpression piped into CallExpression', () => {
|
||||
const code = [
|
||||
@ -97,37 +100,37 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('recast nested binary expression', () => {
|
||||
const code = ['const myVar = 1 + 2 * 5'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast nested binary expression with parans', () => {
|
||||
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('unnecessary paran wrap will be remove', () => {
|
||||
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.replace('(', '').replace(')', ''))
|
||||
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
|
||||
})
|
||||
it('complex nested binary expression', () => {
|
||||
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('multiplied paren expressions', () => {
|
||||
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast array declaration', () => {
|
||||
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
||||
@ -135,7 +138,7 @@ show(mySketch)`
|
||||
)
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast long array declaration', () => {
|
||||
const code = [
|
||||
@ -150,7 +153,7 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast long object exectution', () => {
|
||||
const code = `const three = 3
|
||||
@ -159,26 +162,29 @@ const yo = {
|
||||
anum: 2,
|
||||
identifier: three,
|
||||
binExp: 4 + 5
|
||||
}`
|
||||
}
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('recast short object exectution', () => {
|
||||
const code = `const yo = { key: 'val' }`
|
||||
const code = `const yo = { key: 'val' }
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('recast object execution with member expression', () => {
|
||||
const code = `const yo = { a: { b: { c: '123' } } }
|
||||
const key = 'c'
|
||||
const myVar = yo.a['b'][key]
|
||||
const key2 = 'b'
|
||||
const myVar2 = yo['a'][key2].c`
|
||||
const myVar2 = yo['a'][key2].c
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
})
|
||||
|
||||
@ -186,7 +192,8 @@ describe('testing recasting with comments and whitespace', () => {
|
||||
it('code with comments', () => {
|
||||
const code = `const yo = { a: { b: { c: '123' } } }
|
||||
// this is a comment
|
||||
const key = 'c'`
|
||||
const key = 'c'
|
||||
`
|
||||
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
@ -199,7 +206,8 @@ const key = 'c'`
|
||||
/* this is
|
||||
a
|
||||
comment */
|
||||
const yo = 'bing'`
|
||||
const yo = 'bing'
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -209,13 +217,14 @@ const yo = 'bing'`
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
const key = 'c'
|
||||
|
||||
// this is also a comment`
|
||||
// this is also a comment
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('comments in a fn block', () => {
|
||||
const code = `const myFn = () => {
|
||||
const code = `fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
|
||||
@ -223,7 +232,8 @@ const key = 'c'
|
||||
comment */
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
}`
|
||||
}
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -239,7 +249,7 @@ const key = 'c'
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('comments sprinkled in all over the place', () => {
|
||||
const code = `
|
||||
@ -261,7 +271,8 @@ const mySk1 = startSketchAt([0, 0])
|
||||
|> rx(45, %)
|
||||
/*
|
||||
one more for good measure
|
||||
*/`
|
||||
*/
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(`// comment at start
|
||||
@ -278,7 +289,8 @@ a comment between pipe expression statements */
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure`)
|
||||
// one more for good measure
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -287,19 +299,19 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
||||
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('nested callExpression in unaryExpression', () => {
|
||||
const code = 'const myVar = -min(100, legLen(5, 3))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('with unaryExpression in callExpression', () => {
|
||||
const code = 'const myVar = min(5, -legLen(5, 4))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('with unaryExpression in sketch situation', () => {
|
||||
const code = [
|
||||
@ -308,7 +320,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
})
|
||||
|
||||
@ -323,7 +335,8 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
|> line([-0.42, -1.72], %)
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -333,7 +346,8 @@ show(part001)`
|
||||
angle: 201,
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)`
|
||||
}, %)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -342,7 +356,8 @@ show(part001)`
|
||||
|
||||
describe('it recasts binary expression using brackets where needed', () => {
|
||||
it('when there are two minus in a row', () => {
|
||||
const code = `const part001 = 1 - (def - abc)`
|
||||
const code = `const part001 = 1 - (def - abc)
|
||||
`
|
||||
const recasted = recast(code2ast(code).ast)
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
|
@ -1,20 +1,29 @@
|
||||
import { SourceRange } from 'lang/executor'
|
||||
import { ProgramMemory, SourceRange } from 'lang/executor'
|
||||
import { Selections } from 'useStore'
|
||||
import {
|
||||
VITE_KC_API_WS_MODELING_URL,
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS,
|
||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
|
||||
} from 'env'
|
||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||
|
||||
interface ResultCommand {
|
||||
let lastMessage = ''
|
||||
|
||||
interface CommandInfo {
|
||||
commandType: CommandTypes
|
||||
range: SourceRange
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
|
||||
interface ResultCommand extends CommandInfo {
|
||||
type: 'result'
|
||||
data: any
|
||||
raw: WebSocketResponse
|
||||
}
|
||||
interface PendingCommand {
|
||||
interface PendingCommand extends CommandInfo {
|
||||
type: 'pending'
|
||||
promise: Promise<any>
|
||||
resolve: (val: any) => void
|
||||
@ -32,7 +41,7 @@ interface NewTrackArgs {
|
||||
mediaStream: MediaStream
|
||||
}
|
||||
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
type ClientMetrics = Models['ClientMetrics_type']
|
||||
|
||||
// EngineConnection encapsulates the connection(s) to the Engine
|
||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||
@ -53,6 +62,9 @@ export class EngineConnection {
|
||||
private onClose: (engineConnection: EngineConnection) => void
|
||||
private onNewTrack: (track: NewTrackArgs) => void
|
||||
|
||||
// TODO: actual type is ClientMetrics
|
||||
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||
|
||||
constructor({
|
||||
url,
|
||||
token,
|
||||
@ -188,6 +200,7 @@ export class EngineConnection {
|
||||
)
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
Promise.all([
|
||||
handshakeSpan.promise,
|
||||
iceSpan.promise,
|
||||
@ -197,6 +210,7 @@ export class EngineConnection {
|
||||
console.log('All spans finished, reporting')
|
||||
webrtcMediaTransaction?.finish()
|
||||
})
|
||||
}
|
||||
|
||||
this.onWebsocketOpen(this)
|
||||
})
|
||||
@ -297,8 +311,10 @@ export class EngineConnection {
|
||||
|
||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
||||
if (this.pc?.iceConnectionState === 'connected') {
|
||||
if (this.shouldTrace()) {
|
||||
iceSpan.resolve?.()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.pc.addEventListener('icecandidate', (event) => {
|
||||
@ -330,6 +346,17 @@ export class EngineConnection {
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
} else if (resp.type === 'metrics_request') {
|
||||
if (this.webrtcStatsCollector === undefined) {
|
||||
// TODO: Error message here?
|
||||
return
|
||||
}
|
||||
this.webrtcStatsCollector().then((client_metrics) => {
|
||||
this.send({
|
||||
type: 'metrics_response',
|
||||
metrics: client_metrics,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(paultag): This ought to be both controllable, as well as something
|
||||
@ -361,127 +388,57 @@ export class EngineConnection {
|
||||
})
|
||||
}
|
||||
|
||||
// Set up the background thread to keep an eye on statistical
|
||||
// information about the WebRTC media stream from the server to
|
||||
// us. We'll also eventually want more global statistical information,
|
||||
// but this will give us a baseline.
|
||||
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
|
||||
setInterval(() => {
|
||||
if (this.pc === undefined) {
|
||||
return
|
||||
}
|
||||
if (!this.shouldTrace()) {
|
||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (mediaStream.getVideoTracks().length !== 1) {
|
||||
reject(new Error('too many video tracks to report'))
|
||||
return
|
||||
}
|
||||
|
||||
// Use the WebRTC Statistics API to collect statistical information
|
||||
// about the WebRTC connection we're using to report to Sentry.
|
||||
mediaStream.getVideoTracks().forEach((videoTrack) => {
|
||||
let trackStats = new Map<string, any>()
|
||||
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||
// Sentry only allows 10 metrics per transaction. We're going
|
||||
// to have to pick carefully here, eventually send like a prom
|
||||
// file or something to the peer.
|
||||
let client_metrics: ClientMetrics = {
|
||||
rtc_frames_decoded: 0,
|
||||
rtc_frames_dropped: 0,
|
||||
rtc_frames_received: 0,
|
||||
rtc_frames_per_second: 0,
|
||||
rtc_freeze_count: 0,
|
||||
rtc_jitter_sec: 0.0,
|
||||
rtc_keyframes_decoded: 0,
|
||||
rtc_total_freezes_duration_sec: 0.0,
|
||||
}
|
||||
|
||||
// TODO(paultag): Since we can technically have multiple WebRTC
|
||||
// video tracks (even if the Server doesn't at the moment), we
|
||||
// ought to send stats for every video track(?), and add the stream
|
||||
// ID into it. This raises the cardinality of collected metrics
|
||||
// when/if we do, but for now, just report the one stream.
|
||||
|
||||
const transaction = Sentry.startTransaction({
|
||||
name: 'webrtc-stats',
|
||||
})
|
||||
videoTrackStats.forEach((videoTrackReport) => {
|
||||
if (videoTrackReport.type === 'inbound-rtp') {
|
||||
// RTC Stream Info
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.framesDecoded',
|
||||
// videoTrackReport.framesDecoded,
|
||||
// 'frame'
|
||||
// )
|
||||
transaction.setMeasurement(
|
||||
'rtcFramesDropped',
|
||||
videoTrackReport.framesDropped,
|
||||
''
|
||||
)
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.framesReceived',
|
||||
// videoTrackReport.framesReceived,
|
||||
// 'frame'
|
||||
// )
|
||||
transaction.setMeasurement(
|
||||
'rtcFramesPerSecond',
|
||||
videoTrackReport.framesPerSecond,
|
||||
'fps'
|
||||
)
|
||||
transaction.setMeasurement(
|
||||
'rtcFreezeCount',
|
||||
videoTrackReport.freezeCount,
|
||||
''
|
||||
)
|
||||
transaction.setMeasurement(
|
||||
'rtcJitter',
|
||||
videoTrackReport.jitter,
|
||||
'second'
|
||||
)
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.jitterBufferDelay',
|
||||
// videoTrackReport.jitterBufferDelay,
|
||||
// ''
|
||||
// )
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.jitterBufferEmittedCount',
|
||||
// videoTrackReport.jitterBufferEmittedCount,
|
||||
// ''
|
||||
// )
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.jitterBufferMinimumDelay',
|
||||
// videoTrackReport.jitterBufferMinimumDelay,
|
||||
// ''
|
||||
// )
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.jitterBufferTargetDelay',
|
||||
// videoTrackReport.jitterBufferTargetDelay,
|
||||
// ''
|
||||
// )
|
||||
transaction.setMeasurement(
|
||||
'rtcKeyFramesDecoded',
|
||||
videoTrackReport.keyFramesDecoded,
|
||||
''
|
||||
)
|
||||
transaction.setMeasurement(
|
||||
'rtcTotalFreezesDuration',
|
||||
videoTrackReport.totalFreezesDuration,
|
||||
'second'
|
||||
)
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.totalInterFrameDelay',
|
||||
// videoTrackReport.totalInterFrameDelay,
|
||||
// ''
|
||||
// )
|
||||
transaction.setMeasurement(
|
||||
'rtcTotalPausesDuration',
|
||||
videoTrackReport.totalPausesDuration,
|
||||
'second'
|
||||
)
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.totalProcessingDelay',
|
||||
// videoTrackReport.totalProcessingDelay,
|
||||
// 'second'
|
||||
// )
|
||||
client_metrics.rtc_frames_decoded =
|
||||
videoTrackReport.framesDecoded
|
||||
client_metrics.rtc_frames_dropped =
|
||||
videoTrackReport.framesDropped
|
||||
client_metrics.rtc_frames_received =
|
||||
videoTrackReport.framesReceived
|
||||
client_metrics.rtc_frames_per_second =
|
||||
videoTrackReport.framesPerSecond || 0
|
||||
client_metrics.rtc_freeze_count =
|
||||
videoTrackReport.freezeCount || 0
|
||||
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
|
||||
client_metrics.rtc_keyframes_decoded =
|
||||
videoTrackReport.keyFramesDecoded
|
||||
client_metrics.rtc_total_freezes_duration_sec =
|
||||
videoTrackReport.totalFreezesDuration || 0
|
||||
} else if (videoTrackReport.type === 'transport') {
|
||||
// // Bytes i/o
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.bytesReceived',
|
||||
// videoTrackReport.bytesReceived,
|
||||
// 'byte'
|
||||
// )
|
||||
// transaction.setMeasurement(
|
||||
// 'mediaStreamTrack.bytesSent',
|
||||
// videoTrackReport.bytesSent,
|
||||
// 'byte'
|
||||
// )
|
||||
}
|
||||
})
|
||||
transaction?.finish()
|
||||
resolve(client_metrics)
|
||||
})
|
||||
})
|
||||
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
|
||||
}
|
||||
|
||||
this.onNewTrack({
|
||||
@ -490,10 +447,6 @@ export class EngineConnection {
|
||||
})
|
||||
})
|
||||
|
||||
// During startup, we'll track the time from `connect` being called
|
||||
// until the 'done' event fires.
|
||||
let connectionStarted = new Date()
|
||||
|
||||
this.pc.addEventListener('datachannel', (event) => {
|
||||
this.unreliableDataChannel = event.channel
|
||||
|
||||
@ -523,6 +476,13 @@ export class EngineConnection {
|
||||
|
||||
this.onConnectionStarted(this)
|
||||
}
|
||||
unreliableSend(message: object | string) {
|
||||
// TODO(paultag): Add in logic to determine the connection state and
|
||||
// take actions if needed?
|
||||
this.unreliableDataChannel?.send(
|
||||
typeof message === 'string' ? message : JSON.stringify(message)
|
||||
)
|
||||
}
|
||||
send(message: object | string) {
|
||||
// TODO(paultag): Add in logic to determine the connection state and
|
||||
// take actions if needed?
|
||||
@ -537,6 +497,7 @@ export class EngineConnection {
|
||||
this.websocket = undefined
|
||||
this.pc = undefined
|
||||
this.unreliableDataChannel = undefined
|
||||
this.webrtcStatsCollector = undefined
|
||||
|
||||
this.onClose(this)
|
||||
this.ready = false
|
||||
@ -546,6 +507,8 @@ export class EngineConnection {
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
||||
|
||||
type CommandTypes = Models['ModelingCmd_type']['type']
|
||||
|
||||
type UnreliableResponses = Extract<
|
||||
Models['OkModelingCmdResponse_type'],
|
||||
{ type: 'highlight_set_entity' }
|
||||
@ -687,16 +650,26 @@ export class EngineCommandManager {
|
||||
const resolve = command.resolve
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
range: command.range,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command?.commandType,
|
||||
range: command?.range,
|
||||
data: modelingResponse,
|
||||
raw: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -747,8 +720,29 @@ export class EngineCommandManager {
|
||||
delete this.unreliableSubscriptions[event][id]
|
||||
}
|
||||
endSession() {
|
||||
// this.websocket?.close()
|
||||
// socket.off('command')
|
||||
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||
// we need to loop over them each individualy because if the engine doesn't recognise a single
|
||||
// id the whole command fails.
|
||||
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
|
||||
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
|
||||
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
|
||||
// however all of the segments in the path are its children so there don't need to be deleted.
|
||||
// this fact is very opaque in the api and docs (as to what should can be deleted).
|
||||
// Using an array is the list is likely to grow.
|
||||
'start_path',
|
||||
]
|
||||
if (!artifactTypesToDelete.includes(artifact.commandType)) return
|
||||
|
||||
const deletCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'remove_scene_objects',
|
||||
object_ids: [id],
|
||||
},
|
||||
}
|
||||
this.engineConnection?.send(deletCmd)
|
||||
})
|
||||
}
|
||||
cusorsSelected(selections: {
|
||||
otherSelections: Selections['otherSelections']
|
||||
@ -775,6 +769,13 @@ export class EngineCommandManager {
|
||||
})
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
console.log('sending command', command.cmd.type)
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('socket not ready')
|
||||
return Promise.resolve()
|
||||
@ -782,14 +783,13 @@ export class EngineCommandManager {
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||
const cmd = command.cmd
|
||||
if (
|
||||
cmd.type === 'camera_drag_move' &&
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
cmd.type === 'handle_mouse_drag_move') &&
|
||||
this.engineConnection?.unreliableDataChannel
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableDataChannel?.send(
|
||||
JSON.stringify(command)
|
||||
)
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
} else if (
|
||||
cmd.type === 'highlight_set_entity' &&
|
||||
@ -797,15 +797,20 @@ export class EngineCommandManager {
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableDataChannel?.send(
|
||||
JSON.stringify(command)
|
||||
)
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
} else if (
|
||||
cmd.type === 'mouse_move' &&
|
||||
this.engineConnection.unreliableDataChannel
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableSend(command)
|
||||
return Promise.resolve()
|
||||
}
|
||||
console.log('sending command', command)
|
||||
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||
this.engineConnection?.send(command)
|
||||
return this.handlePendingCommand(command.cmd_id)
|
||||
return this.handlePendingCommand(command.cmd_id, command.cmd)
|
||||
}
|
||||
sendModelingCommand({
|
||||
id,
|
||||
@ -823,15 +828,35 @@ export class EngineCommandManager {
|
||||
return Promise.resolve()
|
||||
}
|
||||
this.engineConnection?.send(command)
|
||||
return this.handlePendingCommand(id)
|
||||
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, command?.cmd, range)
|
||||
} else if (typeof command === 'string') {
|
||||
const parseCommand: EngineCommand = JSON.parse(command)
|
||||
if (parseCommand.type === 'modeling_cmd_req')
|
||||
return this.handlePendingCommand(id, parseCommand?.cmd, range)
|
||||
}
|
||||
handlePendingCommand(id: string) {
|
||||
throw 'shouldnt reach here'
|
||||
}
|
||||
handlePendingCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
range?: SourceRange
|
||||
) {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command.type === 'extend_path') {
|
||||
return command.path
|
||||
}
|
||||
// TODO handle other commands that have a parent
|
||||
}
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
@ -853,7 +878,10 @@ export class EngineCommandManager {
|
||||
}
|
||||
const range: SourceRange = JSON.parse(rangeStr)
|
||||
|
||||
return this.sendModelingCommand({ id, range, command: commandStr })
|
||||
// We only care about the modeling command response.
|
||||
return this.sendModelingCommand({ id, range, command: commandStr }).then(
|
||||
({ raw }) => JSON.stringify(raw)
|
||||
)
|
||||
}
|
||||
commandResult(id: string): Promise<any> {
|
||||
const command = this.artifactMap[id]
|
||||
@ -865,7 +893,10 @@ export class EngineCommandManager {
|
||||
}
|
||||
return command.promise
|
||||
}
|
||||
async waitForAllCommands(): Promise<{
|
||||
async waitForAllCommands(
|
||||
ast?: Program,
|
||||
programMemory?: ProgramMemory
|
||||
): Promise<{
|
||||
artifactMap: ArtifactMap
|
||||
sourceRangeMap: SourceRangeMap
|
||||
}> {
|
||||
@ -874,9 +905,92 @@ export class EngineCommandManager {
|
||||
) as PendingCommand[]
|
||||
const proms = pendingCommands.map(({ promise }) => promise)
|
||||
await Promise.all(proms)
|
||||
if (ast && programMemory) {
|
||||
await this.fixIdMappings(ast, programMemory)
|
||||
}
|
||||
|
||||
return {
|
||||
artifactMap: this.artifactMap,
|
||||
sourceRangeMap: this.sourceRangeMap,
|
||||
}
|
||||
}
|
||||
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
|
||||
/* This is a temporary solution since the cmd_ids that are sent through when
|
||||
sending 'extend_path' ids are not used as the segment ids.
|
||||
|
||||
We have a way to back fill them with 'path_get_info', however this relies on one
|
||||
the sketchGroup array and the segements array returned from the server to be in
|
||||
the same length and order. plus it's super hacky, we first use the path_id to get
|
||||
the source range of the pipe expression then use the name of the variable to get
|
||||
the sketchGroup from programMemory.
|
||||
|
||||
I feel queezy about relying on all these steps to always line up.
|
||||
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
|
||||
We should get the cmd_ids to match with the segment ids and delete this method.
|
||||
*/
|
||||
const pathInfoProms = []
|
||||
for (const [id, artifact] of Object.entries(this.artifactMap)) {
|
||||
if (artifact.commandType === 'start_path') {
|
||||
pathInfoProms.push(
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_info',
|
||||
path_id: id,
|
||||
},
|
||||
}).then(({ data }) => ({
|
||||
originalId: id,
|
||||
segments: data?.data?.segments,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pathInfos = await Promise.all(pathInfoProms)
|
||||
pathInfos.forEach(({ originalId, segments }) => {
|
||||
const originalArtifact = this.artifactMap[originalId]
|
||||
if (!originalArtifact || originalArtifact.type === 'pending') {
|
||||
return
|
||||
}
|
||||
const pipeExpPath = getNodePathFromSourceRange(
|
||||
ast,
|
||||
originalArtifact.range
|
||||
)
|
||||
const pipeExp = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
pipeExpPath,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
if (pipeExp.type !== 'VariableDeclarator') {
|
||||
return
|
||||
}
|
||||
const variableName = pipeExp.id.name
|
||||
const memoryItem = programMemory.root[variableName]
|
||||
if (!memoryItem) {
|
||||
return
|
||||
} else if (memoryItem.type !== 'SketchGroup') {
|
||||
return
|
||||
}
|
||||
|
||||
const relevantSegments = segments.filter(
|
||||
({ command_id }: { command_id: string | null }) => command_id
|
||||
)
|
||||
if (memoryItem.value.length !== relevantSegments.length) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < relevantSegments.length; i++) {
|
||||
const engineSegment = relevantSegments[i]
|
||||
const memorySegment = memoryItem.value[i]
|
||||
const oldId = memorySegment.__geoMeta.id
|
||||
const artifact = this.artifactMap[oldId]
|
||||
delete this.artifactMap[oldId]
|
||||
delete this.sourceRangeMap[oldId]
|
||||
if (artifact) {
|
||||
this.artifactMap[engineSegment.command_id] = artifact
|
||||
this.sourceRangeMap[engineSegment.command_id] = artifact.range
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
addNewSketchLn,
|
||||
getYComponent,
|
||||
getXComponent,
|
||||
addCloseToPipe,
|
||||
} from './sketch'
|
||||
import { parser_wasm } from '../abstractSyntaxTree'
|
||||
import { getNodePathFromSourceRange } from '../queryAst'
|
||||
@ -101,7 +102,8 @@ describe('testing changeSketchArguments', () => {
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
const ast = parser_wasm(code)
|
||||
@ -115,6 +117,7 @@ show(mySketch001)`
|
||||
{
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathId: '',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [
|
||||
@ -145,7 +148,7 @@ show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(lineToChange)
|
||||
expect(sourceStart).toBe(66)
|
||||
const { modifiedAst } = addNewSketchLn({
|
||||
let { modifiedAst } = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [2, 3],
|
||||
@ -159,12 +162,34 @@ show(mySketch001)`
|
||||
],
|
||||
})
|
||||
// Enable rotations #152
|
||||
const expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||
let expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||
// |> rx(45, %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> lineTo([2, 3], %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
|
||||
modifiedAst = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory,
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
})
|
||||
|
||||
expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||
// |> rx(45, %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> close(%)
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
})
|
||||
@ -177,7 +202,8 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(originalLine)
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
PathToNode,
|
||||
MemoryItem,
|
||||
} from '../executor'
|
||||
import {
|
||||
Program,
|
||||
@ -19,8 +20,9 @@ import {
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||
import { GuiModes, toolTips, ToolTip } from '../../useStore'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
@ -55,7 +57,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
|
||||
}
|
||||
|
||||
export function createFirstArg(
|
||||
sketchFn: TooTip,
|
||||
sketchFn: ToolTip,
|
||||
val: Value | [Value, Value] | [Value, Value, Value],
|
||||
tag?: Value
|
||||
): Value {
|
||||
@ -185,7 +187,7 @@ export const line: SketchLineHelper = {
|
||||
createCallback,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression>(
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
@ -197,12 +199,12 @@ export const line: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
@ -220,7 +222,11 @@ export const line: SketchLineHelper = {
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -238,22 +244,10 @@ export const line: SketchLineHelper = {
|
||||
createLiteral(roundOff(to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
if (
|
||||
callExpression.arguments?.[0].type === 'Literal' &&
|
||||
callExpression.arguments?.[0].value === 'default'
|
||||
) {
|
||||
callExpression.arguments[0] = toArrExp
|
||||
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
const toProp = callExpression.arguments?.[0].properties?.find(
|
||||
({ key }) => key.name === 'to'
|
||||
)
|
||||
if (
|
||||
toProp &&
|
||||
toProp.value.type === 'Literal' &&
|
||||
toProp.value.value === 'default'
|
||||
) {
|
||||
toProp.value = toArrExp
|
||||
}
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
} else {
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
||||
@ -301,7 +295,7 @@ export const xLineTo: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newX = createLiteral(roundOff(to[0], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
|
||||
@ -349,7 +343,7 @@ export const yLineTo: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newY = createLiteral(roundOff(to[1], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
|
||||
@ -399,7 +393,7 @@ export const xLine: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
|
||||
@ -443,7 +437,7 @@ export const yLine: SketchLineHelper = {
|
||||
pathToNode
|
||||
)
|
||||
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
|
||||
@ -546,7 +540,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
||||
const newLine = createCallback
|
||||
@ -619,7 +613,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
||||
@ -876,7 +870,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
|
||||
const intersectPath = sketchGroup.value.find(
|
||||
({ name }) => name === intersectTagName
|
||||
({ name }: Path) => name === intersectTagName
|
||||
)
|
||||
let offset = 0
|
||||
if (intersectPath) {
|
||||
@ -949,17 +943,29 @@ interface CreateLineFnCallArgs {
|
||||
programMemory: ProgramMemory
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
fnName: TooTip
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export function compareVec2Epsilon(
|
||||
vec1: [number, number],
|
||||
vec2: [number, number]
|
||||
) {
|
||||
const compareEpsilon = 0.015625 // or 2^-6
|
||||
const xDifference = Math.abs(vec1[0] - vec2[0])
|
||||
const yDifference = Math.abs(vec1[0] - vec2[0])
|
||||
return xDifference < compareEpsilon && yDifference < compareEpsilon
|
||||
}
|
||||
|
||||
export function addNewSketchLn({
|
||||
node: _node,
|
||||
programMemory: previousProgramMemory,
|
||||
to,
|
||||
fnName,
|
||||
pathToNode,
|
||||
}: Omit<CreateLineFnCallArgs, 'from'>): { modifiedAst: Program } {
|
||||
}: Omit<CreateLineFnCallArgs, 'from'>): {
|
||||
modifiedAst: Program
|
||||
} {
|
||||
const node = JSON.parse(JSON.stringify(_node))
|
||||
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
||||
if (!add || !updateArgs) throw new Error('not a sketch line helper')
|
||||
@ -968,62 +974,15 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const { node: pipeExp, shallowPath: pipePath } =
|
||||
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
|
||||
const maybeStartSketchAt = pipeExp.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'startSketchAt' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const maybeDefaultLine = pipeExp.body.findIndex(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'line' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const defaultLinePath: PathToNode = [
|
||||
...pipePath,
|
||||
['body', ''],
|
||||
[maybeDefaultLine, ''],
|
||||
]
|
||||
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
||||
PipeExpression | CallExpression
|
||||
>(node, pathToNode, 'PipeExpression')
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
if (maybeStartSketchAt) {
|
||||
const startSketchAt = maybeStartSketchAt as any
|
||||
startSketchAt.arguments[0] = createArrayExpression([
|
||||
createLiteral(to[0]),
|
||||
createLiteral(to[1]),
|
||||
])
|
||||
return {
|
||||
modifiedAst: node,
|
||||
}
|
||||
}
|
||||
if (maybeDefaultLine !== -1) {
|
||||
const defaultLine = getNodeFromPath<CallExpression>(
|
||||
node,
|
||||
defaultLinePath
|
||||
).node
|
||||
const { from } = getSketchSegmentFromSourceRange(sketch, [
|
||||
defaultLine.start,
|
||||
defaultLine.end,
|
||||
]).segment
|
||||
return updateArgs({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
pathToNode: defaultLinePath,
|
||||
to,
|
||||
from,
|
||||
})
|
||||
}
|
||||
|
||||
const last = sketch.value[sketch.value.length - 1]
|
||||
const last = sketch.value[sketch.value.length - 1] || sketch.start
|
||||
const from = last.to
|
||||
|
||||
return add({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -1034,6 +993,29 @@ export function addNewSketchLn({
|
||||
})
|
||||
}
|
||||
|
||||
export function addCloseToPipe({
|
||||
node,
|
||||
pathToNode,
|
||||
}: {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
pathToNode: PathToNode
|
||||
}) {
|
||||
const _node = { ...node }
|
||||
const closeExpression = createCallExpression('close', [
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
).node
|
||||
if (pipeExpression.type !== 'PipeExpression')
|
||||
throw new Error('not a pipe expression')
|
||||
pipeExpression.body = [...pipeExpression.body, closeExpression]
|
||||
return _node
|
||||
}
|
||||
|
||||
export function replaceSketchLine({
|
||||
node,
|
||||
programMemory,
|
||||
@ -1047,7 +1029,7 @@ export function replaceSketchLine({
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
sourceRange: SourceRange
|
||||
fnName: TooTip
|
||||
fnName: ToolTip
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
createCallback: TransformCallback
|
||||
@ -1089,10 +1071,11 @@ export function addTagForSketchOnFace(
|
||||
|
||||
function isAngleLiteral(lineArugement: Value): boolean {
|
||||
return lineArugement?.type === 'ArrayExpression'
|
||||
? lineArugement.elements[0].type === 'Literal'
|
||||
? isLiteralArrayOrStatic(lineArugement.elements[0])
|
||||
: lineArugement?.type === 'ObjectExpression'
|
||||
? lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
||||
.type === 'Literal'
|
||||
? isLiteralArrayOrStatic(
|
||||
lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
||||
)
|
||||
: false
|
||||
}
|
||||
|
||||
@ -1198,14 +1181,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
} {
|
||||
// used for lineTo, line
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg.type === 'Literal' && firstArg.value === 'default') {
|
||||
return {
|
||||
val:
|
||||
callExpression.callee.name === 'startSketchAt'
|
||||
? [createLiteral(0), createLiteral(0)]
|
||||
: [createLiteral(1), createLiteral(1)],
|
||||
}
|
||||
}
|
||||
if (firstArg.type === 'ArrayExpression') {
|
||||
return { val: [firstArg.elements[0], firstArg.elements[1]] }
|
||||
}
|
||||
@ -1215,8 +1190,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
if (to?.type === 'ArrayExpression') {
|
||||
const [x, y] = to.elements
|
||||
return { val: [x, y], tag }
|
||||
} else if (to?.type === 'Literal' && to.value === 'default') {
|
||||
return { val: [createLiteral(0), createLiteral(0)], tag }
|
||||
}
|
||||
}
|
||||
throw new Error('expected ArrayExpression or ObjectExpression')
|
||||
@ -1235,7 +1208,7 @@ function getFirstArgValuesForAngleFns(callExpression: CallExpression): {
|
||||
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
|
||||
const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value
|
||||
const secondArgName = ['angledLineToX', 'angledLineToY'].includes(
|
||||
callExpression?.callee?.name as TooTip
|
||||
callExpression?.callee?.name as ToolTip
|
||||
)
|
||||
? 'to'
|
||||
: 'length'
|
||||
|
@ -401,6 +401,11 @@ show(part001)`
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
[index, index]
|
||||
).segment
|
||||
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
|
||||
expect(segment).toEqual({
|
||||
to: [0, 0.04],
|
||||
from: [0, 0.04],
|
||||
name: '',
|
||||
type: 'base',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { TooTip, toolTips } from '../../useStore'
|
||||
import { ToolTip, toolTips } from '../../useStore'
|
||||
import {
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
CallExpression,
|
||||
} from '../abstractSyntaxTreeTypes'
|
||||
import { SketchGroup, SourceRange } from '../executor'
|
||||
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
|
||||
startSourceRange[1] >= rangeEnd &&
|
||||
sketchGroup.start
|
||||
)
|
||||
return { segment: sketchGroup.start, index: -1 }
|
||||
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
|
||||
|
||||
const lineIndex = sketchGroup.value.findIndex(
|
||||
({ __geoMeta: { sourceRange } }) =>
|
||||
({ __geoMeta: { sourceRange } }: Path) =>
|
||||
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
|
||||
)
|
||||
const line = sketchGroup.value[lineIndex]
|
||||
@ -67,7 +67,10 @@ export function isSketchVariablesLinked(
|
||||
return false
|
||||
const firstCallExp = // first in pipe expression or just the call expression
|
||||
init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression)
|
||||
if (!firstCallExp || !toolTips.includes(firstCallExp?.callee?.name as TooTip))
|
||||
if (
|
||||
!firstCallExp ||
|
||||
!toolTips.includes(firstCallExp?.callee?.name as ToolTip)
|
||||
)
|
||||
return false
|
||||
// convention for sketch fns is that the second argument is the sketch group
|
||||
const secondArg = firstCallExp?.arguments[1]
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
getConstraintLevelFromSourceRange,
|
||||
} from './sketchcombos'
|
||||
import { initPromise } from '../rust'
|
||||
import { Selections, TooTip } from '../../useStore'
|
||||
import { Selections, ToolTip } from '../../useStore'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
import { recast } from '../../lang/recast'
|
||||
|
||||
@ -68,7 +68,7 @@ function getConstraintTypeFromSourceHelper(
|
||||
Value,
|
||||
Value
|
||||
]
|
||||
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
|
||||
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
||||
return getConstraintType(args, fnName)
|
||||
}
|
||||
function getConstraintTypeFromSourceHelper2(
|
||||
@ -76,7 +76,7 @@ function getConstraintTypeFromSourceHelper2(
|
||||
): ReturnType<typeof getConstraintType> {
|
||||
const ast = parser_wasm(code)
|
||||
const arg = (ast.body[0] as any).expression.arguments[0] as Value
|
||||
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
|
||||
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
||||
return getConstraintType(arg, fnName)
|
||||
}
|
||||
|
||||
@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const expectModifiedScript = `const myVar = 3
|
||||
const myVar2 = 5
|
||||
const myVar3 = 6
|
||||
@ -195,7 +196,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|
||||
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('should transform the ast', async () => {
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('should transform horizontal lines the ast', async () => {
|
||||
const expectModifiedScript = `const myVar = 2
|
||||
const myVar2 = 12
|
||||
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
.split('\n')
|
||||
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
.split('\n')
|
||||
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> line([0.45, 1.46], %) // free
|
||||
|> line([myVar, 0.01], %) // xRelative
|
||||
|> line([0.7, myVar], %) // yRelative
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('testing for free to horizontal and vertical distance', async () => {
|
||||
const expectedHorizontalCode = await helperThing(
|
||||
inputScript,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TransformCallback } from './stdTypes'
|
||||
import { Selections, toolTips, TooTip, Selection } from '../../useStore'
|
||||
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
|
||||
import {
|
||||
CallExpression,
|
||||
Program,
|
||||
@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
||||
import { PathToNode, ProgramMemory } from '../executor'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
|
||||
|
||||
type LineInputsType =
|
||||
| 'xAbsolute'
|
||||
@ -53,7 +54,7 @@ export type ConstraintType =
|
||||
| 'setAngleBetween'
|
||||
|
||||
function createCallWrapper(
|
||||
a: TooTip,
|
||||
a: ToolTip,
|
||||
val: [Value, Value] | Value,
|
||||
tag?: Value,
|
||||
valueUsedInTransform?: number
|
||||
@ -100,7 +101,7 @@ function intersectCallWrapper({
|
||||
}
|
||||
|
||||
export type TransformInfo = {
|
||||
tooltip: TooTip
|
||||
tooltip: ToolTip
|
||||
createNode: (a: {
|
||||
varValA: Value // x / angle
|
||||
varValB: Value // y / length or x y for angledLineOfXlength etc
|
||||
@ -111,7 +112,7 @@ export type TransformInfo = {
|
||||
}
|
||||
|
||||
type TransformMap = {
|
||||
[key in TooTip]?: {
|
||||
[key in ToolTip]?: {
|
||||
[key in LineInputsType | 'free']?: {
|
||||
[key in ConstraintType]?: TransformInfo
|
||||
}
|
||||
@ -1094,12 +1095,12 @@ export function getRemoveConstraintsTransform(
|
||||
sketchFnExp: CallExpression,
|
||||
constraintType: ConstraintType
|
||||
): TransformInfo | false {
|
||||
let name = sketchFnExp.callee.name as TooTip
|
||||
let name = sketchFnExp.callee.name as ToolTip
|
||||
if (!toolTips.includes(name)) {
|
||||
return false
|
||||
}
|
||||
const xyLineMap: {
|
||||
[key in TooTip]?: TooTip
|
||||
[key in ToolTip]?: ToolTip
|
||||
} = {
|
||||
xLine: 'line',
|
||||
yLine: 'line',
|
||||
@ -1136,27 +1137,18 @@ export function getRemoveConstraintsTransform(
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' || b?.type !== 'Literal') {
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return transformInfo
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return transformInfo
|
||||
}
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isTwoValFree) {
|
||||
return false
|
||||
}
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isOneValFree) {
|
||||
return transformInfo
|
||||
}
|
||||
@ -1175,37 +1167,24 @@ function getTransformMapPath(
|
||||
constraintType: ConstraintType
|
||||
):
|
||||
| {
|
||||
toolTip: TooTip
|
||||
toolTip: ToolTip
|
||||
lineInputType: LineInputsType | 'free'
|
||||
constraintType: ConstraintType
|
||||
}
|
||||
| false {
|
||||
const name = sketchFnExp.callee.name as TooTip
|
||||
const name = sketchFnExp.callee.name as ToolTip
|
||||
if (!toolTips.includes(name)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') {
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
if (isTwoValFree || isOneValFree) {
|
||||
if (isLiteralArrayOrStatic(firstArg.val)) {
|
||||
const info = transformMap?.[name]?.free?.[constraintType]
|
||||
if (info)
|
||||
return {
|
||||
@ -1246,7 +1225,7 @@ export function getTransformInfo(
|
||||
|
||||
export function getConstraintType(
|
||||
val: Value | [Value, Value] | [Value, Value, Value],
|
||||
fnName: TooTip
|
||||
fnName: ToolTip
|
||||
): LineInputsType | null {
|
||||
// this function assumes that for two val sketch functions that one arg is locked down not both
|
||||
// and for one val sketch functions that the arg is NOT locked down
|
||||
@ -1259,7 +1238,7 @@ export function getConstraintType(
|
||||
if (fnName === 'xLineTo') return 'yAbsolute'
|
||||
if (fnName === 'yLineTo') return 'xAbsolute'
|
||||
} else {
|
||||
const isFirstArgLockedDown = val?.[0]?.type !== 'Literal'
|
||||
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
|
||||
if (fnName === 'line')
|
||||
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
|
||||
if (fnName === 'lineTo')
|
||||
@ -1426,7 +1405,6 @@ export function transformAstSketchLines({
|
||||
selectionRanges.codeBasedSelections.forEach(({ range }, index) => {
|
||||
const callBack = transformInfos?.[index].createNode
|
||||
const transformTo = transformInfos?.[index].tooltip
|
||||
console.log('transformTo', transformInfos)
|
||||
if (!callBack || !transformTo) throw new Error('no callback helper')
|
||||
|
||||
const getNode = getNodeFromPathCurry(
|
||||
@ -1453,7 +1431,7 @@ export function transformAstSketchLines({
|
||||
|
||||
const varName = varDec.id.name
|
||||
const sketchGroup = programMemory.root?.[varName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||
throw new Error('not a sketch group')
|
||||
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
||||
const referencedSegment = referencedSegmentRange
|
||||
@ -1467,7 +1445,7 @@ export function transformAstSketchLines({
|
||||
programMemory,
|
||||
sourceRange: range,
|
||||
referencedSegment,
|
||||
fnName: transformTo || (callExp.callee.name as TooTip),
|
||||
fnName: transformTo || (callExp.callee.name as ToolTip),
|
||||
to,
|
||||
from,
|
||||
createCallback: callBack({
|
||||
@ -1533,29 +1511,52 @@ export function getConstraintLevelFromSourceRange(
|
||||
getNodePathFromSourceRange(ast, cursorRange),
|
||||
'CallExpression'
|
||||
)
|
||||
const name = sketchFnExp?.callee?.name as TooTip
|
||||
const name = sketchFnExp?.callee?.name as ToolTip
|
||||
if (!toolTips.includes(name)) return 'free'
|
||||
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
|
||||
// check if the function is fully constrained
|
||||
if (Array.isArray(firstArg.val)) {
|
||||
const [a, b] = firstArg.val
|
||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') return 'full'
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') return 'full'
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
return 'full'
|
||||
}
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) &&
|
||||
firstArg.val?.[0]?.type === 'Literal' &&
|
||||
firstArg.val?.[1]?.type === 'Literal'
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
|
||||
if (isTwoValFree) return 'free'
|
||||
if (isOneValFree) return 'partial'
|
||||
|
||||
return 'partial'
|
||||
}
|
||||
|
||||
export function isLiteralArrayOrStatic(
|
||||
val: Value | [Value, Value] | [Value, Value, Value] | undefined
|
||||
): boolean {
|
||||
if (!val) return false
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
const [a, b] = val
|
||||
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
|
||||
}
|
||||
return (
|
||||
val.type === 'Literal' ||
|
||||
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
|
||||
)
|
||||
}
|
||||
|
||||
export function isNotLiteralArrayOrStatic(
|
||||
val: Value | [Value, Value] | [Value, Value, Value]
|
||||
): boolean {
|
||||
if (Array.isArray(val)) {
|
||||
const [a, b] = val
|
||||
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
|
||||
}
|
||||
return (
|
||||
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
|
||||
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ProgramMemory, Path, SourceRange } from '../executor'
|
||||
import { Program, Value } from '../abstractSyntaxTreeTypes'
|
||||
import { TooTip } from '../../useStore'
|
||||
import { ToolTip } from '../../useStore'
|
||||
import { PathToNode } from '../executor'
|
||||
import { EngineCommandManager } from './engineConnection'
|
||||
|
||||
@ -45,7 +45,7 @@ export type TransformCallback = (
|
||||
}
|
||||
|
||||
export type SketchCallTransfromMap = {
|
||||
[key in TooTip]: TransformCallback
|
||||
[key in ToolTip]: TransformCallback
|
||||
}
|
||||
|
||||
export interface SketchLineHelper {
|
||||
|
@ -131,10 +131,12 @@ const yi=45`
|
||||
})
|
||||
it('test negative and decimal numbers', () => {
|
||||
expect(stringSummaryLexer('-1')).toEqual([
|
||||
"number '-1' from 0 to 2",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1' from 1 to 2",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5')).toEqual([
|
||||
"number '-1.5' from 0 to 4",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
])
|
||||
expect(stringSummaryLexer('1.5')).toEqual([
|
||||
"number '1.5' from 0 to 3",
|
||||
@ -158,10 +160,12 @@ const yi=45`
|
||||
"whitespace ' ' from 3 to 4",
|
||||
"operator '+' from 4 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"number '-2.5' from 6 to 10",
|
||||
"operator '-' from 6 to 7",
|
||||
"number '2.5' from 7 to 10",
|
||||
])
|
||||
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
|
||||
"number '-1.5' from 0 to 4",
|
||||
"operator '-' from 0 to 1",
|
||||
"number '1.5' from 1 to 4",
|
||||
"whitespace ' ' from 4 to 5",
|
||||
"operator '+' from 5 to 6",
|
||||
"whitespace ' ' from 6 to 7",
|
||||
|
156
src/lib/cameraControls.ts
Normal file
156
src/lib/cameraControls.ts
Normal file
@ -0,0 +1,156 @@
|
||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||
|
||||
export type CameraSystem =
|
||||
| 'KittyCAD'
|
||||
| 'OnShape'
|
||||
| 'Trackpad Friendly'
|
||||
| 'Solidworks'
|
||||
| 'NX'
|
||||
| 'Creo'
|
||||
| 'AutoCAD'
|
||||
|
||||
export const cameraSystems: CameraSystem[] = [
|
||||
'KittyCAD',
|
||||
'OnShape',
|
||||
'Trackpad Friendly',
|
||||
'Solidworks',
|
||||
'NX',
|
||||
'Creo',
|
||||
'AutoCAD',
|
||||
]
|
||||
|
||||
interface MouseGuardHandler {
|
||||
description: string
|
||||
callback: (e: React.MouseEvent) => boolean
|
||||
lenientDragStartButton?: number
|
||||
}
|
||||
|
||||
interface MouseGuardZoomHandler {
|
||||
description: string
|
||||
dragCallback: (e: React.MouseEvent) => boolean
|
||||
scrollCallback: (e: React.MouseEvent) => boolean
|
||||
lenientDragStartButton?: number
|
||||
}
|
||||
|
||||
interface MouseGuard {
|
||||
pan: MouseGuardHandler
|
||||
zoom: MouseGuardZoomHandler
|
||||
rotate: MouseGuardHandler
|
||||
}
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
KittyCAD: {
|
||||
pan: {
|
||||
description: 'Right click + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 1 && noModifiersPressed(e)) ||
|
||||
(e.button === 2 && e.shiftKey),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
OnShape: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 2 && e.ctrlKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
dragCallback: () => false,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
'Trackpad Friendly': {
|
||||
pan: {
|
||||
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
||||
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Left click + Alt + drag',
|
||||
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
|
||||
lenientDragStartButton: 0,
|
||||
},
|
||||
},
|
||||
Solidworks: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag',
|
||||
callback: (e) => e.button === 2 && e.ctrlKey,
|
||||
lenientDragStartButton: 2,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.shiftKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
NX: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
Creo: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
AutoCAD: {
|
||||
pan: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
dragCallback: () => false,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
},
|
||||
}
|
21
src/lib/exampleKcl.ts
Normal file
21
src/lib/exampleKcl.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const bracket = `// Material: 6061-T6 Aluminum
|
||||
const sigmaAllow = 35000 // psi
|
||||
const width = 9 // inch
|
||||
const p = 150 // Force on shelf - lbs
|
||||
const distance = 6 // inches
|
||||
const FOS = 2
|
||||
|
||||
const leg1 = 5 // inches
|
||||
const leg2 = 8 // inches
|
||||
const thickness = sqrt(distance * p * FOS * 6 / sigmaAllow / width) // inches
|
||||
const bracket = startSketchAt([0, 0])
|
||||
|> line([0, leg1], %)
|
||||
|> line([leg2, 0], %)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([-leg2 + thickness, 0], %)
|
||||
|> line([0, -leg1 + thickness], %)
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
|
||||
show(bracket)
|
||||
`
|
@ -5,7 +5,7 @@ import {
|
||||
readDir,
|
||||
writeTextFile,
|
||||
} from '@tauri-apps/api/fs'
|
||||
import { documentDir } from '@tauri-apps/api/path'
|
||||
import { documentDir, homeDir } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
|
||||
return directory
|
||||
}
|
||||
|
||||
const docDirectory = await documentDir()
|
||||
let docDirectory: string
|
||||
try {
|
||||
docDirectory = await documentDir()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
docDirectory = await homeDir() // seems to work better on Linux
|
||||
}
|
||||
|
||||
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
||||
|
||||
|
@ -49,7 +49,7 @@ class MockEngineCommandManager {
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const mockEngineCommandManager = new MockEngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
@ -64,7 +64,7 @@ export async function enginelessExecutor(
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const engineCommandManager = new EngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
@ -75,6 +75,6 @@ export async function executor(
|
||||
await engineCommandManager.waitForReady
|
||||
engineCommandManager.startNewSession()
|
||||
const programMemory = await _executor(ast, pm, engineCommandManager)
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
await engineCommandManager.waitForAllCommands(ast, programMemory)
|
||||
return programMemory
|
||||
}
|
||||
|
@ -56,6 +56,27 @@ export function throttle<T>(
|
||||
return throttled
|
||||
}
|
||||
|
||||
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||
export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
||||
let timeout: ReturnType<typeof setTimeout> | null
|
||||
let latestArgs: T
|
||||
|
||||
function later() {
|
||||
timeout = null
|
||||
func(latestArgs)
|
||||
}
|
||||
|
||||
function deferred(args: T) {
|
||||
latestArgs = args
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
|
||||
return deferred
|
||||
}
|
||||
|
||||
export function getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
|
@ -2,6 +2,9 @@ import { createMachine, assign } from 'xstate'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import withBaseURL from '../lib/withBaseURL'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
|
||||
const SKIP_AUTH =
|
||||
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
||||
@ -115,19 +118,27 @@ async function getUser(context: UserContext) {
|
||||
const headers: { [key: string]: string } = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (!context.token && '__TAURI__' in window) throw 'not log in'
|
||||
|
||||
if (!context.token && isTauri()) throw new Error('No token found')
|
||||
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
||||
if (SKIP_AUTH) return LOCAL_USER
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
|
||||
const userPromise = !isTauri()
|
||||
? fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
})
|
||||
const user = await response.json()
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.error('error from Browser getUser', err))
|
||||
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
|
||||
token: context.token,
|
||||
hostname: VITE_KC_API_BASE_URL,
|
||||
}).catch((err) => console.error('error from Tauri getUser', err))
|
||||
|
||||
const user = await userPromise
|
||||
|
||||
if ('error_code' in user) throw new Error(user.message)
|
||||
|
||||
return user
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,54 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { BaseUnit, baseUnitsUnion } from '../useStore'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
||||
|
||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||
|
||||
export enum UnitSystem {
|
||||
Imperial = 'imperial',
|
||||
Metric = 'metric',
|
||||
}
|
||||
|
||||
export const baseUnits = {
|
||||
imperial: ['in', 'ft'],
|
||||
metric: ['mm', 'cm', 'm'],
|
||||
} as const
|
||||
|
||||
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
|
||||
|
||||
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
|
||||
|
||||
export type Toggle = 'On' | 'Off'
|
||||
|
||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||
|
||||
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
'Set Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
'Set Base Unit': {
|
||||
displayValue: (args: string[]) => 'Set your default base unit',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
name: 'baseUnit',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
options: Object.values(Themes).map((v) => ({ name: v })) as {
|
||||
name: string
|
||||
}[],
|
||||
defaultValue: 'baseUnit',
|
||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Camera Controls': {
|
||||
displayValue: (args: string[]) => 'Set your camera controls',
|
||||
args: [
|
||||
{
|
||||
name: 'cameraControls',
|
||||
type: 'select',
|
||||
defaultValue: 'cameraControls',
|
||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Default Directory': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
displayValue: (args: string[]) => 'Set a new default project name',
|
||||
hide: 'web',
|
||||
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Default Directory': {
|
||||
'Set Onboarding Status': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Text Wrapping': {
|
||||
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
||||
args: [
|
||||
{
|
||||
name: 'textWrapping',
|
||||
type: 'select',
|
||||
defaultValue: 'textWrapping',
|
||||
options: [{ name: 'On' }, { name: 'Off' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
options: Object.values(Themes).map((v): { name: string } => ({
|
||||
name: v,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Unit System': {
|
||||
displayValue: (args: string[]) => 'Set your default unit system',
|
||||
args: [
|
||||
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Base Unit': {
|
||||
displayValue: (args: string[]) => 'Set your default base unit',
|
||||
args: [
|
||||
{
|
||||
name: 'baseUnit',
|
||||
type: 'select',
|
||||
defaultValue: 'baseUnit',
|
||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Onboarding Status': {
|
||||
hide: 'both',
|
||||
},
|
||||
}
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {
|
||||
theme: Themes.System,
|
||||
defaultProjectName: '',
|
||||
unitSystem: UnitSystem.Imperial,
|
||||
baseUnit: 'in' as BaseUnit,
|
||||
cameraControls: 'KittyCAD' as CameraSystem,
|
||||
defaultDirectory: '',
|
||||
showDebugPanel: false,
|
||||
defaultProjectName: DEFAULT_PROJECT_NAME,
|
||||
onboardingStatus: '',
|
||||
showDebugPanel: false,
|
||||
textWrapping: 'On' as Toggle,
|
||||
theme: Themes.System,
|
||||
unitSystem: UnitSystem.Imperial,
|
||||
},
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
entry: ['setThemeClass'],
|
||||
on: {
|
||||
'Set Theme': {
|
||||
'Set Base Unit': {
|
||||
actions: [
|
||||
assign({
|
||||
theme: (_, event) => event.data.theme,
|
||||
}),
|
||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
'Set Camera Controls': {
|
||||
actions: [
|
||||
assign({
|
||||
defaultProjectName: (_, event) => event.data.defaultProjectName,
|
||||
cameraControls: (_, event) => event.data.cameraControls,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Unit System': {
|
||||
'Set Default Project Name': {
|
||||
actions: [
|
||||
assign({
|
||||
unitSystem: (_, event) => event.data.unitSystem,
|
||||
baseUnit: (_, event) =>
|
||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||
defaultProjectName: (_, event) =>
|
||||
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Base Unit': {
|
||||
'Set Onboarding Status': {
|
||||
actions: [
|
||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||
assign({
|
||||
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||
}),
|
||||
'persistSettings',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Text Wrapping': {
|
||||
actions: [
|
||||
assign({
|
||||
textWrapping: (_, event) => event.data.textWrapping,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Theme': {
|
||||
actions: [
|
||||
assign({
|
||||
theme: (_, event) => event.data.theme,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Unit System': {
|
||||
actions: [
|
||||
assign({
|
||||
unitSystem: (_, event) => event.data.unitSystem,
|
||||
baseUnit: (_, event) =>
|
||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
],
|
||||
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Onboarding Status': {
|
||||
actions: [
|
||||
assign({
|
||||
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||
}),
|
||||
'persistSettings',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||
schema: {
|
||||
events: {} as
|
||||
| { type: 'Set Theme'; data: { theme: Themes } }
|
||||
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||
| {
|
||||
type: 'Set Camera Controls'
|
||||
data: { cameraControls: CameraSystem }
|
||||
}
|
||||
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||
| {
|
||||
type: 'Set Default Project Name'
|
||||
data: { defaultProjectName: string }
|
||||
}
|
||||
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
|
||||
| { type: 'Set Theme'; data: { theme: Themes } }
|
||||
| {
|
||||
type: 'Set Unit System'
|
||||
data: { unitSystem: UnitSystem }
|
||||
}
|
||||
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||
| { type: 'Toggle Debug Panel' },
|
||||
},
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user