Compare commits
132 Commits
paultag/ad
...
v0.7.0
Author | SHA1 | Date | |
---|---|---|---|
66ba60dc8e | |||
8fcc8cdd17 | |||
bba9bdc563 | |||
760a180f56 | |||
0eeff8cb45 | |||
3c76721159 | |||
6ac79ae645 | |||
90d7c33c92 | |||
e02bc76bdb | |||
0466f04d82 | |||
f8ed830b60 | |||
b7ca91bf6d | |||
2261f92b0b | |||
bbe9e621b1 | |||
bf087d760b | |||
a4353c63fd | |||
c438d11c3d | |||
43284e33c8 | |||
77dce7f0dd | |||
d559862051 | |||
7382ed87ba | |||
3324ed31de | |||
ba9dbc2205 | |||
b0028d4874 | |||
9e6be9651c | |||
b145ab0106 | |||
84e0fbb70f | |||
990605bbea | |||
d075c4ad13 | |||
a3f41f5519 | |||
cb173e2850 | |||
87cd3b67f4 | |||
fe3ee3806e | |||
c9ed6c724c | |||
a5fa259d55 | |||
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
8071eb6f8a | |||
11f789e980 | |||
3f82522fe9 | |||
c5cb0e2fd4 | |||
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c | |||
3da6fc3b7e | |||
34dd15ead7 | |||
b3d441e9d6 | |||
4b3dc3756c | |||
10027b98b5 | |||
da17dad63b | |||
fba6c422a8 | |||
0b4b93932d | |||
f42900ec46 | |||
eeca624ba6 | |||
84d08bad16 | |||
1181f33e9d | |||
797e200d08 | |||
d2f231066b | |||
86d40c964f | |||
2604449239 | |||
e992a96d3b | |||
22c4406105 | |||
ad3f0fda6a | |||
cccedceea0 | |||
ed68a34560 | |||
00ee913e3f | |||
46cc67e2db | |||
ff1be34f54 | |||
848bf61277 | |||
043333d3bc | |||
19d90b8081 | |||
4837c52908 | |||
afcf820bdd | |||
18959510f8 | |||
798cbe968a | |||
9cbc088ba3 | |||
2693a5609b | |||
3507da7b39 | |||
56cfb6d1f0 | |||
2b974ef1de | |||
253f1992fd | |||
76d3794b45 | |||
e52c8c9db6 | |||
eb48d51309 | |||
f3274e03ff | |||
46937199a3 | |||
e2a4798c2f | |||
659e6d5b45 | |||
1fbd0ad675 | |||
743ea1af4d | |||
2b1a556b81 | |||
853389ba22 | |||
023af60781 | |||
18db6f2dc1 | |||
4afec15323 | |||
152108f7a5 | |||
32d928ae0c | |||
6f0fae625f | |||
9bc47cf14a | |||
eea47aae1e | |||
25b9b4cf98 | |||
0f3f0b3b68 | |||
33eb6126d4 | |||
dccb83f614 | |||
b56a3398ad | |||
11658e2ff5 | |||
de255acc59 | |||
d33ddb2f1b | |||
a0730ded4e | |||
afd2b507ef | |||
8983a8231b | |||
9dd708db5d | |||
c25dd1800c | |||
e56e7ba0fa | |||
dbe4e7faa6 |
@ -1,3 +1,6 @@
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
@ -1,3 +1,6 @@
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/wasm-lib/*
|
2
.github/workflows/cargo-build.yml
vendored
2
.github/workflows/cargo-build.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
13
.github/workflows/cargo-clippy.yml
vendored
13
.github/workflows/cargo-clippy.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install latest rust
|
||||
@ -40,6 +40,17 @@ 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 }}"
|
||||
|
17
.github/workflows/cargo-test.yml
vendored
17
.github/workflows/cargo-test.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install latest rust
|
||||
@ -41,8 +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 llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast
|
||||
cargo test --all
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
||||
|
171
.github/workflows/ci.yml
vendored
171
.github/workflows/ci.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -13,17 +13,31 @@ jobs:
|
||||
check-format:
|
||||
runs-on: 'ubuntu-20.04'
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
|
||||
- run: yarn fmt-check
|
||||
|
||||
check-types:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./src/wasm-lib"
|
||||
|
||||
- run: yarn build:wasm
|
||||
- run: yarn tsc
|
||||
|
||||
|
||||
build-test-web:
|
||||
runs-on: ubuntu-20.04
|
||||
@ -36,12 +50,15 @@ jobs:
|
||||
- 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,14 +66,12 @@ 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:
|
||||
@ -87,6 +102,10 @@ jobs:
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./src/wasm-lib"
|
||||
|
||||
- name: wasm prep
|
||||
shell: bash
|
||||
run: |
|
||||
@ -110,19 +129,26 @@ jobs:
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
- name: install apple silicon target mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
- name: Build the app for the current platform (no upload)
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: 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
|
||||
sign-windows-msi:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'release'
|
||||
needs: [build-test-web, build-apps]
|
||||
env:
|
||||
@ -131,29 +157,89 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Setup Certificate
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
id: variables
|
||||
run: |
|
||||
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
|
||||
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
|
||||
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
|
||||
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
|
||||
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
|
||||
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Setup SSM KSP on windows latest
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
smksp_registrar.exe list
|
||||
smctl.exe keypair ls
|
||||
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||
smksp_cert_sync.exe
|
||||
shell: cmd
|
||||
|
||||
- name: Signing using Signtool
|
||||
run: |
|
||||
signtool.exe sign /sha1 ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "artifact\msi\*.msi"
|
||||
signtool.exe verify /v /pa "artifact\msi\*.msi"
|
||||
|
||||
# TODO: for the updater, investigate if we need to also replace what's in the .zip, and what to do about the .sig file
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: artifact/*
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event_name == 'release'
|
||||
needs: [build-test-web, build-apps, sign-windows-msi]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
PUB_DATE: ${{ github.event.release.created_at }}
|
||||
NOTES: ${{ github.event.release.body }}
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact
|
||||
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-modeling-app.app.tar.gz" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
||||
--arg linux_sig "$LINUX_SIG" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
||||
--arg 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-modeling-app_${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 +252,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:
|
||||
@ -175,17 +289,28 @@ jobs:
|
||||
uses: google-github-actions/setup-gcloud@v1.1.1
|
||||
with:
|
||||
project_id: kittycadapi
|
||||
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/kittycad-modeling-app*'
|
||||
glob: '*/*itty*'
|
||||
parent: false
|
||||
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||
|
||||
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: last_update.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload release files to Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifact/*/*itty*
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,5 +25,6 @@ yarn-error.log*
|
||||
# rust
|
||||
src/wasm-lib/target
|
||||
src/wasm-lib/bindings
|
||||
src/wasm-lib/kcl/bindings
|
||||
public/wasm_lib_bg.wasm
|
||||
src/wasm-lib/lcov.info
|
||||
|
@ -5,3 +5,5 @@ coverage
|
||||
# Ignore Rust projects:
|
||||
*.rs
|
||||
target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
|
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).
|
||||
|
19534
docs/kcl/std.json
Normal file
19534
docs/kcl/std.json
Normal file
File diff suppressed because it is too large
Load Diff
3556
docs/kcl/std.md
Normal file
3556
docs/kcl/std.md
Normal file
File diff suppressed because it is too large
Load Diff
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).
|
@ -11,6 +11,7 @@
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
|
||||
<title>KittyCAD Modeling App</title>
|
||||
</head>
|
||||
<body class="body-bg">
|
||||
|
27
package.json
27
package.json
@ -1,28 +1,36 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.0.4",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@kittycad/lib": "^0.0.29",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.37",
|
||||
"@lezer/javascript": "^1.4.7",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@sentry/react": "^7.65.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.15.1",
|
||||
"@uiw/react-codemirror": "^4.21.13",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"formik": "^2.4.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"http-server": "^14.1.1",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -40,6 +48,8 @@
|
||||
"typescript": "^4.4.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vitest": "^0.34.1",
|
||||
"vscode-jsonrpc": "^8.1.0",
|
||||
"vscode-languageserver-protocol": "^3.17.3",
|
||||
"wasm-pack": "^0.12.1",
|
||||
"web-vitals": "^2.1.0",
|
||||
"ws": "^8.13.0",
|
||||
@ -54,15 +64,15 @@
|
||||
"build:both:local": "yarn build:wasm && vite build",
|
||||
"test": "vitest --mode development",
|
||||
"test:nowatch": "vitest run --mode development",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests)",
|
||||
"test:cov": "vitest run --coverage --mode development",
|
||||
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/bindings",
|
||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
||||
},
|
||||
@ -89,6 +99,7 @@
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
|
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 |
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)
|
||||
|
320
src-tauri/Cargo.lock
generated
320
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.71"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -83,7 +83,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs-extra",
|
||||
"tokio",
|
||||
"toml 0.6.0",
|
||||
"toml 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -110,22 +110,6 @@ dependencies = [
|
||||
"system-deps 6.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@ -234,6 +218,9 @@ name = "bytes"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
@ -266,7 +253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -325,8 +312,10 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -495,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -535,7 +524,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -546,7 +535,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -628,7 +617,7 @@ checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"rustc_version",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
"vswhom",
|
||||
"winreg 0.11.0",
|
||||
]
|
||||
@ -648,6 +637,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
@ -799,7 +794,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -823,6 +818,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
@ -1150,7 +1146,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@ -1163,6 +1159,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
@ -1270,7 +1272,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa 1.0.6",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.4.9",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@ -1291,6 +1293,19 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.56"
|
||||
@ -1378,7 +1393,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -1628,6 +1654,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.6.2"
|
||||
@ -1716,15 +1748,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom8"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -1807,9 +1830,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "4.4.1"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a6e2a2b13a56ebeabba9142f911745be6456163fd6c3d361274ebcd891a80c"
|
||||
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"chrono",
|
||||
@ -1912,7 +1935,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2099,9 +2122,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@ -2122,7 +2145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@ -2161,7 +2184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.8",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2196,9 +2219,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -2388,10 +2411,12 @@ dependencies = [
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@ -2401,11 +2426,14 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg 0.10.1",
|
||||
@ -2502,9 +2530,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.1"
|
||||
version = "0.101.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
|
||||
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
@ -2622,29 +2650,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.188"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
|
||||
dependencies = [
|
||||
"itoa 1.0.6",
|
||||
"ryu",
|
||||
@ -2669,14 +2697,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
|
||||
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -2695,14 +2723,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "2.3.3"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
|
||||
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"base64 0.21.2",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@ -2711,14 +2740,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "2.3.3"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
|
||||
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2810,6 +2839,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soup2"
|
||||
version = "0.2.1"
|
||||
@ -2904,9 +2943,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2935,7 +2974,7 @@ dependencies = [
|
||||
"cfg-expr 0.15.2",
|
||||
"heck 0.4.1",
|
||||
"pkg-config",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
@ -3016,12 +3055,13 @@ checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.3.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
|
||||
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
"base64 0.21.2",
|
||||
"bytes",
|
||||
"cocoa",
|
||||
"dirs-next",
|
||||
"embed_plist",
|
||||
@ -3034,6 +3074,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"http",
|
||||
"ignore",
|
||||
"minisign-verify",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"open",
|
||||
@ -3041,6 +3082,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"semver",
|
||||
"serde",
|
||||
@ -3055,19 +3097,21 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.39.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389"
|
||||
checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@ -3078,14 +3122,13 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde"
|
||||
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"brotli",
|
||||
@ -3109,9 +3152,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46"
|
||||
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@ -3124,7 +3167,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7e58dc8502f654b99d51c087421f84ccc0e03119"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5b814f56e6368fdec46c4ddb04a07e0923ff995a"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -3135,9 +3178,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422"
|
||||
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@ -3156,9 +3199,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1"
|
||||
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@ -3176,12 +3219,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
|
||||
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
"dunce",
|
||||
"glob",
|
||||
"heck 0.4.1",
|
||||
"html5ever",
|
||||
@ -3209,7 +3253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
|
||||
dependencies = [
|
||||
"embed-resource",
|
||||
"toml 0.7.3",
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3259,7 +3303,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3301,21 +3345,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.29.1"
|
||||
version = "1.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.5.4",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
@ -3351,69 +3404,60 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.6.0"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.5.1",
|
||||
"toml_edit 0.18.1",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
|
||||
checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.2",
|
||||
"toml_edit 0.19.8",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.5.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.18.1"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"nom8",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.5.1",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.8"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
|
||||
checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.2",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@ -3443,7 +3487,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3668,7 +3712,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -3702,7 +3746,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
"syn 2.0.33",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -3713,6 +3757,19 @@ version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.63"
|
||||
@ -3772,9 +3829,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
@ -4135,9 +4192,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.1"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
|
||||
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -4228,3 +4285,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
@ -12,16 +12,16 @@ rust-version = "1.60"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.3.0", features = [] }
|
||||
tauri-build = { version = "1.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
oauth2 = "4.4.1"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "1.3.0", features = [ "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 = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater"] }
|
||||
tokio = { version = "1.32.0", features = ["time"] }
|
||||
toml = "0.8.0"
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
|
||||
[features]
|
||||
|
@ -7,8 +7,8 @@
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling-app",
|
||||
"version": "0.0.4"
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.7.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
7
src-tauri/tauri.macos.conf.json
Normal file
7
src-tauri/tauri.macos.conf.json
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"package": {
|
||||
"productName": "KittyCAD Modeling"
|
||||
}
|
||||
}
|
7
src-tauri/tauri.windows.conf.json
Normal file
7
src-tauri/tauri.windows.conf.json
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"package": {
|
||||
"productName": "KittyCAD Modeling"
|
||||
}
|
||||
}
|
@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'
|
||||
import { App } from './App'
|
||||
import { describe, test, vi } from 'vitest'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||
import CommandBarProvider from 'components/CommandBar'
|
||||
|
||||
let listener: ((rect: any) => void) | undefined = undefined
|
||||
;(global as any).ResizeObserver = class ResizeObserver {
|
||||
@ -43,7 +44,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
459
src/App.tsx
459
src/App.tsx
@ -1,37 +1,17 @@
|
||||
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 { langs } from '@uiw/codemirror-extensions-langs'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { ViewUpdate } from '@codemirror/view'
|
||||
import {
|
||||
lineHighlightField,
|
||||
addLineHighlight,
|
||||
} from './editor/highlightextension'
|
||||
import { PaneType, Selections, Themes, useStore } from './useStore'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
import { Logs, KCLErrors } from './components/Logs'
|
||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||
import { MemoryPanel } from './components/MemoryPanel'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
import {
|
||||
EngineCommand,
|
||||
EngineCommandManager,
|
||||
} from './lang/std/engineConnection'
|
||||
import { isOverlap, throttle } from './lib/utils'
|
||||
import { EngineCommand } from './lang/std/engineConnection'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
faCode,
|
||||
@ -39,101 +19,53 @@ import {
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TEST } from './env'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { getSystemTheme } from './lib/getSystemTheme'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
import { useLoaderData, 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 { useAuthMachine } from './hooks/useAuthMachine'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useCodeEval } from 'hooks/useCodeEval'
|
||||
|
||||
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,
|
||||
isMouseDownInStream,
|
||||
cmdId,
|
||||
setCmdId,
|
||||
formatCode,
|
||||
debugPanel,
|
||||
theme,
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
onboardingStatus,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
setStreamDimensions,
|
||||
streamDimensions,
|
||||
guiMode,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
setEditorView: s.setEditorView,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setGuiMode: s.setGuiMode,
|
||||
addLog: s.addLog,
|
||||
code: s.code,
|
||||
guiMode: s.guiMode,
|
||||
setCode: s.setCode,
|
||||
setAst: s.setAst,
|
||||
setError: s.setError,
|
||||
setProgramMemory: s.setProgramMemory,
|
||||
resetLogs: s.resetLogs,
|
||||
resetKCLErrors: s.resetKCLErrors,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
isShiftDown: s.isShiftDown,
|
||||
setCursor: s.setCursor,
|
||||
setCursor2: s.setCursor2,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
setMediaStream: s.setMediaStream,
|
||||
isStreamReady: s.isStreamReady,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
cmdId: s.cmdId,
|
||||
setCmdId: s.setCmdId,
|
||||
formatCode: s.formatCode,
|
||||
debugPanel: s.debugPanel,
|
||||
addKCLError: s.addKCLError,
|
||||
theme: s.theme,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
onboardingStatus: s.onboardingStatus,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||
|
||||
const {
|
||||
auth: {
|
||||
context: { token },
|
||||
},
|
||||
settings: {
|
||||
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
|
||||
@ -152,7 +84,7 @@ export function App() {
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
|
||||
const paneOpacity =
|
||||
onboardingStatus === 'camera'
|
||||
onboardingStatus === onboardingPaths.CAMERA
|
||||
? 'opacity-20'
|
||||
: didDragInStream
|
||||
? 'opacity-40'
|
||||
@ -172,236 +104,74 @@ 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 pixelDensity = window.devicePixelRatio
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
const streamHeight = streamRef?.current?.offsetHeight
|
||||
|
||||
const width = streamWidth ? streamWidth * pixelDensity : 0
|
||||
const quadWidth = Math.round(width / 4) * 4
|
||||
const height = streamHeight ? streamHeight * pixelDensity : 0
|
||||
const quadHeight = Math.round(height / 4) * 4
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
streamHeight: quadHeight,
|
||||
})
|
||||
if (!width || !height) return
|
||||
const eng = new EngineCommandManager({
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
token,
|
||||
})
|
||||
setEngineCommandManager(eng)
|
||||
return () => {
|
||||
eng?.tearDown()
|
||||
}
|
||||
}, [quadWidth, quadHeight])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStreamReady) return
|
||||
const asyncWrap = async () => {
|
||||
try {
|
||||
if (!code) {
|
||||
setAst(null)
|
||||
return
|
||||
}
|
||||
const _ast = await asyncParser(code)
|
||||
setAst(_ast)
|
||||
resetLogs()
|
||||
resetKCLErrors()
|
||||
if (engineCommandManager) {
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
}
|
||||
if (!engineCommandManager) return
|
||||
const programMemory = await _executor(
|
||||
_ast,
|
||||
{
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
addLog(a)
|
||||
},
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [],
|
||||
sourceRange: [0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
_0: {
|
||||
type: 'userVal',
|
||||
value: 0,
|
||||
__meta: [],
|
||||
},
|
||||
_90: {
|
||||
type: 'userVal',
|
||||
value: 90,
|
||||
__meta: [],
|
||||
},
|
||||
_180: {
|
||||
type: 'userVal',
|
||||
value: 180,
|
||||
__meta: [],
|
||||
},
|
||||
_270: {
|
||||
type: 'userVal',
|
||||
value: 270,
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
},
|
||||
engineCommandManager,
|
||||
{ bodyType: 'root' },
|
||||
[]
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
|
||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||
engineCommandManager.onHover((id) => {
|
||||
if (!id) {
|
||||
setHighlightRange([0, 0])
|
||||
} else {
|
||||
const sourceRange = sourceRangeMap[id]
|
||||
setHighlightRange(sourceRange)
|
||||
}
|
||||
})
|
||||
engineCommandManager.onClick((selections) => {
|
||||
if (!selections) {
|
||||
setCursor2()
|
||||
return
|
||||
}
|
||||
const { id, type } = selections
|
||||
setCursor2({ range: sourceRangeMap[id], type })
|
||||
})
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setError()
|
||||
} catch (e: any) {
|
||||
if (e instanceof KCLError) {
|
||||
addKCLError(e)
|
||||
} else {
|
||||
setError('problem')
|
||||
console.log(e)
|
||||
addLog(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
asyncWrap()
|
||||
}, [code, isStreamReady])
|
||||
useSetupEngineManager(streamRef, token)
|
||||
useCodeEval()
|
||||
|
||||
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()
|
||||
setCmdId(newCmdId)
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'mouse_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
|
||||
const eWithButton = { ...e, button: buttonDownInStream }
|
||||
|
||||
if (interactionGuards.pan.callback(eWithButton)) {
|
||||
interaction = 'pan'
|
||||
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||
interaction = 'rotate'
|
||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
console.log('none')
|
||||
return
|
||||
}
|
||||
|
||||
if (cmdId && isMouseDownInStream) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -411,28 +181,9 @@ export function App() {
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const extraExtensions = useMemo(() => {
|
||||
if (TEST) return []
|
||||
return [
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
]
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||
@ -443,7 +194,7 @@ export function App() {
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
project={project}
|
||||
enableMenu={true}
|
||||
@ -452,17 +203,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={{
|
||||
@ -476,31 +227,9 @@ export function App() {
|
||||
icon={faCode}
|
||||
className="open:!mb-2"
|
||||
open={openPanes.includes('code')}
|
||||
menu={<CodeMenu />}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
<button
|
||||
// disabled={!shouldFormat}
|
||||
onClick={formatCode}
|
||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
||||
>
|
||||
format
|
||||
</button>
|
||||
</div>
|
||||
<div id="code-mirror-override">
|
||||
<CodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={[
|
||||
langs.javascript({ jsx: true }),
|
||||
lineHighlightField,
|
||||
...extraExtensions,
|
||||
]}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={editorTheme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
<TextEditor theme={editorTheme} />
|
||||
</CollapsiblePanel>
|
||||
<section className="flex flex-col">
|
||||
<MemoryPanel
|
||||
@ -525,13 +254,13 @@ export function App() {
|
||||
</div>
|
||||
</Resizable>
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{debugPanel && (
|
||||
{showDebugPanel && (
|
||||
<DebugPanel
|
||||
title="Debug"
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
|
@ -1,9 +1,12 @@
|
||||
import Loading from './components/Loading'
|
||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
// Wrapper around protected routes, used in src/Router.tsx
|
||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
|
||||
const {
|
||||
auth: { state },
|
||||
} = useGlobalStateContext()
|
||||
const isLoggedIn = state.matches('checkIfLoggedIn')
|
||||
|
||||
return isLoggedIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
|
112
src/Router.tsx
112
src/Router.tsx
@ -3,8 +3,15 @@ import {
|
||||
createBrowserRouter,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLocation,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
import {
|
||||
matchRoutes,
|
||||
createRoutesFromChildren,
|
||||
useNavigationType,
|
||||
} from 'react-router'
|
||||
import { useEffect } from 'react'
|
||||
import { ErrorPage } from './components/ErrorPage'
|
||||
import { Settings } from './routes/Settings'
|
||||
import Onboarding, {
|
||||
@ -24,7 +31,47 @@ import {
|
||||
} from './lib/tauriFS'
|
||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||
import {
|
||||
SETTINGS_PERSIST_KEY,
|
||||
settingsMachine,
|
||||
} from './machines/settingsMachine'
|
||||
import { ContextFrom } from 'xstate'
|
||||
import CommandBarProvider from 'components/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
|
||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||
Sentry.init({
|
||||
dsn: VITE_KC_SENTRY_DSN,
|
||||
// TODO(paultag): pass in the right env here.
|
||||
// environment: "production",
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes
|
||||
),
|
||||
}),
|
||||
new Sentry.Replay(),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// TODO: Add in kittycad.io endpoints
|
||||
tracePropagationTargets: ['localhost'],
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -68,7 +115,11 @@ const addGlobalContextToElements = (
|
||||
'element' in route
|
||||
? {
|
||||
...route,
|
||||
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{route.element}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
}
|
||||
: route
|
||||
)
|
||||
@ -95,26 +146,25 @@ const router = createBrowserRouter(
|
||||
request,
|
||||
params,
|
||||
}): Promise<IndexLoaderData | Response> => {
|
||||
const store = localStorage.getItem('store')
|
||||
if (store === null) {
|
||||
return redirect(paths.ONBOARDING.INDEX)
|
||||
} else {
|
||||
const status = JSON.parse(store).state.onboardingStatus || ''
|
||||
const notEnRouteToOnboarding =
|
||||
!request.url.includes(paths.ONBOARDING.INDEX) &&
|
||||
request.method === 'GET'
|
||||
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||
const hasValidOnboardingStatus =
|
||||
(status !== undefined && status.length === 0) ||
|
||||
!(status === 'done' || status === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||
ContextFrom<typeof settingsMachine>
|
||||
>
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(
|
||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
|
||||
)
|
||||
}
|
||||
const status = persistedSettings.onboardingStatus || ''
|
||||
const notEnRouteToOnboarding = !request.url.includes(
|
||||
paths.ONBOARDING.INDEX
|
||||
)
|
||||
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||
const hasValidOnboardingStatus =
|
||||
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(
|
||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.id && params.id !== 'new') {
|
||||
@ -164,9 +214,23 @@ const router = createBrowserRouter(
|
||||
if (!isTauri()) {
|
||||
return redirect(paths.FILE + '/new')
|
||||
}
|
||||
|
||||
const projectDir = await initializeProjectDirectory()
|
||||
const projectsNoMeta = (await readDir(projectDir.dir)).filter(
|
||||
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||
ContextFrom<typeof settingsMachine>
|
||||
>
|
||||
const projectDir = await initializeProjectDirectory(
|
||||
persistedSettings.defaultDirectory || ''
|
||||
)
|
||||
if (projectDir !== persistedSettings.defaultDirectory) {
|
||||
localStorage.setItem(
|
||||
SETTINGS_PERSIST_KEY,
|
||||
JSON.stringify({
|
||||
...persistedSettings,
|
||||
defaultDirectory: projectDir,
|
||||
})
|
||||
)
|
||||
}
|
||||
const projectsNoMeta = (await readDir(projectDir)).filter(
|
||||
isProjectDirectory
|
||||
)
|
||||
const projects = await Promise.all(
|
||||
|
60
src/Toolbar.module.css
Normal file
60
src/Toolbar.module.css
Normal file
@ -0,0 +1,60 @@
|
||||
.toolbarWrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@apply flex gap-4 items-center rounded-full;
|
||||
@apply border border-cool-20/30 bg-cool-10/50;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar {
|
||||
@apply border-cool-100/50 bg-cool-120/50;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbar {
|
||||
@apply border-fern-20/20 bg-fern-10/20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbar {
|
||||
@apply border-fern-120/50 bg-fern-100/30;
|
||||
}
|
||||
|
||||
.toolbarCap {
|
||||
@apply text-sm font-bold;
|
||||
@apply bg-cool-20/50 text-cool-100;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbarCap {
|
||||
@apply bg-cool-90/50 text-cool-30;
|
||||
}
|
||||
|
||||
:global(.sketch) .toolbarCap {
|
||||
@apply bg-fern-20/50 text-fern-100;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .toolbarCap {
|
||||
@apply bg-fern-90/50 text-fern-30;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply self-stretch flex items-center px-4 py-1;
|
||||
@apply rounded-l-full;
|
||||
}
|
||||
|
||||
.popoverToggle {
|
||||
@apply self-stretch m-0 flex items-center px-4 py-1;
|
||||
@apply rounded-r-full border-none;
|
||||
@apply hover:bg-cool-20;
|
||||
}
|
||||
|
||||
:global(.dark) .popoverToggle {
|
||||
@apply hover:bg-cool-90;
|
||||
}
|
||||
|
||||
:global(.sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-20;
|
||||
}
|
||||
|
||||
:global(.dark .sketch) .popoverToggle {
|
||||
@apply hover:bg-fern-90;
|
||||
}
|
347
src/Toolbar.tsx
347
src/Toolbar.tsx
@ -1,4 +1,4 @@
|
||||
import { useStore, toolTips } from './useStore'
|
||||
import { useStore, toolTips, Selections } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
@ -8,9 +8,15 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAppMode } from 'hooks/useAppMode'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -20,6 +26,7 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
@ -27,74 +34,30 @@ export const Toolbar = () => {
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
}))
|
||||
useAppMode()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Start Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditExtrude' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst } = sketchOnExtrudedFace(
|
||||
ast,
|
||||
pathToNode,
|
||||
programMemory
|
||||
)
|
||||
updateAst(modifiedAst)
|
||||
}}
|
||||
>
|
||||
SketchOnFace
|
||||
</button>
|
||||
)}
|
||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<>
|
||||
useEffect(() => {
|
||||
console.log('guiMode', guiMode)
|
||||
}, [guiMode])
|
||||
|
||||
function ToolbarButtons() {
|
||||
return (
|
||||
<span className="overflow-x-auto">
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch
|
||||
Start Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditExtrude' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
@ -102,77 +65,211 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
const { modifiedAst } = sketchOnExtrudedFace(
|
||||
ast,
|
||||
pathToNode,
|
||||
false
|
||||
programMemory
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
updateAst(modifiedAst)
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch (w/o pipe)
|
||||
SketchOnFace
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
guiMode.mode !== 'sketch' ||
|
||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||
)
|
||||
return null
|
||||
return (
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('guiMode.pathId', guiMode.pathId)
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<>
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() =>
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
? {
|
||||
sketchMode: 'sketchEdit',
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
ExtrudeSketch
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
false
|
||||
)
|
||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
>
|
||||
ExtrudeSketch (w/o pipe)
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
}}
|
||||
>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
})}
|
||||
<br></br>
|
||||
<ConvertToVariable />
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
<EqualAngle />
|
||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||
<SetAbsDistance buttonType="snapToYAxis" />
|
||||
<SetAbsDistance buttonType="xAbs" />
|
||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||
<SetAbsDistance buttonType="snapToXAxis" />
|
||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||
<SetAbsDistance buttonType="yAbs" />
|
||||
<SetAngleLength angleOrLength="setAngle" />
|
||||
<SetAngleLength angleOrLength="setLength" />
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</div>
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
guiMode.mode !== 'sketch' ||
|
||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||
)
|
||||
return null
|
||||
return (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
? {
|
||||
sketchMode: 'sketchEdit',
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
<EqualAngle />
|
||||
<SetHorzVertDistance buttonType="alignEndsVertically" />
|
||||
<SetHorzVertDistance buttonType="setHorzDistance" />
|
||||
<SetAbsDistance buttonType="snapToYAxis" />
|
||||
<SetAbsDistance buttonType="xAbs" />
|
||||
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
|
||||
<SetAbsDistance buttonType="snapToXAxis" />
|
||||
<SetHorzVertDistance buttonType="setVertDistance" />
|
||||
<SetAbsDistance buttonType="yAbs" />
|
||||
<SetAngleLength angleOrLength="setAngle" />
|
||||
<SetAngleLength angleOrLength="setLength" />
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||
</span>
|
||||
<menu className="flex flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<Popover.Button
|
||||
className={styles.toolbarCap + ' ' + styles.popoverToggle}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 translate-y-1 scale-95"
|
||||
enterTo="opacity-100 translate-y-0 scale-100"
|
||||
leave="transition ease-out duration-75"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-2"
|
||||
>
|
||||
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
|
||||
<section className="flex justify-between items-center">
|
||||
<p
|
||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||
>
|
||||
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||
</p>
|
||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||
</Popover.Button>
|
||||
</section>
|
||||
<section>
|
||||
<ToolbarButtons />
|
||||
</section>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
const iconSizes = {
|
||||
sm: 12,
|
||||
md: 14.4,
|
||||
lg: 18,
|
||||
lg: 20,
|
||||
xl: 28,
|
||||
}
|
||||
|
||||
export interface ActionIconProps extends React.PropsWithChildren {
|
||||
icon?: SolidIconDefinition | BrandIconDefinition
|
||||
className?: string
|
||||
bgClassName?: string
|
||||
iconClassName?: string
|
||||
size?: keyof typeof iconSizes
|
||||
@ -20,6 +22,7 @@ export interface ActionIconProps extends React.PropsWithChildren {
|
||||
|
||||
export const ActionIcon = ({
|
||||
icon = faCircleExclamation,
|
||||
className,
|
||||
bgClassName,
|
||||
iconClassName,
|
||||
size = 'md',
|
||||
@ -28,7 +31,9 @@ export const ActionIcon = ({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'p-1 w-fit inline-grid place-content-center ' +
|
||||
`p-${
|
||||
size === 'xl' ? '2' : '1'
|
||||
} w-fit inline-grid place-content-center ${className} ` +
|
||||
(bgClassName ||
|
||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
||||
}
|
||||
@ -40,7 +45,7 @@ export const ActionIcon = ({
|
||||
height={iconSizes[size]}
|
||||
className={
|
||||
iconClassName ||
|
||||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
7
src/components/AppHeader.module.css
Normal file
7
src/components/AppHeader.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
Some CSS cannot be represented
|
||||
in Tailwind, such as complex grid layouts.
|
||||
*/
|
||||
.header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
@ -2,7 +2,9 @@ import { Toolbar } from '../Toolbar'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -18,12 +20,18 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const [user] = useAuthMachine((s) => s?.context?.user)
|
||||
const {
|
||||
auth: {
|
||||
context: { user },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
'overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
|
||||
(showToolbar ? 'grid ' : 'flex justify-between ') +
|
||||
styles.header +
|
||||
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
@ -35,7 +43,12 @@ export const AppHeader = ({
|
||||
</div>
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || <UserSidebarMenu user={user} />}
|
||||
{children || (
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
182
src/components/AstExplorer.tsx
Normal file
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;
|
||||
}
|
59
src/components/CodeMenu.tsx
Normal file
59
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { useStore } from 'useStore'
|
||||
import styles from './CodeMenu.module.css'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { editorShortcutMeta } from './TextEditor'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
const { formatCode } = useStore((s) => ({
|
||||
formatCode: s.formatCode,
|
||||
}))
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<div
|
||||
className="relative"
|
||||
onClick={(e) => {
|
||||
if (e.eventPhase === 3) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Button className="p-0 border-none relative">
|
||||
<ActionIcon
|
||||
icon={faEllipsis}
|
||||
bgClassName={
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||
}
|
||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||
<Menu.Item>
|
||||
<button onClick={() => formatCode()} className={styles.button}>
|
||||
<span>Format code</span>
|
||||
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={handleConvertToVarClick}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
<small>{editorShortcutMeta.convertToVariable.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
.panel {
|
||||
@apply relative overflow-auto z-0;
|
||||
@apply bg-chalkboard-20/40;
|
||||
@apply relative z-0;
|
||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
:global(.dark) .panel {
|
||||
@apply bg-chalkboard-110/50;
|
||||
@apply bg-chalkboard-110/50 backdrop-blur-0;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 z-10 cursor-pointer;
|
||||
@apply flex items-center gap-2 w-full p-2;
|
||||
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-20;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
||||
title: string
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
@ -18,21 +19,27 @@ export const PanelHeader = ({
|
||||
title,
|
||||
icon,
|
||||
iconClassNames,
|
||||
menu,
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
bgClassName={
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
<div className="flex gap-2 align-center flex-1">
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
bgClassName={
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||
{menu}
|
||||
</div>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
||||
children,
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
||||
{...props}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
>
|
||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
||||
<PanelHeader
|
||||
title={title}
|
||||
icon={icon}
|
||||
iconClassNames={iconClassNames}
|
||||
menu={menu}
|
||||
/>
|
||||
{children}
|
||||
</details>
|
||||
)
|
||||
|
290
src/components/CommandBar.tsx
Normal file
290
src/components/CommandBar.tsx
Normal file
@ -0,0 +1,290 @@
|
||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
||||
import {
|
||||
Dispatch,
|
||||
Fragment,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fuse from 'fuse.js'
|
||||
import { Command, SubCommand } from '../lib/commands'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
export type SortedCommand = {
|
||||
item: Partial<Command | SubCommand> & { name: string }
|
||||
}
|
||||
|
||||
export const CommandsContext = createContext(
|
||||
{} as {
|
||||
commands: Command[]
|
||||
addCommands: (commands: Command[]) => void
|
||||
removeCommands: (commands: Command[]) => void
|
||||
commandBarOpen: boolean
|
||||
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
)
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [commands, internalSetCommands] = useState([] as Command[])
|
||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||
|
||||
const addCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||
}
|
||||
const removeCommands = (newCommands: Command[]) => {
|
||||
internalSetCommands((prevCommands) =>
|
||||
prevCommands.filter((command) => !newCommands.includes(command))
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandsContext.Provider
|
||||
value={{
|
||||
commands,
|
||||
addCommands,
|
||||
removeCommands,
|
||||
commandBarOpen,
|
||||
setCommandBarOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandBar />
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandBar = () => {
|
||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
||||
useHotkeys('meta+k', () => {
|
||||
if (commands.length === 0) return
|
||||
setCommandBarOpen(!commandBarOpen)
|
||||
})
|
||||
|
||||
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
|
||||
null
|
||||
)
|
||||
// keep track of the current subcommand index
|
||||
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
||||
const [subCommandData, setSubCommandData] = useState<{
|
||||
[key: string]: string
|
||||
}>({})
|
||||
|
||||
// if the subcommand index is null, we're not in a subcommand
|
||||
const inSubCommand =
|
||||
selectedCommand &&
|
||||
'meta' in selectedCommand.item &&
|
||||
selectedCommand.item.meta?.args !== undefined &&
|
||||
subCommandIndex !== undefined
|
||||
const currentSubCommand =
|
||||
inSubCommand && 'meta' in selectedCommand.item
|
||||
? selectedCommand.item.meta?.args[subCommandIndex]
|
||||
: undefined
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const availableCommands =
|
||||
inSubCommand && currentSubCommand
|
||||
? currentSubCommand.type === 'string'
|
||||
? query
|
||||
? [{ name: query }]
|
||||
: currentSubCommand.options
|
||||
: currentSubCommand.options
|
||||
: commands
|
||||
|
||||
const fuse = new Fuse(availableCommands || [], {
|
||||
keys: ['name', 'description'],
|
||||
})
|
||||
|
||||
const filteredCommands = query
|
||||
? fuse.search(query)
|
||||
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
||||
|
||||
function clearState() {
|
||||
setQuery('')
|
||||
setCommandBarOpen(false)
|
||||
setSelectedCommand(null)
|
||||
setSubCommandIndex(undefined)
|
||||
setSubCommandData({})
|
||||
}
|
||||
|
||||
function handleCommandSelection(entry: SortedCommand) {
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
||||
setSelectedCommand(entry)
|
||||
setSubCommandIndex(0)
|
||||
setQuery('')
|
||||
return
|
||||
}
|
||||
|
||||
const { item } = entry
|
||||
// If we have just selected a command with no subcommands, run it
|
||||
const isCommandWithoutSubcommands =
|
||||
'callback' in item && !('meta' in item && item.meta)
|
||||
if (isCommandWithoutSubcommands) {
|
||||
if (item.callback === undefined) return
|
||||
item.callback()
|
||||
setCommandBarOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have subcommands and have not yet gathered all the
|
||||
// data required from them, set the selected command to the
|
||||
// current command and increment the subcommand index
|
||||
if (
|
||||
selectedCommand &&
|
||||
subCommandIndex !== undefined &&
|
||||
'meta' in selectedCommand.item
|
||||
) {
|
||||
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
||||
|
||||
if (subCommand) {
|
||||
const newSubCommandData = {
|
||||
...subCommandData,
|
||||
[subCommand.name]: item.name,
|
||||
}
|
||||
const newSubCommandIndex = subCommandIndex + 1
|
||||
|
||||
// If we have subcommands and have gathered all the data required
|
||||
// from them, run the command with the gathered data
|
||||
if (
|
||||
selectedCommand.item.callback &&
|
||||
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
||||
) {
|
||||
selectedCommand.item.callback(newSubCommandData)
|
||||
setCommandBarOpen(false)
|
||||
} else {
|
||||
// Otherwise, set the subcommand data and increment the subcommand index
|
||||
setSubCommandData(newSubCommandData)
|
||||
setSubCommandIndex(newSubCommandIndex)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayValue(command: Command) {
|
||||
if (command.meta?.displayValue === undefined || !command.meta.args)
|
||||
return command.name
|
||||
return command.meta?.displayValue(
|
||||
command.meta.args.map((c) =>
|
||||
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={
|
||||
commandBarOpen &&
|
||||
availableCommands?.length !== undefined &&
|
||||
availableCommands.length > 0
|
||||
}
|
||||
as={Fragment}
|
||||
afterLeave={() => clearState()}
|
||||
>
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
as={Fragment}
|
||||
>
|
||||
<Combobox
|
||||
value={selectedCommand}
|
||||
onChange={handleCommandSelection}
|
||||
className="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
||||
as="div"
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||
<div>
|
||||
{inSubCommand && (
|
||||
<p className="text-liquid-70 dark:text-liquid-30">
|
||||
{selectedCommand.item &&
|
||||
getDisplayValue(selectedCommand.item as Command)}
|
||||
</p>
|
||||
)}
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="bg-transparent focus:outline-none w-full"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
setCommandBarOpen(false)
|
||||
if (
|
||||
inSubCommand &&
|
||||
event.key === 'Backspace' &&
|
||||
!event.currentTarget.value
|
||||
) {
|
||||
setSubCommandIndex(subCommandIndex - 1)
|
||||
setSelectedCommand(null)
|
||||
}
|
||||
}}
|
||||
displayValue={(command: SortedCommand) =>
|
||||
command !== null ? command.item.name : ''
|
||||
}
|
||||
placeholder={
|
||||
inSubCommand
|
||||
? `Enter <${currentSubCommand?.name}>`
|
||||
: 'Search for a command'
|
||||
}
|
||||
value={query}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
||||
{filteredCommands?.map((commandResult) => (
|
||||
<Combobox.Option
|
||||
key={commandResult.item.name}
|
||||
value={commandResult}
|
||||
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
|
||||
>
|
||||
<p>{commandResult.item.name}</p>
|
||||
{(commandResult.item as SubCommand).description && (
|
||||
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||
{(commandResult.item as SubCommand).description}
|
||||
</p>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarProvider
|
@ -5,6 +5,8 @@ import { EngineCommand } from '../lang/std/engineConnection'
|
||||
import { useState } from 'react'
|
||||
import { ActionButton } from '../components/ActionButton'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { AstExplorer } from './AstExplorer'
|
||||
|
||||
type SketchModeCmd = Extract<
|
||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||
@ -22,7 +24,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
distance_to_plane: 100,
|
||||
ortho: true,
|
||||
animated: true, // TODO #273 get prefers reduced motion from CSS
|
||||
animated: !isReducedMotion(),
|
||||
})
|
||||
if (!sketchModeCmd) return null
|
||||
return (
|
||||
@ -93,6 +95,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
>
|
||||
Send sketch mode command
|
||||
</ActionButton>
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
</section>
|
||||
</CollapsiblePanel>
|
||||
)
|
||||
|
@ -39,6 +39,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
const initialValues: OutputFormat = {
|
||||
type: defaultType,
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
}
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
@ -127,7 +128,9 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
||||
id="storage"
|
||||
name="storage"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.storage}
|
||||
value={
|
||||
'storage' in formik.values ? formik.values.storage : ''
|
||||
}
|
||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
||||
>
|
||||
{type === 'gltf' && (
|
||||
|
158
src/components/GlobalStateProvider.tsx
Normal file
158
src/components/GlobalStateProvider.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import {
|
||||
authCommandBarMeta,
|
||||
authMachine,
|
||||
TOKEN_PERSIST_KEY,
|
||||
} from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect, useRef } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import {
|
||||
SETTINGS_PERSIST_KEY,
|
||||
settingsCommandBarMeta,
|
||||
settingsMachine,
|
||||
} from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { setThemeClass, Themes } from 'lib/theme'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
}
|
||||
|
||||
type GlobalContext = {
|
||||
auth: MachineContext<typeof authMachine>
|
||||
settings: MachineContext<typeof settingsMachine>
|
||||
}
|
||||
|
||||
export const GlobalStateContext = createContext({} as GlobalContext)
|
||||
|
||||
export const GlobalStateProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { commands } = useCommandsContext()
|
||||
|
||||
// Settings machine setup
|
||||
const retrievedSettings = useRef(
|
||||
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
||||
)
|
||||
const persistedSettings = Object.assign(
|
||||
settingsMachine.initialState.context,
|
||||
JSON.parse(retrievedSettings.current) as Partial<
|
||||
(typeof settingsMachine)['context']
|
||||
>
|
||||
)
|
||||
|
||||
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
||||
context: persistedSettings,
|
||||
actions: {
|
||||
toastSuccess: (context, event) => {
|
||||
const truncatedNewValue =
|
||||
'data' in event && event.data instanceof Object
|
||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||
.toString()
|
||||
.substring(0, 28) as any)
|
||||
: undefined
|
||||
toast.success(
|
||||
event.type +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"`
|
||||
: '')
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
state: settingsState,
|
||||
send: settingsSend,
|
||||
commands,
|
||||
owner: 'settings',
|
||||
commandBarMeta: settingsCommandBarMeta,
|
||||
})
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
// This is only done if the theme setting is set to 'system'.
|
||||
// It can't be done in XState (in an invoked callback, for example)
|
||||
// because there doesn't seem to be a good way to listen to
|
||||
// events outside of the machine that also depend on the machine's context
|
||||
useEffect(() => {
|
||||
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
if (settingsState.context.theme !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
|
||||
matcher.addEventListener('change', listener)
|
||||
return () => matcher.removeEventListener('change', listener)
|
||||
}, [settingsState.context])
|
||||
|
||||
// Auth machine setup
|
||||
const [authState, authSend] = useMachine(authMachine, {
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(paths.SIGN_IN)
|
||||
logout()
|
||||
},
|
||||
goToIndexPage: () => {
|
||||
if (window.location.pathname.includes(paths.SIGN_IN)) {
|
||||
navigate(paths.INDEX)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useStateMachineCommands({
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commands,
|
||||
commandBarMeta: authCommandBarMeta,
|
||||
owner: 'auth',
|
||||
})
|
||||
|
||||
return (
|
||||
<GlobalStateContext.Provider
|
||||
value={{
|
||||
auth: {
|
||||
state: authState,
|
||||
context: authState.context,
|
||||
send: authSend,
|
||||
},
|
||||
settings: {
|
||||
state: settingsState,
|
||||
context: settingsState.context,
|
||||
send: settingsSend,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GlobalStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default GlobalStateProvider
|
||||
|
||||
export function logout() {
|
||||
const url = withBaseUrl('/logout')
|
||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useEffect } from 'react'
|
||||
import { Themes, useStore } from '../useStore'
|
||||
import { useStore } from '../useStore'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
|
@ -10,16 +10,16 @@ describe('processMemory', () => {
|
||||
// Enable rotations #152
|
||||
const code = `
|
||||
const myVar = 5
|
||||
const myFn = (a) => {
|
||||
fn myFn = (a) => {
|
||||
return a - 2
|
||||
}
|
||||
const otherVar = myFn(5)
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|
||||
const theExtrude = startSketchAt([0, 0])
|
||||
|> lineTo([-2.4, myVar], %)
|
||||
|> lineTo([-0.76, otherVar], %)
|
||||
|> extrude(4, %)
|
||||
|
||||
|
||||
const theSketch = startSketchAt([0, 0])
|
||||
|> lineTo([-3.35, 0.17], %)
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
@ -28,30 +28,21 @@ describe('processMemory', () => {
|
||||
show(theExtrude, theSketch)`
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
console.log('raw log', a)
|
||||
},
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
pendingMemory: {},
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.myFn).toEqual('__function__')
|
||||
expect(output.otherVar).toEqual(3)
|
||||
expect(output).toEqual({
|
||||
myVar: 5,
|
||||
myFn: '__function__',
|
||||
myFn: undefined,
|
||||
otherVar: 3,
|
||||
theExtrude: [],
|
||||
theSketch: [
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
|
||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
|
||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
|
||||
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
|
||||
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
|
||||
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,9 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes, useStore } from '../useStore'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory } from '../lang/executor'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
@ -48,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
Object.keys(programMemory.root).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
|
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,10 +1,11 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ProjectWithEntryPointMetadata, paths } from '../Router'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExportButton } from './ExportButton'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -34,7 +35,7 @@ const ProjectSidebarMenu = ({
|
||||
) : (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className="border-0 px-1 pr-2 pl-0 flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-energy-50"
|
||||
className="border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
|
||||
data-testid="project-sidebar-toggle"
|
||||
>
|
||||
<img
|
||||
@ -46,54 +47,77 @@ const ProjectSidebarMenu = ({
|
||||
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
<Transition
|
||||
enter="duration-200 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
|
||||
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 shadow-md rounded-r-lg overflow-hidden">
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
<Transition
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 -translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 -translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden">
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
|
||||
<img
|
||||
src="/kitt-8bit-winking.svg"
|
||||
alt="KittyCAD App"
|
||||
className="h-9 w-auto"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-energy-10 text-mono"
|
||||
data-testid="projectName"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</p>
|
||||
{project?.entrypoint_metadata && (
|
||||
<p className="m-0 text-energy-40 text-xs" data-testid="createdAt">
|
||||
Created{' '}
|
||||
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-energy-10 text-mono"
|
||||
data-testid="projectName"
|
||||
>
|
||||
{project?.name ? project.name : 'KittyCAD Modeling App'}
|
||||
</p>
|
||||
{project?.entrypoint_metadata && (
|
||||
<p
|
||||
className="m-0 text-energy-40 text-xs"
|
||||
data-testid="createdAt"
|
||||
>
|
||||
Created{' '}
|
||||
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
}}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ExportButton
|
||||
className={{
|
||||
button:
|
||||
'border-transparent dark:border-transparent dark:hover:border-energy-60',
|
||||
}}
|
||||
>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to={paths.HOME}
|
||||
icon={{
|
||||
icon: faHome,
|
||||
}}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
|
||||
>
|
||||
Go to Home
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -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,67 +22,65 @@ export const SetVarNameModal = ({
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onReject}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
onClose={onReject}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
enterFrom="opacity-0 translate-y-4"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="ease-in duration-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
toast.success(`Added variable ${newVariableName}`)
|
||||
}}
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
icon={{ icon: faPlus }}
|
||||
>
|
||||
Set {valueName}
|
||||
</Dialog.Title>
|
||||
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
setShouldCreateVariable={() => {}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||
!isNewVariableNameUnique
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
Add variable
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
|
@ -7,31 +7,52 @@ 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 { addNewSketchLn } from 'lang/std/sketch'
|
||||
|
||||
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,
|
||||
setCmdId,
|
||||
setButtonDownInStream,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
||||
setButtonDownInStream: s.setButtonDownInStream,
|
||||
fileId: s.fileId,
|
||||
setCmdId: s.setCmdId,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { cameraControls },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -44,40 +65,70 @@ 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()
|
||||
setCmdId(newId)
|
||||
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
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'
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(true)
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||
e.preventDefault()
|
||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -94,6 +145,7 @@ export const Stream = ({ className = '' }) => {
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -104,7 +156,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',
|
||||
@ -112,9 +164,8 @@ export const Stream = ({ className = '' }) => {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(false)
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -126,7 +177,109 @@ 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 ({ data }) => {
|
||||
if (command.cmd.type !== 'mouse_click' || !ast) return
|
||||
if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any as 'line')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst)
|
||||
} else if (
|
||||
data?.data?.entities_modified?.length &&
|
||||
!guiMode.waitingFirstClick
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
updateAst(_modifiedAst)
|
||||
}
|
||||
})
|
||||
setDidDragInStream(false)
|
||||
setClickCoords(undefined)
|
||||
}
|
||||
|
||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!clickCoords) return
|
||||
|
||||
const delta =
|
||||
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
|
||||
0.5
|
||||
|
||||
if (delta > 5 && !didDragInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -142,7 +295,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">
|
||||
|
298
src/components/TextEditor.tsx
Normal file
298
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
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,
|
||||
defferedSetCode,
|
||||
editorView,
|
||||
engineCommandManager,
|
||||
formatCode,
|
||||
isLSPServerReady,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setSelectionRanges,
|
||||
sourceRangeMap,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
defferedSetCode: s.defferedSetCode,
|
||||
editorView: s.editorView,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
formatCode: s.formatCode,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
}))
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { textWrapping },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
// Below is the client and server promise.
|
||||
const { lspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
lspServer.start()
|
||||
setIsLSPServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsLSPServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup becuase it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isLSPServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: lspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [lspClient, isLSPServerReady])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
defferedSetCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
@ -2,7 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { GlobalStateProvider } from '../hooks/useAuthMachine'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
@ -94,7 +95,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { paths } from '../Router'
|
||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
@ -15,7 +15,9 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const [_, send] = useAuthMachine()
|
||||
const {
|
||||
auth: { send },
|
||||
} = useGlobalStateContext()
|
||||
|
||||
// Fallback logic for displaying user's "name":
|
||||
// 1. user.name
|
||||
@ -59,82 +61,102 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
Menu
|
||||
</ActionButton>
|
||||
)}
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
<Transition
|
||||
enter="duration-200 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||
</Transition>
|
||||
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg overflow-hidden">
|
||||
{({ close }) => (
|
||||
<>
|
||||
{user && (
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || ''}
|
||||
className="h-8 w-8"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-liquid-10 text-mono"
|
||||
data-testid="username"
|
||||
>
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-liquid-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
</p>
|
||||
<Transition
|
||||
enter="duration-100 ease-out"
|
||||
enterFrom="opacity-0 translate-x-1/4"
|
||||
enterTo="opacity-100 translate-x-0"
|
||||
leave="duration-75 ease-in"
|
||||
leaveFrom="opacity-100 translate-x-0"
|
||||
leaveTo="opacity-0 translate-x-4"
|
||||
as={Fragment}
|
||||
>
|
||||
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden">
|
||||
{({ close }) => (
|
||||
<>
|
||||
{user && (
|
||||
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||
{user.image && !imageLoadFailed && (
|
||||
<div className="rounded-full shadow-inner overflow-hidden">
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || ''}
|
||||
className="h-8 w-8"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setImageLoadFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p
|
||||
className="m-0 text-liquid-10 text-mono"
|
||||
data-testid="username"
|
||||
>
|
||||
{displayedName || ''}
|
||||
</p>
|
||||
{displayedName !== user.email && (
|
||||
<p
|
||||
className="m-0 text-liquid-40 text-xs"
|
||||
data-testid="email"
|
||||
>
|
||||
{user.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faGear }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||
icon={{ icon: faGithub }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
>
|
||||
Request a feature
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send('Log out')}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
||||
>
|
||||
Sign out
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: faGear }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
onClick={() => {
|
||||
// since /settings is a nested route the sidebar doesn't close
|
||||
// automatically when navigating to it
|
||||
close()
|
||||
navigate(makeUrlPathRelative(paths.SETTINGS))
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="link"
|
||||
to="https://github.com/KittyCAD/modeling-app/discussions"
|
||||
icon={{ icon: faGithub }}
|
||||
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
|
||||
>
|
||||
Request a feature
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => send('logout')}
|
||||
icon={{
|
||||
icon: faSignOutAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||
}}
|
||||
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
|
||||
>
|
||||
Sign out
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
185
src/editor/lsp/client.ts
Normal file
185
src/editor/lsp/client.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
import {
|
||||
registerServerCapability,
|
||||
unregisterServerCapability,
|
||||
} from './server-capability-registration'
|
||||
import { Codec, FromServer, IntoServer } from './codec'
|
||||
|
||||
const client_capabilities: LSP.ClientCapabilities = {
|
||||
textDocument: {
|
||||
hover: {
|
||||
dynamicRegistration: true,
|
||||
contentFormat: ['plaintext', 'markdown'],
|
||||
},
|
||||
moniker: {},
|
||||
synchronization: {
|
||||
dynamicRegistration: true,
|
||||
willSave: false,
|
||||
didSave: false,
|
||||
willSaveWaitUntil: false,
|
||||
},
|
||||
completion: {
|
||||
dynamicRegistration: true,
|
||||
completionItem: {
|
||||
snippetSupport: false,
|
||||
commitCharactersSupport: true,
|
||||
documentationFormat: ['plaintext', 'markdown'],
|
||||
deprecatedSupport: false,
|
||||
preselectSupport: false,
|
||||
},
|
||||
contextSupport: false,
|
||||
},
|
||||
signatureHelp: {
|
||||
dynamicRegistration: true,
|
||||
signatureInformation: {
|
||||
documentationFormat: ['plaintext', 'markdown'],
|
||||
},
|
||||
},
|
||||
declaration: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
definition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
typeDefinition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
implementation: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
didChangeConfiguration: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
afterInitializedHooks: (() => Promise<void>)[] = []
|
||||
#fromServer: FromServer
|
||||
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
||||
|
||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
||||
super(
|
||||
new jsrpc.JSONRPCServer(),
|
||||
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
||||
const encoded = Codec.encode(json)
|
||||
intoServer.enqueue(encoded)
|
||||
if (null != json.id) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const response = await fromServer.responses.get(json.id)!
|
||||
this.client.receive(response as jsrpc.JSONRPCResponse)
|
||||
}
|
||||
})
|
||||
)
|
||||
this.#fromServer = fromServer
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// process "window/logMessage": client <- server
|
||||
this.addMethod(LSP.LogMessageNotification.type.method, (params) => {
|
||||
const { type, message } = params as {
|
||||
type: LSP.MessageType
|
||||
message: string
|
||||
}
|
||||
let messageString = ''
|
||||
switch (type) {
|
||||
case LSP.MessageType.Error: {
|
||||
messageString += '[error] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Warning: {
|
||||
messageString += ' [warn] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Info: {
|
||||
messageString += ' [info] '
|
||||
break
|
||||
}
|
||||
case LSP.MessageType.Log: {
|
||||
messageString += ' [log] '
|
||||
break
|
||||
}
|
||||
}
|
||||
messageString += message
|
||||
// console.log(messageString)
|
||||
return
|
||||
})
|
||||
|
||||
// process "client/registerCapability": client <- server
|
||||
this.addMethod(LSP.RegistrationRequest.type.method, (params) => {
|
||||
// Register a server capability.
|
||||
params.registrations.forEach(
|
||||
(capabilityRegistration: LSP.Registration) => {
|
||||
this.serverCapabilities = registerServerCapability(
|
||||
this.serverCapabilities,
|
||||
capabilityRegistration
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// process "client/unregisterCapability": client <- server
|
||||
this.addMethod(LSP.UnregistrationRequest.type.method, (params) => {
|
||||
// Unregister a server capability.
|
||||
params.unregisterations.forEach(
|
||||
(capabilityUnregistration: LSP.Unregistration) => {
|
||||
this.serverCapabilities = unregisterServerCapability(
|
||||
this.serverCapabilities,
|
||||
capabilityUnregistration
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// request "initialize": client <-> server
|
||||
const { capabilities } = await this.request(
|
||||
LSP.InitializeRequest.type.method,
|
||||
{
|
||||
processId: null,
|
||||
clientInfo: {
|
||||
name: 'kcl-language-client',
|
||||
},
|
||||
capabilities: client_capabilities,
|
||||
rootUri: null,
|
||||
} as LSP.InitializeParams
|
||||
)
|
||||
|
||||
this.serverCapabilities = capabilities
|
||||
|
||||
// notify "initialized": client --> server
|
||||
this.notify(LSP.InitializedNotification.type.method, {})
|
||||
|
||||
await Promise.all(
|
||||
this.afterInitializedHooks.map((f: () => Promise<void>) => f())
|
||||
)
|
||||
await Promise.all([this.processNotifications(), this.processRequests()])
|
||||
}
|
||||
|
||||
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
||||
return this.serverCapabilities
|
||||
}
|
||||
|
||||
async processNotifications(): Promise<void> {
|
||||
for await (const notification of this.#fromServer.notifications) {
|
||||
await this.receiveAndSend(notification)
|
||||
}
|
||||
}
|
||||
|
||||
async processRequests(): Promise<void> {
|
||||
for await (const request of this.#fromServer.requests) {
|
||||
await this.receiveAndSend(request)
|
||||
}
|
||||
}
|
||||
|
||||
pushAfterInitializeHook(...hooks: (() => Promise<void>)[]): void {
|
||||
this.afterInitializedHooks.push(...hooks)
|
||||
}
|
||||
}
|
53
src/editor/lsp/codec.ts
Normal file
53
src/editor/lsp/codec.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import * as vsrpc from 'vscode-jsonrpc'
|
||||
|
||||
import Bytes from './codec/bytes'
|
||||
import StreamDemuxer from './codec/demuxer'
|
||||
import Headers from './codec/headers'
|
||||
import Queue from './codec/queue'
|
||||
import Tracer from './tracer'
|
||||
|
||||
export const encoder = new TextEncoder()
|
||||
export const decoder = new TextDecoder()
|
||||
|
||||
export class Codec {
|
||||
static encode(
|
||||
json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse
|
||||
): Uint8Array {
|
||||
const message = JSON.stringify(json)
|
||||
const delimited = Headers.add(message)
|
||||
return Bytes.encode(delimited)
|
||||
}
|
||||
|
||||
static decode<T>(data: Uint8Array): T {
|
||||
const delimited = Bytes.decode(data)
|
||||
const message = Headers.remove(delimited)
|
||||
return JSON.parse(message) as T
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: tracing effiency
|
||||
export class IntoServer
|
||||
extends Queue<Uint8Array>
|
||||
implements AsyncGenerator<Uint8Array, never, void>
|
||||
{
|
||||
enqueue(item: Uint8Array): void {
|
||||
Tracer.client(Headers.remove(decoder.decode(item)))
|
||||
super.enqueue(item)
|
||||
}
|
||||
}
|
||||
|
||||
export interface FromServer extends WritableStream<Uint8Array> {
|
||||
readonly responses: {
|
||||
get(key: number | string): null | Promise<vsrpc.ResponseMessage>
|
||||
}
|
||||
readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void>
|
||||
readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FromServer {
|
||||
export function create(): FromServer {
|
||||
return new StreamDemuxer()
|
||||
}
|
||||
}
|
27
src/editor/lsp/codec/bytes.ts
Normal file
27
src/editor/lsp/codec/bytes.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { encoder, decoder } from '../codec'
|
||||
|
||||
export default class Bytes {
|
||||
static encode(input: string): Uint8Array {
|
||||
return encoder.encode(input)
|
||||
}
|
||||
|
||||
static decode(input: Uint8Array): string {
|
||||
return decoder.decode(input)
|
||||
}
|
||||
|
||||
static append<
|
||||
T extends { length: number; set(arr: T, offset: number): void }
|
||||
>(constructor: { new (length: number): T }, ...arrays: T[]) {
|
||||
let totalLength = 0
|
||||
for (const arr of arrays) {
|
||||
totalLength += arr.length
|
||||
}
|
||||
const result = new constructor(totalLength)
|
||||
let offset = 0
|
||||
for (const arr of arrays) {
|
||||
result.set(arr, offset)
|
||||
offset += arr.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
82
src/editor/lsp/codec/demuxer.ts
Normal file
82
src/editor/lsp/codec/demuxer.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as vsrpc from 'vscode-jsonrpc'
|
||||
|
||||
import Bytes from './bytes'
|
||||
import PromiseMap from './map'
|
||||
import Queue from './queue'
|
||||
import Tracer from '../tracer'
|
||||
|
||||
export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
||||
new PromiseMap()
|
||||
readonly notifications: Queue<vsrpc.NotificationMessage> =
|
||||
new Queue<vsrpc.NotificationMessage>()
|
||||
readonly requests: Queue<vsrpc.RequestMessage> =
|
||||
new Queue<vsrpc.RequestMessage>()
|
||||
|
||||
readonly #start: Promise<void>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.#start = this.start()
|
||||
}
|
||||
|
||||
private async start(): Promise<void> {
|
||||
let contentLength: null | number = null
|
||||
let buffer = new Uint8Array()
|
||||
|
||||
for await (const bytes of this) {
|
||||
buffer = Bytes.append(Uint8Array, buffer, bytes)
|
||||
while (buffer.length > 0) {
|
||||
// check if the content length is known
|
||||
if (null == contentLength) {
|
||||
// if not, try to match the prefixed headers
|
||||
const match = Bytes.decode(buffer).match(
|
||||
/^Content-Length:\s*(\d+)\s*/
|
||||
)
|
||||
if (null == match) continue
|
||||
|
||||
// try to parse the content-length from the headers
|
||||
const length = parseInt(match[1])
|
||||
if (isNaN(length)) throw new Error('invalid content length')
|
||||
|
||||
// slice the headers since we now have the content length
|
||||
buffer = buffer.slice(match[0].length)
|
||||
|
||||
// set the content length
|
||||
contentLength = length
|
||||
}
|
||||
|
||||
// if the buffer doesn't contain a full message; await another iteration
|
||||
if (buffer.length < contentLength) continue
|
||||
|
||||
// Get just the slice of the buffer that is our content length.
|
||||
const slice = buffer.slice(0, contentLength)
|
||||
|
||||
// decode buffer to a string
|
||||
const delimited = Bytes.decode(slice)
|
||||
|
||||
// reset the buffer
|
||||
buffer = buffer.slice(contentLength)
|
||||
// reset the contentLength
|
||||
contentLength = null
|
||||
|
||||
const message = JSON.parse(delimited) as vsrpc.Message
|
||||
Tracer.server(message)
|
||||
|
||||
// demux the message stream
|
||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
||||
this.responses.set(message.id, message)
|
||||
continue
|
||||
}
|
||||
if (vsrpc.Message.isNotification(message)) {
|
||||
this.notifications.enqueue(message)
|
||||
continue
|
||||
}
|
||||
if (vsrpc.Message.isRequest(message)) {
|
||||
this.requests.enqueue(message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/editor/lsp/codec/headers.ts
Normal file
9
src/editor/lsp/codec/headers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default class Headers {
|
||||
static add(message: string): string {
|
||||
return `Content-Length: ${message.length}\r\n\r\n${message}`
|
||||
}
|
||||
|
||||
static remove(delimited: string): string {
|
||||
return delimited.replace(/^Content-Length:\s*\d+\s*/, '')
|
||||
}
|
||||
}
|
72
src/editor/lsp/codec/map.ts
Normal file
72
src/editor/lsp/codec/map.ts
Normal file
@ -0,0 +1,72 @@
|
||||
export default class PromiseMap<K, V extends { toString(): string }> {
|
||||
#map: Map<K, PromiseMap.Entry<V>> = new Map()
|
||||
|
||||
get(key: K & { toString(): string }): null | Promise<V> {
|
||||
let initialized: PromiseMap.Entry<V>
|
||||
// if the entry doesn't exist, set it
|
||||
if (!this.#map.has(key)) {
|
||||
initialized = this.#set(key)
|
||||
} else {
|
||||
// otherwise return the entry
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
initialized = this.#map.get(key)!
|
||||
}
|
||||
// if the entry is a pending promise, return it
|
||||
if (initialized.status === 'pending') {
|
||||
return initialized.promise
|
||||
} else {
|
||||
// otherwise return null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
#set(key: K, value?: V): PromiseMap.Entry<V> {
|
||||
if (this.#map.has(key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return this.#map.get(key)!
|
||||
}
|
||||
// placeholder resolver for entry
|
||||
let resolve = (item: V) => {
|
||||
void item
|
||||
}
|
||||
// promise for entry (which assigns the resolver
|
||||
const promise = new Promise<V>((resolver) => {
|
||||
resolve = resolver
|
||||
})
|
||||
// the initialized entry
|
||||
const initialized: PromiseMap.Entry<V> = {
|
||||
status: 'pending',
|
||||
resolve,
|
||||
promise,
|
||||
}
|
||||
if (null != value) {
|
||||
initialized.resolve(value)
|
||||
}
|
||||
// set the entry
|
||||
this.#map.set(key, initialized)
|
||||
return initialized
|
||||
}
|
||||
|
||||
set(key: K & { toString(): string }, value: V): this {
|
||||
const initialized = this.#set(key, value)
|
||||
// if the promise is pending ...
|
||||
if (initialized.status === 'pending') {
|
||||
// ... set the entry status to resolved to free the promise
|
||||
this.#map.set(key, { status: 'resolved' })
|
||||
// ... and resolve the promise with the given value
|
||||
initialized.resolve(value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#map.size
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PromiseMap {
|
||||
export type Entry<V> =
|
||||
| { status: 'pending'; resolve: (item: V) => void; promise: Promise<V> }
|
||||
| { status: 'resolved' }
|
||||
}
|
113
src/editor/lsp/codec/queue.ts
Normal file
113
src/editor/lsp/codec/queue.ts
Normal file
@ -0,0 +1,113 @@
|
||||
export default class Queue<T>
|
||||
implements WritableStream<T>, AsyncGenerator<T, never, void>
|
||||
{
|
||||
readonly #promises: Promise<T>[] = []
|
||||
readonly #resolvers: ((item: T) => void)[] = []
|
||||
readonly #observers: ((item: T) => void)[] = []
|
||||
|
||||
#closed = false
|
||||
#locked = false
|
||||
readonly #stream: WritableStream<T>
|
||||
|
||||
static #__add<X>(
|
||||
promises: Promise<X>[],
|
||||
resolvers: ((item: X) => void)[]
|
||||
): void {
|
||||
promises.push(
|
||||
new Promise((resolve) => {
|
||||
resolvers.push(resolve)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static #__enqueue<X>(
|
||||
closed: boolean,
|
||||
promises: Promise<X>[],
|
||||
resolvers: ((item: X) => void)[],
|
||||
item: X
|
||||
): void {
|
||||
if (!closed) {
|
||||
if (!resolvers.length) Queue.#__add(promises, resolvers)
|
||||
const resolve = resolvers.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
resolve(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const closed = this.#closed
|
||||
const promises = this.#promises
|
||||
const resolvers = this.#resolvers
|
||||
this.#stream = new WritableStream({
|
||||
write(item: T): void {
|
||||
Queue.#__enqueue(closed, promises, resolvers, item)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#add(): void {
|
||||
return Queue.#__add(this.#promises, this.#resolvers)
|
||||
}
|
||||
|
||||
enqueue(item: T): void {
|
||||
return Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item)
|
||||
}
|
||||
|
||||
dequeue(): Promise<T> {
|
||||
if (!this.#promises.length) this.#add()
|
||||
const item = this.#promises.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
return item
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return !this.#promises.length
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
return !!this.#resolvers.length
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.#promises.length - this.#resolvers.length
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T, never>> {
|
||||
const done = false
|
||||
const value = await this.dequeue()
|
||||
for (const observer of this.#observers) {
|
||||
observer(value)
|
||||
}
|
||||
return { done, value }
|
||||
}
|
||||
|
||||
return(): Promise<IteratorResult<T, never>> {
|
||||
return new Promise(() => {
|
||||
// empty
|
||||
})
|
||||
}
|
||||
|
||||
throw(err: Error): Promise<IteratorResult<T, never>> {
|
||||
return new Promise((_resolve, reject) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T, never, void> {
|
||||
return this
|
||||
}
|
||||
|
||||
get locked(): boolean {
|
||||
return this.#stream.locked
|
||||
}
|
||||
|
||||
abort(reason?: Error): Promise<void> {
|
||||
return this.#stream.abort(reason)
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.#stream.close()
|
||||
}
|
||||
|
||||
getWriter(): WritableStreamDefaultWriter<T> {
|
||||
return this.#stream.getWriter()
|
||||
}
|
||||
}
|
151
src/editor/lsp/index.ts
Normal file
151
src/editor/lsp/index.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import Client from './client'
|
||||
import { LanguageServerPlugin } from './plugin'
|
||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
|
||||
// Client to server then server to client
|
||||
interface LSPRequestMap {
|
||||
initialize: [LSP.InitializeParams, LSP.InitializeResult]
|
||||
'textDocument/hover': [LSP.HoverParams, LSP.Hover]
|
||||
'textDocument/completion': [
|
||||
LSP.CompletionParams,
|
||||
LSP.CompletionItem[] | LSP.CompletionList | null
|
||||
]
|
||||
'textDocument/semanticTokens/full': [
|
||||
LSP.SemanticTokensParams,
|
||||
LSP.SemanticTokens
|
||||
]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
interface LSPNotifyMap {
|
||||
initialized: LSP.InitializedParams
|
||||
'textDocument/didChange': LSP.DidChangeTextDocumentParams
|
||||
'textDocument/didOpen': LSP.DidOpenTextDocumentParams
|
||||
}
|
||||
|
||||
// Server to client
|
||||
interface LSPEventMap {
|
||||
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
|
||||
}
|
||||
|
||||
export type Notification = {
|
||||
[key in keyof LSPEventMap]: {
|
||||
jsonrpc: '2.0'
|
||||
id?: null | undefined
|
||||
method: key
|
||||
params: LSPEventMap[key]
|
||||
}
|
||||
}[keyof LSPEventMap]
|
||||
|
||||
export interface LanguageServerClientOptions {
|
||||
client: Client
|
||||
}
|
||||
|
||||
export class LanguageServerClient {
|
||||
private client: Client
|
||||
|
||||
public ready: boolean
|
||||
|
||||
private plugins: LanguageServerPlugin[]
|
||||
|
||||
public initializePromise: Promise<void>
|
||||
|
||||
private isUpdatingSemanticTokens: boolean = false
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
|
||||
constructor(options: LanguageServerClientOptions) {
|
||||
this.plugins = []
|
||||
this.client = options.client
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.initializePromise = this.initialize()
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Start the client in the background.
|
||||
this.client.start()
|
||||
|
||||
this.ready = true
|
||||
}
|
||||
|
||||
getServerCapabilities(): LSP.ServerCapabilities<any> {
|
||||
return this.client.getServerCapabilities()
|
||||
}
|
||||
|
||||
close() {}
|
||||
|
||||
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
||||
this.notify('textDocument/didOpen', params)
|
||||
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
|
||||
this.notify('textDocument/didChange', params)
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
async updateSemanticTokens(uri: string) {
|
||||
// Make sure we can only run, if we aren't already running.
|
||||
if (!this.isUpdatingSemanticTokens) {
|
||||
this.isUpdatingSemanticTokens = true
|
||||
|
||||
const result = await this.request('textDocument/semanticTokens/full', {
|
||||
textDocument: {
|
||||
uri,
|
||||
},
|
||||
})
|
||||
|
||||
this.semanticTokens = deserializeTokens(
|
||||
result.data,
|
||||
this.getServerCapabilities().semanticTokensProvider
|
||||
)
|
||||
|
||||
this.isUpdatingSemanticTokens = false
|
||||
}
|
||||
}
|
||||
|
||||
getSemanticTokens(): SemanticToken[] {
|
||||
return this.semanticTokens
|
||||
}
|
||||
|
||||
async textDocumentHover(params: LSP.HoverParams) {
|
||||
return await this.request('textDocument/hover', params)
|
||||
}
|
||||
|
||||
async textDocumentCompletion(params: LSP.CompletionParams) {
|
||||
return await this.request('textDocument/completion', params)
|
||||
}
|
||||
|
||||
attachPlugin(plugin: LanguageServerPlugin) {
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
|
||||
detachPlugin(plugin: LanguageServerPlugin) {
|
||||
const i = this.plugins.indexOf(plugin)
|
||||
if (i === -1) return
|
||||
this.plugins.splice(i, 1)
|
||||
}
|
||||
|
||||
private request<K extends keyof LSPRequestMap>(
|
||||
method: K,
|
||||
params: LSPRequestMap[K][0]
|
||||
): Promise<LSPRequestMap[K][1]> {
|
||||
return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
|
||||
}
|
||||
|
||||
private notify<K extends keyof LSPNotifyMap>(
|
||||
method: K,
|
||||
params: LSPNotifyMap[K]
|
||||
): void {
|
||||
return this.client.notify(method, params)
|
||||
}
|
||||
|
||||
private processNotification(notification: Notification) {
|
||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||
}
|
||||
}
|
36
src/editor/lsp/language.ts
Normal file
36
src/editor/lsp/language.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Code mirror language implementation for kcl.
|
||||
|
||||
import {
|
||||
Language,
|
||||
defineLanguageFacet,
|
||||
LanguageSupport,
|
||||
} from '@codemirror/language'
|
||||
import { LanguageServerClient } from '.'
|
||||
import { kclPlugin } from './plugin'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { parser as jsParser } from '@lezer/javascript'
|
||||
|
||||
const data = defineLanguageFacet({})
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
||||
documentUri: string
|
||||
client: LanguageServerClient
|
||||
}
|
||||
|
||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
const lang = new Language(data, jsParser, [], 'kcl')
|
||||
|
||||
// Create our supporting extension.
|
||||
const kclLsp = kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
})
|
||||
|
||||
return new LanguageSupport(lang, [kclLsp])
|
||||
}
|
168
src/editor/lsp/parser.ts
Normal file
168
src/editor/lsp/parser.ts
Normal file
@ -0,0 +1,168 @@
|
||||
// Extends the codemirror Parser for kcl.
|
||||
|
||||
import {
|
||||
Parser,
|
||||
Input,
|
||||
TreeFragment,
|
||||
PartialParse,
|
||||
Tree,
|
||||
NodeType,
|
||||
NodeSet,
|
||||
} from '@lezer/common'
|
||||
import { LanguageServerClient } from '.'
|
||||
import { posToOffset } from './plugin'
|
||||
import { SemanticToken } from './semantic_tokens'
|
||||
import { DocInput } from '@codemirror/language'
|
||||
import { tags, styleTags } from '@lezer/highlight'
|
||||
|
||||
export default class KclParser extends Parser {
|
||||
private client: LanguageServerClient
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
createParse(
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[]
|
||||
): PartialParse {
|
||||
let parse: PartialParse = new Context(this, input, fragments, ranges)
|
||||
return parse
|
||||
}
|
||||
|
||||
getTokenTypes(): string[] {
|
||||
return this.client.getServerCapabilities().semanticTokensProvider!.legend
|
||||
.tokenTypes
|
||||
}
|
||||
|
||||
getSemanticTokens(): SemanticToken[] {
|
||||
return this.client.getSemanticTokens()
|
||||
}
|
||||
}
|
||||
|
||||
class Context implements PartialParse {
|
||||
private parser: KclParser
|
||||
private input: DocInput
|
||||
private fragments: readonly TreeFragment[]
|
||||
private ranges: readonly { from: number; to: number }[]
|
||||
|
||||
private nodeTypes: { [key: string]: NodeType }
|
||||
stoppedAt: number = 0
|
||||
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
private currentLine: number = 0
|
||||
private currentColumn: number = 0
|
||||
private nodeSet: NodeSet
|
||||
|
||||
constructor(
|
||||
/// The parser configuration used.
|
||||
parser: KclParser,
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[]
|
||||
) {
|
||||
this.parser = parser
|
||||
this.input = input as DocInput
|
||||
this.fragments = fragments
|
||||
this.ranges = ranges
|
||||
|
||||
// Iterate over the semantic token types and create a node type for each.
|
||||
this.nodeTypes = {}
|
||||
let nodeArray: NodeType[] = []
|
||||
this.parser.getTokenTypes().forEach((tokenType, index) => {
|
||||
const nodeType = NodeType.define({
|
||||
id: index,
|
||||
name: tokenType,
|
||||
// props: [this.styleTags],
|
||||
})
|
||||
this.nodeTypes[tokenType] = nodeType
|
||||
nodeArray.push(nodeType)
|
||||
})
|
||||
|
||||
this.semanticTokens = this.parser.getSemanticTokens()
|
||||
const styles = styleTags({
|
||||
number: tags.number,
|
||||
variable: tags.variableName,
|
||||
operator: tags.operator,
|
||||
keyword: tags.keyword,
|
||||
string: tags.string,
|
||||
comment: tags.comment,
|
||||
function: tags.function(tags.variableName),
|
||||
})
|
||||
this.nodeSet = new NodeSet(nodeArray).extend(styles)
|
||||
}
|
||||
|
||||
get parsedPos(): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
advance(): Tree | null {
|
||||
if (this.semanticTokens.length === 0) {
|
||||
return new Tree(NodeType.none, [], [], 0)
|
||||
}
|
||||
const tree = this.createTree(this.semanticTokens[0], 0)
|
||||
this.stoppedAt = this.input.doc.length
|
||||
return tree
|
||||
}
|
||||
|
||||
createTree(token: SemanticToken, index: number): Tree {
|
||||
const changedLine = token.delta_line !== 0
|
||||
this.currentLine += token.delta_line
|
||||
if (changedLine) {
|
||||
this.currentColumn = 0
|
||||
}
|
||||
this.currentColumn += token.delta_start
|
||||
|
||||
// Let's get our position relative to the start of the file.
|
||||
let currentPosition = posToOffset(this.input.doc, {
|
||||
line: this.currentLine,
|
||||
character: this.currentColumn,
|
||||
})
|
||||
|
||||
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
|
||||
|
||||
if (currentPosition === undefined) {
|
||||
// This is bad and weird.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
if (index >= this.semanticTokens.length - 1) {
|
||||
// We have no children.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
const nextIndex = index + 1
|
||||
const nextToken = this.semanticTokens[nextIndex]
|
||||
const changedLineNext = nextToken.delta_line !== 0
|
||||
const nextLine = this.currentLine + nextToken.delta_line
|
||||
const nextColumn = changedLineNext
|
||||
? nextToken.delta_start
|
||||
: this.currentColumn + nextToken.delta_start
|
||||
const nextPosition = posToOffset(this.input.doc, {
|
||||
line: nextLine,
|
||||
character: nextColumn,
|
||||
})
|
||||
|
||||
if (nextPosition === undefined) {
|
||||
// This is bad and weird.
|
||||
return new Tree(nodeType, [], [], token.length)
|
||||
}
|
||||
|
||||
// Let's get the
|
||||
|
||||
return new Tree(
|
||||
nodeType,
|
||||
[this.createTree(nextToken, nextIndex)],
|
||||
|
||||
// The positions (offsets relative to the start of this tree) of the children.
|
||||
[nextPosition - currentPosition],
|
||||
token.length
|
||||
)
|
||||
}
|
||||
|
||||
stopAt(pos: number) {
|
||||
this.stoppedAt = pos
|
||||
}
|
||||
}
|
360
src/editor/lsp/plugin.ts
Normal file
360
src/editor/lsp/plugin.ts
Normal file
@ -0,0 +1,360 @@
|
||||
import { autocompletion, completeFromList } from '@codemirror/autocomplete'
|
||||
import { setDiagnostics } from '@codemirror/lint'
|
||||
import { Facet } from '@codemirror/state'
|
||||
import {
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
Tooltip,
|
||||
hoverTooltip,
|
||||
tooltips,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
DiagnosticSeverity,
|
||||
CompletionItemKind,
|
||||
CompletionTriggerKind,
|
||||
} from 'vscode-languageserver-protocol'
|
||||
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
} from '@codemirror/autocomplete'
|
||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
||||
import type { Text } from '@codemirror/state'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { LanguageServerClient, Notification } from '.'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
const changesDelay = 500
|
||||
|
||||
const CompletionItemKindMap = Object.fromEntries(
|
||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||
) as Record<CompletionItemKind, string>
|
||||
|
||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||
const documentUri = Facet.define<string, string>({ combine: useLast })
|
||||
const languageId = Facet.define<string, string>({ combine: useLast })
|
||||
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
|
||||
combine: useLast,
|
||||
})
|
||||
|
||||
export interface LanguageServerOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[] | null
|
||||
documentUri: string
|
||||
allowHTMLContent: boolean
|
||||
client: LanguageServerClient
|
||||
}
|
||||
|
||||
export class LanguageServerPlugin implements PluginValue {
|
||||
public client: LanguageServerClient
|
||||
|
||||
private documentUri: string
|
||||
private languageId: string
|
||||
private documentVersion: number
|
||||
|
||||
private changesTimeout: number
|
||||
|
||||
constructor(private view: EditorView, private allowHTMLContent: boolean) {
|
||||
this.client = this.view.state.facet(client)
|
||||
this.documentUri = this.view.state.facet(documentUri)
|
||||
this.languageId = this.view.state.facet(languageId)
|
||||
this.documentVersion = 0
|
||||
this.changesTimeout = 0
|
||||
|
||||
this.client.attachPlugin(this)
|
||||
|
||||
this.initialize({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
update({ docChanged }: ViewUpdate) {
|
||||
if (!docChanged) return
|
||||
if (this.changesTimeout) clearTimeout(this.changesTimeout)
|
||||
this.changesTimeout = window.setTimeout(() => {
|
||||
this.sendChange({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
})
|
||||
}, changesDelay)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.client.detachPlugin(this)
|
||||
}
|
||||
|
||||
async initialize({ documentText }: { documentText: string }) {
|
||||
if (this.client.initializePromise) {
|
||||
await this.client.initializePromise
|
||||
}
|
||||
this.client.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
languageId: this.languageId,
|
||||
text: documentText,
|
||||
version: this.documentVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async sendChange({ documentText }: { documentText: string }) {
|
||||
if (!this.client.ready) return
|
||||
try {
|
||||
await this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
contentChanges: [{ text: documentText }],
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
requestDiagnostics(view: EditorView) {
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
}
|
||||
|
||||
async requestHoverTooltip(
|
||||
view: EditorView,
|
||||
{ line, character }: { line: number; character: number }
|
||||
): Promise<Tooltip | null> {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().hoverProvider
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
const result = await this.client.textDocumentHover({
|
||||
textDocument: { uri: this.documentUri },
|
||||
position: { line, character },
|
||||
})
|
||||
if (!result) return null
|
||||
const { contents, range } = result
|
||||
let pos = posToOffset(view.state.doc, { line, character })!
|
||||
let end: number | undefined
|
||||
if (range) {
|
||||
pos = posToOffset(view.state.doc, range.start)!
|
||||
end = posToOffset(view.state.doc, range.end)
|
||||
}
|
||||
if (pos === null) return null
|
||||
const dom = document.createElement('div')
|
||||
dom.classList.add('documentation')
|
||||
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
||||
else dom.textContent = formatContents(contents)
|
||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||
}
|
||||
|
||||
async requestCompletion(
|
||||
context: CompletionContext,
|
||||
{ line, character }: { line: number; character: number },
|
||||
{
|
||||
triggerKind,
|
||||
triggerCharacter,
|
||||
}: {
|
||||
triggerKind: CompletionTriggerKind
|
||||
triggerCharacter: string | undefined
|
||||
}
|
||||
): Promise<CompletionResult | null> {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
!this.client.getServerCapabilities().completionProvider
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({
|
||||
documentText: context.state.doc.toString(),
|
||||
})
|
||||
|
||||
const result = await this.client.textDocumentCompletion({
|
||||
textDocument: { uri: this.documentUri },
|
||||
position: { line, character },
|
||||
context: {
|
||||
triggerKind,
|
||||
triggerCharacter,
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const items = 'items' in result ? result.items : result
|
||||
|
||||
let options = items.map(
|
||||
({
|
||||
detail,
|
||||
label,
|
||||
labelDetails,
|
||||
kind,
|
||||
textEdit,
|
||||
documentation,
|
||||
deprecated,
|
||||
insertText,
|
||||
insertTextFormat,
|
||||
sortText,
|
||||
filterText,
|
||||
}) => {
|
||||
const completion: Completion & {
|
||||
filterText: string
|
||||
sortText?: string
|
||||
apply: string
|
||||
} = {
|
||||
label,
|
||||
detail: labelDetails ? labelDetails.detail : detail,
|
||||
apply: label,
|
||||
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
||||
sortText: sortText ?? label,
|
||||
filterText: filterText ?? label,
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = () => {
|
||||
const htmlString = formatContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
return { dom: htmlNode }
|
||||
}
|
||||
}
|
||||
|
||||
return completion
|
||||
}
|
||||
)
|
||||
|
||||
return completeFromList(options)(context)
|
||||
}
|
||||
|
||||
processNotification(notification: Notification) {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
this.processDiagnostics(notification.params)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||
if (params.uri !== this.documentUri) return
|
||||
|
||||
const diagnostics = params.diagnostics
|
||||
.map(({ range, message, severity }) => ({
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
severity: (
|
||||
{
|
||||
[DiagnosticSeverity.Error]: 'error',
|
||||
[DiagnosticSeverity.Warning]: 'warning',
|
||||
[DiagnosticSeverity.Information]: 'info',
|
||||
[DiagnosticSeverity.Hint]: 'info',
|
||||
} as const
|
||||
)[severity!],
|
||||
message,
|
||||
}))
|
||||
.filter(
|
||||
({ from, to }) =>
|
||||
from !== null && to !== null && from !== undefined && to !== undefined
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.from < b.from:
|
||||
return -1
|
||||
case a.from > b.from:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
export function kclPlugin(options: LanguageServerOptions) {
|
||||
let plugin: LanguageServerPlugin | null = null
|
||||
|
||||
return [
|
||||
client.of(options.client),
|
||||
documentUri.of(options.documentUri),
|
||||
languageId.of('kcl'),
|
||||
ViewPlugin.define(
|
||||
(view) =>
|
||||
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
||||
),
|
||||
hoverTooltip(
|
||||
(view, pos) =>
|
||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||
null
|
||||
),
|
||||
tooltips({
|
||||
position: 'absolute',
|
||||
}),
|
||||
autocompletion({
|
||||
override: [
|
||||
async (context) => {
|
||||
if (plugin == null) return null
|
||||
|
||||
const { state, pos, explicit } = context
|
||||
const line = state.doc.lineAt(pos)
|
||||
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||
let trigChar: string | undefined
|
||||
if (
|
||||
!explicit &&
|
||||
plugin.client
|
||||
.getServerCapabilities()
|
||||
.completionProvider?.triggerCharacters?.includes(
|
||||
line.text[pos - line.from - 1]
|
||||
)
|
||||
) {
|
||||
trigKind = CompletionTriggerKind.TriggerCharacter
|
||||
trigChar = line.text[pos - line.from - 1]
|
||||
}
|
||||
if (
|
||||
trigKind === CompletionTriggerKind.Invoked &&
|
||||
!context.matchBefore(/\w+$/)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return await plugin.requestCompletion(
|
||||
context,
|
||||
offsetToPos(state.doc, pos),
|
||||
{
|
||||
triggerKind: trigKind,
|
||||
triggerCharacter: trigChar,
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
export function posToOffset(
|
||||
doc: Text,
|
||||
pos: { line: number; character: number }
|
||||
): number | undefined {
|
||||
if (pos.line >= doc.lines) return
|
||||
const offset = doc.line(pos.line + 1).from + pos.character
|
||||
if (offset > doc.length) return
|
||||
return offset
|
||||
}
|
||||
|
||||
function offsetToPos(doc: Text, offset: number) {
|
||||
const line = doc.lineAt(offset)
|
||||
return {
|
||||
line: line.number - 1,
|
||||
character: offset - line.from,
|
||||
}
|
||||
}
|
||||
|
||||
function formatContents(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
if (Array.isArray(contents)) {
|
||||
return contents.map((c) => formatContents(c) + '\n\n').join('')
|
||||
} else if (typeof contents === 'string') {
|
||||
return Marked.parse(contents)
|
||||
} else {
|
||||
return Marked.parse(contents.value)
|
||||
}
|
||||
}
|
51
src/editor/lsp/semantic_tokens.ts
Normal file
51
src/editor/lsp/semantic_tokens.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
export class SemanticToken {
|
||||
delta_line: number
|
||||
delta_start: number
|
||||
length: number
|
||||
token_type: string
|
||||
token_modifiers_bitset: string
|
||||
|
||||
constructor(
|
||||
delta_line = 0,
|
||||
delta_start = 0,
|
||||
length = 0,
|
||||
token_type = '',
|
||||
token_modifiers_bitset = ''
|
||||
) {
|
||||
this.delta_line = delta_line
|
||||
this.delta_start = delta_start
|
||||
this.length = length
|
||||
this.token_type = token_type
|
||||
this.token_modifiers_bitset = token_modifiers_bitset
|
||||
}
|
||||
}
|
||||
|
||||
export function deserializeTokens(
|
||||
data: number[],
|
||||
semanticTokensProvider?: LSP.SemanticTokensOptions
|
||||
): SemanticToken[] {
|
||||
if (!semanticTokensProvider) {
|
||||
return []
|
||||
}
|
||||
// Check if data length is divisible by 5
|
||||
if (data.length % 5 !== 0) {
|
||||
throw new Error('Length is not divisible by 5')
|
||||
}
|
||||
|
||||
const tokens = []
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
tokens.push(
|
||||
new SemanticToken(
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2],
|
||||
semanticTokensProvider.legend.tokenTypes[data[i + 3]],
|
||||
semanticTokensProvider.legend.tokenModifiers[data[i + 4]]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
80
src/editor/lsp/server-capability-registration.ts
Normal file
80
src/editor/lsp/server-capability-registration.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
Registration,
|
||||
ServerCapabilities,
|
||||
Unregistration,
|
||||
} from 'vscode-languageserver-protocol'
|
||||
|
||||
interface IFlexibleServerCapabilities extends ServerCapabilities {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface IMethodServerCapabilityProviderDictionary {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = {
|
||||
'textDocument/hover': 'hoverProvider',
|
||||
'textDocument/completion': 'completionProvider',
|
||||
'textDocument/signatureHelp': 'signatureHelpProvider',
|
||||
'textDocument/definition': 'definitionProvider',
|
||||
'textDocument/typeDefinition': 'typeDefinitionProvider',
|
||||
'textDocument/implementation': 'implementationProvider',
|
||||
'textDocument/references': 'referencesProvider',
|
||||
'textDocument/documentHighlight': 'documentHighlightProvider',
|
||||
'textDocument/documentSymbol': 'documentSymbolProvider',
|
||||
'textDocument/workspaceSymbol': 'workspaceSymbolProvider',
|
||||
'textDocument/codeAction': 'codeActionProvider',
|
||||
'textDocument/codeLens': 'codeLensProvider',
|
||||
'textDocument/documentFormatting': 'documentFormattingProvider',
|
||||
'textDocument/documentRangeFormatting': 'documentRangeFormattingProvider',
|
||||
'textDocument/documentOnTypeFormatting': 'documentOnTypeFormattingProvider',
|
||||
'textDocument/rename': 'renameProvider',
|
||||
'textDocument/documentLink': 'documentLinkProvider',
|
||||
'textDocument/color': 'colorProvider',
|
||||
'textDocument/foldingRange': 'foldingRangeProvider',
|
||||
'textDocument/declaration': 'declarationProvider',
|
||||
'textDocument/executeCommand': 'executeCommandProvider',
|
||||
}
|
||||
|
||||
function registerServerCapability(
|
||||
serverCapabilities: ServerCapabilities,
|
||||
registration: Registration
|
||||
): ServerCapabilities {
|
||||
const serverCapabilitiesCopy = JSON.parse(
|
||||
JSON.stringify(serverCapabilities)
|
||||
) as IFlexibleServerCapabilities
|
||||
const { method, registerOptions } = registration
|
||||
const providerName = ServerCapabilitiesProviders[method]
|
||||
|
||||
if (providerName) {
|
||||
if (!registerOptions) {
|
||||
serverCapabilitiesCopy[providerName] = true
|
||||
} else {
|
||||
serverCapabilitiesCopy[providerName] = Object.assign(
|
||||
{},
|
||||
JSON.parse(JSON.stringify(registerOptions))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not register server capability.')
|
||||
}
|
||||
|
||||
return serverCapabilitiesCopy
|
||||
}
|
||||
|
||||
function unregisterServerCapability(
|
||||
serverCapabilities: ServerCapabilities,
|
||||
unregistration: Unregistration
|
||||
): ServerCapabilities {
|
||||
const serverCapabilitiesCopy = JSON.parse(
|
||||
JSON.stringify(serverCapabilities)
|
||||
) as IFlexibleServerCapabilities
|
||||
const { method } = unregistration
|
||||
const providerName = ServerCapabilitiesProviders[method]
|
||||
|
||||
delete serverCapabilitiesCopy[providerName]
|
||||
|
||||
return serverCapabilitiesCopy
|
||||
}
|
||||
|
||||
export { registerServerCapability, unregisterServerCapability }
|
42
src/editor/lsp/server.ts
Normal file
42
src/editor/lsp/server.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import init, {
|
||||
InitOutput,
|
||||
lsp_run,
|
||||
ServerConfig,
|
||||
} 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
|
||||
readonly #fromServer: FromServer
|
||||
|
||||
private constructor(
|
||||
initOutput: InitOutput,
|
||||
intoServer: IntoServer,
|
||||
fromServer: FromServer
|
||||
) {
|
||||
this.initOutput = initOutput
|
||||
this.#intoServer = intoServer
|
||||
this.#fromServer = fromServer
|
||||
}
|
||||
|
||||
static async initialize(
|
||||
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')
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const config = new ServerConfig(this.#intoServer, this.#fromServer)
|
||||
await lsp_run(config)
|
||||
}
|
||||
}
|
21
src/editor/lsp/tracer.ts
Normal file
21
src/editor/lsp/tracer.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Message } from 'vscode-languageserver-protocol'
|
||||
|
||||
const env = import.meta.env.MODE
|
||||
|
||||
export default class Tracer {
|
||||
static client(message: string): void {
|
||||
// These are really noisy, so we have a special env var for them.
|
||||
if (env === 'lsp_tracing') {
|
||||
console.log('lsp client message', message)
|
||||
}
|
||||
}
|
||||
|
||||
static server(input: string | Message): void {
|
||||
// These are really noisy, so we have a special env var for them.
|
||||
if (env === 'lsp_tracing') {
|
||||
const message: string =
|
||||
typeof input === 'string' ? input : JSON.stringify(input)
|
||||
console.log('lsp server message', message)
|
||||
}
|
||||
}
|
||||
}
|
@ -8,4 +8,7 @@ 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_TIMEOUT_MS = import.meta.env
|
||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||
export const TEST = import.meta.env.TEST
|
||||
|
243
src/hooks/useAppMode.ts
Normal file
243
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,243 @@
|
||||
// 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
|
||||
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
|
||||
) {
|
||||
if (!defaultPlanes) {
|
||||
const xy = 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 },
|
||||
})
|
||||
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 },
|
||||
})
|
||||
setDefaultPlanes({ xy, yz, xz })
|
||||
} else {
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
}
|
||||
}
|
||||
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else 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(),
|
||||
},
|
||||
}
|
||||
)
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
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: [],
|
||||
})
|
||||
|
||||
console.log('sketchModeResponse', sketchModeResponse)
|
||||
},
|
||||
})
|
||||
return unSub
|
||||
}, [engineCommandManager, defaultPlanes])
|
||||
}
|
||||
|
||||
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()
|
||||
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,
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
return planeId
|
||||
}
|
||||
|
||||
function hideDefaultPlanes(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
defaultPlanes: DefaultPlanes
|
||||
) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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' ||
|
||||
'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: false
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
|
||||
export const AuthMachineContext = createActorContext(authMachine)
|
||||
|
||||
export const GlobalStateProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<AuthMachineContext.Provider
|
||||
machine={() =>
|
||||
authMachine.withConfig({
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(paths.SIGN_IN)
|
||||
logout()
|
||||
},
|
||||
goToIndexPage: () => navigate(paths.INDEX),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AuthMachineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuthMachine<T>(
|
||||
selector: (
|
||||
state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0]
|
||||
) => T = () => null as T
|
||||
): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] {
|
||||
// useActor api normally `[state, send] = useActor`
|
||||
// we're only interested in send because of the selector
|
||||
const send = AuthMachineContext.useActor()[1]
|
||||
|
||||
const selection = AuthMachineContext.useSelector(selector)
|
||||
return [selection, send]
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
const url = withBaseUrl('/logout')
|
||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
}
|
156
src/hooks/useCodeEval.ts
Normal file
156
src/hooks/useCodeEval.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { useEffect } from 'react'
|
||||
import { asyncParser } from '../lang/abstractSyntaxTree'
|
||||
import { _executor } from '../lang/executor'
|
||||
import { useStore } from '../useStore'
|
||||
import { KCLError } from '../lang/errors'
|
||||
|
||||
// This recently moved out of app.tsx
|
||||
// and is our old way of thinking that whenever the code changes we need to re-execute, instead of
|
||||
// being more decisive about when and where we execute, its likey this custom hook will be
|
||||
// refactored away entirely at some point
|
||||
|
||||
export function useCodeEval() {
|
||||
const {
|
||||
addLog,
|
||||
addKCLError,
|
||||
setAst,
|
||||
setError,
|
||||
setProgramMemory,
|
||||
resetLogs,
|
||||
resetKCLErrors,
|
||||
setArtifactMap,
|
||||
engineCommandManager,
|
||||
highlightRange,
|
||||
setHighlightRange,
|
||||
setCursor2,
|
||||
isStreamReady,
|
||||
setIsExecuting,
|
||||
defferedCode,
|
||||
} = useStore((s) => ({
|
||||
addLog: s.addLog,
|
||||
defferedCode: s.defferedCode,
|
||||
setAst: s.setAst,
|
||||
setError: s.setError,
|
||||
setProgramMemory: s.setProgramMemory,
|
||||
resetLogs: s.resetLogs,
|
||||
resetKCLErrors: s.resetKCLErrors,
|
||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
highlightRange: s.highlightRange,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
setCursor2: s.setCursor2,
|
||||
isStreamReady: s.isStreamReady,
|
||||
addKCLError: s.addKCLError,
|
||||
setIsExecuting: s.setIsExecuting,
|
||||
}))
|
||||
useEffect(() => {
|
||||
if (!isStreamReady) return
|
||||
if (!engineCommandManager) return
|
||||
let unsubFn: any[] = []
|
||||
const asyncWrap = async () => {
|
||||
try {
|
||||
if (!defferedCode) {
|
||||
setAst({
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
noneCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
})
|
||||
setProgramMemory({ root: {}, return: null })
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
return
|
||||
}
|
||||
const _ast = await asyncParser(defferedCode)
|
||||
setAst(_ast)
|
||||
resetLogs()
|
||||
resetKCLErrors()
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
setIsExecuting(true)
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
return: null,
|
||||
},
|
||||
engineCommandManager
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands(_ast, programMemory)
|
||||
setIsExecuting(false)
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange = 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 = sourceRangeMap[data.entity_id]
|
||||
setCursor2({ range: sourceRange, type: 'default' })
|
||||
},
|
||||
})
|
||||
unsubFn.push(unSubHover, unSubClick)
|
||||
|
||||
setError()
|
||||
} catch (e: any) {
|
||||
setIsExecuting(false)
|
||||
if (e instanceof KCLError) {
|
||||
addKCLError(e)
|
||||
} else {
|
||||
setError('problem')
|
||||
console.log(e)
|
||||
addLog(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
asyncWrap()
|
||||
return () => {
|
||||
unsubFn.forEach((fn) => fn())
|
||||
}
|
||||
}, [defferedCode, isStreamReady, engineCommandManager])
|
||||
}
|
6
src/hooks/useCommandsContext.ts
Normal file
6
src/hooks/useCommandsContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { CommandsContext } from 'components/CommandBar'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useCommandsContext = () => {
|
||||
return useContext(CommandsContext)
|
||||
}
|
6
src/hooks/useGlobalStateContext.ts
Normal file
6
src/hooks/useGlobalStateContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { GlobalStateContext } from 'components/GlobalStateProvider'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useGlobalStateContext = () => {
|
||||
return useContext(GlobalStateContext)
|
||||
}
|
48
src/hooks/useSetupEngineManager.ts
Normal file
48
src/hooks/useSetupEngineManager.ts
Normal file
@ -0,0 +1,48 @@
|
||||
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,
|
||||
} = useStore((s) => ({
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
setMediaStream: s.setMediaStream,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
}))
|
||||
|
||||
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])
|
||||
}
|
42
src/hooks/useStateMachineCommands.ts
Normal file
42
src/hooks/useStateMachineCommands.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
||||
import { useCommandsContext } from './useCommandsContext'
|
||||
|
||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
commandBarMeta?: CommandBarMeta
|
||||
commands: Command[]
|
||||
owner: string
|
||||
}
|
||||
|
||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
||||
state,
|
||||
send,
|
||||
commandBarMeta,
|
||||
owner,
|
||||
}: UseStateMachineCommandsArgs<T>) {
|
||||
const { addCommands, removeCommands } = useCommandsContext()
|
||||
|
||||
useEffect(() => {
|
||||
const newCommands = state.nextEvents
|
||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||
.map((type) =>
|
||||
createMachineCommand<T>({
|
||||
type,
|
||||
state,
|
||||
send,
|
||||
commandBarMeta,
|
||||
owner,
|
||||
})
|
||||
)
|
||||
.filter((c) => c !== null) as Command[]
|
||||
|
||||
addCommands(newCommands)
|
||||
|
||||
return () => {
|
||||
removeCommands(newCommands)
|
||||
}
|
||||
}, [state])
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '../useStore'
|
||||
import { parse } from 'toml'
|
||||
import {
|
||||
createDir,
|
||||
BaseDirectory,
|
||||
readDir,
|
||||
readTextFile,
|
||||
} from '@tauri-apps/api/fs'
|
||||
|
||||
export const useTauriBoot = () => {
|
||||
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
|
||||
defaultDir: s.defaultDir,
|
||||
setDefaultDir: s.setDefaultDir,
|
||||
setHomeMenuItems: s.setHomeMenuItems,
|
||||
}))
|
||||
useEffect(() => {
|
||||
const isTauri = (window as any).__TAURI__
|
||||
if (!isTauri) return
|
||||
const run = async () => {
|
||||
if (!defaultDir.base) {
|
||||
createDir('puffin-projects/example', {
|
||||
dir: BaseDirectory.Home,
|
||||
recursive: true,
|
||||
})
|
||||
setDefaultDir({
|
||||
base: BaseDirectory.Home,
|
||||
dir: 'puffin-projects',
|
||||
})
|
||||
} else {
|
||||
const directoryResult = await readDir(defaultDir.dir, {
|
||||
dir: defaultDir.base,
|
||||
recursive: true,
|
||||
})
|
||||
const puffinProjects = directoryResult.filter(
|
||||
(file) =>
|
||||
!file?.name?.startsWith('.') &&
|
||||
file?.children?.find((child) => child?.name === 'wax.toml')
|
||||
)
|
||||
|
||||
const tomlFiles = await Promise.all(
|
||||
puffinProjects.map(async (file) => {
|
||||
const parsedToml = parse(
|
||||
await readTextFile(`${file.path}/wax.toml`, {
|
||||
dir: defaultDir.base,
|
||||
})
|
||||
)
|
||||
const mainPath = parsedToml?.package?.main
|
||||
const projectName = parsedToml?.package?.name
|
||||
return {
|
||||
file,
|
||||
mainPath,
|
||||
projectName,
|
||||
}
|
||||
})
|
||||
)
|
||||
setHomeMenuItems(
|
||||
tomlFiles.map(({ file, mainPath, projectName }) => ({
|
||||
name: projectName,
|
||||
path: mainPath ? `${file.path}/${mainPath}` : file.path,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
run()
|
||||
}, [])
|
||||
}
|
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)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}
|
||||
|
||||
return { enable, handleClick }
|
||||
}
|
@ -82,12 +82,36 @@ 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,
|
||||
#code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-liquid-10/50;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-activeLine,
|
||||
.dark #code-mirror-override .cm-activeLineGutter {
|
||||
@apply bg-liquid-80/50;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-gutters {
|
||||
@apply bg-chalkboard-10/50;
|
||||
@apply bg-chalkboard-10/30;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-gutters {
|
||||
@ -99,16 +123,68 @@ code {
|
||||
}
|
||||
#code-mirror-override .cm-cursor {
|
||||
display: block;
|
||||
width: 200px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(0, 55, 94) 0%,
|
||||
#0084e2ff 2%,
|
||||
#0084e255 5%,
|
||||
transparent 100%
|
||||
);
|
||||
width: 1ch;
|
||||
@apply bg-liquid-40 mix-blend-multiply;
|
||||
|
||||
animation: blink 2s ease-out infinite;
|
||||
}
|
||||
|
||||
.dark #code-mirror-override .cm-cursor {
|
||||
@apply bg-liquid-50;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.react-json-view {
|
||||
@apply bg-transparent !important;
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip {
|
||||
@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-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 {
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
|
||||
import './index.css'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Themes, useStore } from './useStore'
|
||||
import { Router } from './Router'
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||
import { getSystemTheme } from './lib/getSystemTheme'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
function setThemeClass(state: Partial<{ theme: Themes }>) {
|
||||
const systemTheme = state.theme === Themes.System && getSystemTheme()
|
||||
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
|
||||
document.body.classList.add('dark')
|
||||
} else {
|
||||
document.body.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
const { theme } = useStore.getState()
|
||||
setThemeClass({ theme })
|
||||
useStore.subscribe(setThemeClass)
|
||||
|
||||
root.render(
|
||||
<HotkeysProvider>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { KCLUnexpectedError } from './errors'
|
||||
import { KCLError } from './errors'
|
||||
import { initPromise } from './rust'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
@ -179,6 +179,9 @@ const newVar = myVar + 1
|
||||
name: 'aIdentifier',
|
||||
},
|
||||
],
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
@ -211,10 +214,8 @@ describe('testing function declaration', () => {
|
||||
type: 'FunctionExpression',
|
||||
start: 11,
|
||||
end: 19,
|
||||
id: null,
|
||||
params: [],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 17,
|
||||
end: 19,
|
||||
body: [],
|
||||
@ -251,7 +252,6 @@ describe('testing function declaration', () => {
|
||||
type: 'FunctionExpression',
|
||||
start: 11,
|
||||
end: 39,
|
||||
id: null,
|
||||
params: [
|
||||
{
|
||||
type: 'Identifier',
|
||||
@ -267,7 +267,6 @@ describe('testing function declaration', () => {
|
||||
},
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 21,
|
||||
end: 39,
|
||||
body: [
|
||||
@ -328,7 +327,6 @@ const myVar = funcN(1, 2)`
|
||||
type: 'FunctionExpression',
|
||||
start: 11,
|
||||
end: 37,
|
||||
id: null,
|
||||
params: [
|
||||
{
|
||||
type: 'Identifier',
|
||||
@ -344,7 +342,6 @@ const myVar = funcN(1, 2)`
|
||||
},
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
start: 21,
|
||||
end: 37,
|
||||
body: [
|
||||
@ -419,6 +416,9 @@ const myVar = funcN(1, 2)`
|
||||
raw: '2',
|
||||
},
|
||||
],
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
@ -488,6 +488,7 @@ describe('testing pipe operator special', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
@ -524,6 +525,7 @@ describe('testing pipe operator special', () => {
|
||||
},
|
||||
{ type: 'PipeSubstitution', start: 59, end: 60 },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
@ -596,6 +598,7 @@ describe('testing pipe operator special', () => {
|
||||
},
|
||||
{ type: 'PipeSubstitution', start: 105, end: 106 },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
@ -632,6 +635,7 @@ describe('testing pipe operator special', () => {
|
||||
},
|
||||
{ type: 'PipeSubstitution', start: 128, end: 129 },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
@ -654,6 +658,9 @@ describe('testing pipe operator special', () => {
|
||||
},
|
||||
{ type: 'PipeSubstitution', start: 143, end: 144 },
|
||||
],
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
@ -733,6 +740,9 @@ describe('testing pipe operator special', () => {
|
||||
end: 35,
|
||||
},
|
||||
],
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
@ -1553,7 +1563,10 @@ const key = 'c'`
|
||||
type: 'NoneCodeNode',
|
||||
start: code.indexOf('\n// this is a comment'),
|
||||
end: code.indexOf('const key'),
|
||||
value: '\n// this is a comment\n',
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
value: 'this is a comment',
|
||||
},
|
||||
}
|
||||
const { nonCodeMeta } = parser_wasm(code)
|
||||
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||
@ -1563,7 +1576,9 @@ const key = 'c'`
|
||||
const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
||||
codeWithExtraStartWhitespace
|
||||
)
|
||||
expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
|
||||
expect(nonCodeMeta2.noneCodeNodes[0].value).toStrictEqual(
|
||||
nonCodeMetaInstance.value
|
||||
)
|
||||
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
||||
nonCodeMetaInstance.start
|
||||
)
|
||||
@ -1571,8 +1586,8 @@ const key = 'c'`
|
||||
it('comments nested within a block statement', () => {
|
||||
const code = `const mySketch = startSketchAt([0,0])
|
||||
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|
||||
|> lineTo([1, 1], %) /* this is
|
||||
a comment
|
||||
|> lineTo([1, 1], %) /* this is
|
||||
a comment
|
||||
spanning a few lines */
|
||||
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|
||||
|> close(%)
|
||||
@ -1585,9 +1600,11 @@ const key = 'c'`
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||
type: 'NoneCodeNode',
|
||||
start: 106,
|
||||
end: 168,
|
||||
value:
|
||||
' /* this is \n a comment \n spanning a few lines */\n ',
|
||||
end: 166,
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
value: 'this is\n a comment\n spanning a few lines',
|
||||
},
|
||||
})
|
||||
})
|
||||
it('comments in a pipe expression', () => {
|
||||
@ -1607,7 +1624,10 @@ const key = 'c'`
|
||||
type: 'NoneCodeNode',
|
||||
start: 125,
|
||||
end: 141,
|
||||
value: '\n// a comment\n ',
|
||||
value: {
|
||||
type: 'blockComment',
|
||||
value: 'a comment',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1631,6 +1651,7 @@ describe('test UnaryExpression', () => {
|
||||
{ type: 'Literal', start: 19, end: 20, value: 4, raw: '4' },
|
||||
{ type: 'Literal', start: 22, end: 25, value: 100, raw: '100' },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
})
|
||||
@ -1664,10 +1685,12 @@ describe('testing nested call expressions', () => {
|
||||
{ type: 'Literal', start: 34, end: 35, value: 5, raw: '5' },
|
||||
{ type: 'Literal', start: 37, end: 38, value: 3, raw: '3' },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
})
|
||||
})
|
||||
@ -1699,6 +1722,7 @@ describe('should recognise callExpresions in binaryExpressions', () => {
|
||||
},
|
||||
{ type: 'PipeSubstitution', start: 25, end: 26 },
|
||||
],
|
||||
function: expect.any(Object),
|
||||
optional: false,
|
||||
},
|
||||
right: { type: 'Literal', value: 1, raw: '1', start: 30, end: 31 },
|
||||
@ -1720,6 +1744,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]]
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -3,9 +3,9 @@ import { parse_js } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { initPromise } from './rust'
|
||||
import { Token } from './tokeniser'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
|
||||
const rangeTypeFix = (ranges: number[][]): [number, number][] =>
|
||||
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
|
||||
ranges.map(([start, end]) => [start, end])
|
||||
|
||||
export const parser_wasm = (code: string): Program => {
|
||||
@ -16,10 +16,8 @@ export const parser_wasm = (code: string): Program => {
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
|
||||
parsed.kind === 'invalid_expression'
|
||||
? [[parsed.start, parsed.end]]
|
||||
: rangeTypeFix(parsed.sourceRanges)
|
||||
parsed.msg,
|
||||
rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
@ -36,10 +34,8 @@ export async function asyncParser(code: string): Promise<Program> {
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg,
|
||||
parsed.kind === 'invalid_expression'
|
||||
? [[parsed.start, parsed.end]]
|
||||
: rangeTypeFix(parsed.sourceRanges)
|
||||
parsed.msg,
|
||||
rangeTypeFix(parsed.sourceRanges)
|
||||
)
|
||||
|
||||
console.log(kclError)
|
||||
|
@ -1,20 +1,20 @@
|
||||
export type { Program } from '../wasm-lib/bindings/Program'
|
||||
export type { Value } from '../wasm-lib/bindings/Value'
|
||||
export type { ObjectExpression } from '../wasm-lib/bindings/ObjectExpression'
|
||||
export type { MemberExpression } from '../wasm-lib/bindings/MemberExpression'
|
||||
export type { PipeExpression } from '../wasm-lib/bindings/PipeExpression'
|
||||
export type { VariableDeclaration } from '../wasm-lib/bindings/VariableDeclaration'
|
||||
export type { PipeSubstitution } from '../wasm-lib/bindings/PipeSubstitution'
|
||||
export type { Identifier } from '../wasm-lib/bindings/Identifier'
|
||||
export type { UnaryExpression } from '../wasm-lib/bindings/UnaryExpression'
|
||||
export type { BinaryExpression } from '../wasm-lib/bindings/BinaryExpression'
|
||||
export type { ReturnStatement } from '../wasm-lib/bindings/ReturnStatement'
|
||||
export type { ExpressionStatement } from '../wasm-lib/bindings/ExpressionStatement'
|
||||
export type { CallExpression } from '../wasm-lib/bindings/CallExpression'
|
||||
export type { VariableDeclarator } from '../wasm-lib/bindings/VariableDeclarator'
|
||||
export type { BinaryPart } from '../wasm-lib/bindings/BinaryPart'
|
||||
export type { Literal } from '../wasm-lib/bindings/Literal'
|
||||
export type { ArrayExpression } from '../wasm-lib/bindings/ArrayExpression'
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||
export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
|
||||
export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
|
||||
export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
|
||||
export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution'
|
||||
export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier'
|
||||
export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression'
|
||||
export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression'
|
||||
export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement'
|
||||
export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement'
|
||||
export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
|
||||
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
||||
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
||||
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
||||
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||
|
||||
export type SyntaxType =
|
||||
| 'Program'
|
||||
@ -22,7 +22,6 @@ export type SyntaxType =
|
||||
| 'BinaryExpression'
|
||||
| 'CallExpression'
|
||||
| 'Identifier'
|
||||
| 'BlockStatement'
|
||||
| 'ReturnStatement'
|
||||
| 'VariableDeclaration'
|
||||
| 'VariableDeclarator'
|
||||
|
@ -11,51 +11,52 @@ describe('testing artifacts', () => {
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
type: 'base',
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: '66366561-6465-4734-a463-366330356563',
|
||||
id: expect.any(String),
|
||||
sourceRange: [21, 42],
|
||||
pathToNode: [],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'toPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [48, 73],
|
||||
id: '30366338-6462-4330-a364-303935626163',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [79, 103],
|
||||
id: '32653334-6331-4231-b162-663334363535',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: '39643164-6130-4734-b432-623638393262',
|
||||
__meta: [{ sourceRange: [21, 42], pathToNode: [] }],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [21, 42] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -69,21 +70,20 @@ const mySketch001 = startSketchAt([0, 0])
|
||||
|> extrude(2, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '65383433-3839-4333-b836-343263636638',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [127, 140], pathToNode: [] },
|
||||
{ sourceRange: [21, 42], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [21, 42] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -106,37 +106,33 @@ const sk2 = startSketchAt([0, 0])
|
||||
|> lineTo([2.5, 0], %)
|
||||
// |> transform(theTransf, %)
|
||||
|> extrude(2, %)
|
||||
|
||||
|
||||
|
||||
show(theExtrude, sk2)`
|
||||
const programMemory = await enginelessExecutor(parser_wasm(code))
|
||||
// @ts-ignore
|
||||
const geos = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
({ name }) => programMemory?.root?.[name]
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '63333330-3631-4230-b664-623132643731',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [212, 227], pathToNode: [] },
|
||||
{ sourceRange: [13, 34], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [13, 34] }],
|
||||
},
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
id: '33316639-3438-4661-a334-663262383737',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
__meta: [
|
||||
{ sourceRange: [453, 466], pathToNode: [] },
|
||||
{ sourceRange: [302, 323], pathToNode: [] },
|
||||
],
|
||||
__meta: [{ sourceRange: [302, 323] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { KclError as RustKclError } from '../wasm-lib/bindings/KclError'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError {
|
||||
|
@ -1,11 +1,11 @@
|
||||
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'
|
||||
import { KCLUndefinedValueError } from './errors'
|
||||
import { KCLError } from './errors'
|
||||
|
||||
beforeAll(() => initPromise)
|
||||
|
||||
@ -30,29 +30,6 @@ const newVar = myVar + 1`
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar.value).toBe('a str another str')
|
||||
})
|
||||
it('test with function call', async () => {
|
||||
const code = `
|
||||
const myVar = "hello"
|
||||
log(5, myVar)`
|
||||
const programMemoryOverride: ProgramMemory['root'] = {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: vi.fn(),
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [0, 0],
|
||||
pathToNode: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
const { root } = await enginelessExecutor(parser_wasm(code), {
|
||||
root: programMemoryOverride,
|
||||
pendingMemory: {},
|
||||
})
|
||||
expect(root.myVar.value).toBe('hello')
|
||||
expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello')
|
||||
})
|
||||
it('fn funcN = () => {} execute', async () => {
|
||||
const { root } = await exe(
|
||||
[
|
||||
@ -84,8 +61,7 @@ show(mySketch)
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [43, 80],
|
||||
id: '37333036-3033-4432-b530-643030303837',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'myPath',
|
||||
},
|
||||
@ -93,10 +69,10 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [2, 3],
|
||||
from: [0, 2],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [86, 102],
|
||||
id: '32343136-3330-4134-a462-376437386365',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -105,8 +81,7 @@ show(mySketch)
|
||||
from: [2, 3],
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 151],
|
||||
id: '32306132-6130-4138-b832-636363326330',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'rightPath',
|
||||
},
|
||||
@ -142,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])
|
||||
@ -168,15 +143,14 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.mySk1).toEqual({
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
type: 'base',
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: '37663863-3664-4366-a637-623739336334',
|
||||
id: expect.any(String),
|
||||
sourceRange: [14, 34],
|
||||
pathToNode: [],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
@ -184,10 +158,10 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [1, 1],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [40, 56],
|
||||
id: '34356231-3362-4363-b935-393033353034',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -196,8 +170,7 @@ show(mySketch)
|
||||
from: [1, 1],
|
||||
__geoMeta: {
|
||||
sourceRange: [62, 100],
|
||||
id: '39623339-3538-4366-b633-356630326639',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
name: 'myPath',
|
||||
},
|
||||
@ -205,17 +178,17 @@ show(mySketch)
|
||||
type: 'toPoint',
|
||||
to: [1, 1],
|
||||
from: [0, 1],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [106, 122],
|
||||
id: '30636135-6232-4335-b665-366562303161',
|
||||
pathToNode: [],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: '30376661-3039-4965-b532-653665313731',
|
||||
__meta: [{ sourceRange: [14, 34], pathToNode: [] }],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [14, 34] }],
|
||||
})
|
||||
})
|
||||
it('execute array expression', async () => {
|
||||
@ -226,45 +199,21 @@ 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: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
},
|
||||
yo: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 49],
|
||||
},
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [14, 15],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@ -276,17 +225,10 @@ 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: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [27, 83],
|
||||
},
|
||||
],
|
||||
@ -298,17 +240,10 @@ show(mySketch)
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclaration'],
|
||||
],
|
||||
sourceRange: [41, 50],
|
||||
},
|
||||
],
|
||||
@ -403,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 () => {
|
||||
@ -421,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)`
|
||||
@ -431,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))'
|
||||
@ -451,7 +388,11 @@ const theExtrude = startSketchAt([0, 0])
|
||||
|> extrude(4, %)
|
||||
show(theExtrude)`
|
||||
await expect(exe(code)).rejects.toEqual(
|
||||
new KCLUndefinedValueError('Memory item myVarZ not found', [[100, 106]])
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
'memory item key `myVarZ` is not defined',
|
||||
[[100, 106]]
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -460,7 +401,7 @@ show(theExtrude)`
|
||||
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, pendingMemory: {} }
|
||||
programMemory: ProgramMemory = { root: {}, return: null }
|
||||
) {
|
||||
const ast = parser_wasm(code)
|
||||
|
||||
|
1034
src/lang/executor.ts
1034
src/lang/executor.ts
File diff suppressed because it is too large
Load Diff
@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
|
||||
expect(str).toBe(`const part001 = startSketchAt('default')
|
||||
|> ry(90, %)
|
||||
|> line('default', %)
|
||||
show(part001)`)
|
||||
show(part001)
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -175,21 +176,24 @@ 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
|
||||
const part001 = startSketchAt([-1.2, 4.83])
|
||||
|> line([2.8, 0], %)
|
||||
|> angledLine([100 + 100, 3.09], %)
|
||||
|> angledLine([abc, 3.09], %)
|
||||
|> angledLine([def('yo'), 3.09], %)
|
||||
|> angledLine([def(yo), 3.09], %)
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
|> angledLine([jkl(yo) + 2, 3.09], %)
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
it('should move a binary expression into a new variable', async () => {
|
||||
@ -231,7 +235,7 @@ show(part001)`
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = def('yo')`)
|
||||
expect(newCode).toContain(`const newVar = def(yo)`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a binary expression with call expression into a new variable', async () => {
|
||||
@ -245,7 +249,7 @@ show(part001)`
|
||||
'newVar'
|
||||
)
|
||||
const newCode = recast(modifiedAst)
|
||||
expect(newCode).toContain(`const newVar = jkl('yo') + 2`)
|
||||
expect(newCode).toContain(`const newVar = jkl(yo) + 2`)
|
||||
expect(newCode).toContain(`angledLine([newVar, 3.09], %)`)
|
||||
})
|
||||
it('should move a identifier into a new variable', async () => {
|
||||
|
@ -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,
|
||||
@ -36,14 +78,14 @@ export function addSketchTo(
|
||||
const _node = { ...node }
|
||||
const _name = name || findUniqueName(node, 'part')
|
||||
|
||||
const startSketchAt = createCallExpression('startSketchAt', [
|
||||
const startSketchAt = createCallExpressionStdLib('startSketchAt', [
|
||||
createLiteral('default'),
|
||||
])
|
||||
const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
|
||||
createLiteral(90),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const initialLineTo = createCallExpression('line', [
|
||||
const initialLineTo = createCallExpressionStdLib('line', [
|
||||
createLiteral('default'),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
@ -112,7 +154,9 @@ function addToShow(node: Program, name: string): Program {
|
||||
const dumbyStartend = { start: 0, end: 0 }
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
if (showCallIndex === -1) {
|
||||
const showCall = createCallExpression('show', [createIdentifier(name)])
|
||||
const showCall = createCallExpressionStdLib('show', [
|
||||
createIdentifier(name),
|
||||
])
|
||||
const showExpressionStatement: ExpressionStatement = {
|
||||
type: 'ExpressionStatement',
|
||||
...dumbyStartend,
|
||||
@ -124,7 +168,7 @@ function addToShow(node: Program, name: string): Program {
|
||||
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
|
||||
const showCallArgs = (showCall.expression as CallExpression).arguments
|
||||
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
|
||||
const newShowExpression = createCallExpression('show', newShowCallArgs)
|
||||
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
|
||||
|
||||
_node.body[showCallIndex] = {
|
||||
...showCall,
|
||||
@ -149,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]
|
||||
}
|
||||
})
|
||||
@ -167,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
|
||||
@ -178,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]
|
||||
}
|
||||
})
|
||||
@ -225,7 +269,7 @@ export function extrudeSketch(
|
||||
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||
|
||||
const extrudeCall = createCallExpression('extrude', [
|
||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||
createLiteral(4),
|
||||
shouldPipe
|
||||
? createPipeSubstitution()
|
||||
@ -313,15 +357,15 @@ export function sketchOnExtrudedFace(
|
||||
const newSketch = createVariableDeclaration(
|
||||
newSketchName,
|
||||
createPipeExpression([
|
||||
createCallExpression('startSketchAt', [
|
||||
createCallExpressionStdLib('startSketchAt', [
|
||||
createArrayExpression([createLiteral(0), createLiteral(0)]),
|
||||
]),
|
||||
createCallExpression('lineTo', [
|
||||
createCallExpressionStdLib('lineTo', [
|
||||
createArrayExpression([createLiteral(1), createLiteral(1)]),
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
createCallExpression('transform', [
|
||||
createCallExpression('getExtrudeWallTransform', [
|
||||
createCallExpressionStdLib('getExtrudeWallTransform', [
|
||||
createLiteral(tag),
|
||||
createIdentifier(oldSketchName),
|
||||
]),
|
||||
@ -414,6 +458,40 @@ export function createPipeSubstitution(): PipeSubstitution {
|
||||
}
|
||||
}
|
||||
|
||||
export function createCallExpressionStdLib(
|
||||
name: string,
|
||||
args: CallExpression['arguments']
|
||||
): CallExpression {
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 0,
|
||||
name,
|
||||
},
|
||||
function: {
|
||||
type: 'StdLib',
|
||||
func: {
|
||||
// We only need the name here to map it back when it serializes
|
||||
// to rust, don't worry about the rest.
|
||||
name,
|
||||
summary: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
returnValue: { type: '', required: false, name: '', schema: {} },
|
||||
args: [],
|
||||
unpublished: false,
|
||||
deprecated: false,
|
||||
},
|
||||
},
|
||||
optional: false,
|
||||
arguments: args,
|
||||
}
|
||||
}
|
||||
|
||||
export function createCallExpression(
|
||||
name: string,
|
||||
args: CallExpression['arguments']
|
||||
@ -428,6 +506,9 @@ export function createCallExpression(
|
||||
end: 0,
|
||||
name,
|
||||
},
|
||||
function: {
|
||||
type: 'InMemory',
|
||||
},
|
||||
optional: false,
|
||||
arguments: args,
|
||||
}
|
||||
|
@ -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,12 +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)`
|
||||
const code = `const myVar = "hello"
|
||||
log(5, myVar)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -62,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])
|
||||
@ -75,7 +76,7 @@ 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 = [
|
||||
@ -87,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 = [
|
||||
@ -99,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(
|
||||
@ -137,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 = [
|
||||
@ -152,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
|
||||
@ -161,35 +162,38 @@ 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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing recasting with comments and whitespace', () => {
|
||||
it('code with comments', () => {
|
||||
const code = `
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
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)
|
||||
@ -197,38 +201,39 @@ const key = 'c'`
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('code with comment and extra lines', () => {
|
||||
const code = `
|
||||
const yo = 'c' /* this is
|
||||
const code = `const yo = 'c'
|
||||
|
||||
/* this is
|
||||
a
|
||||
comment */
|
||||
|
||||
const yo = 'bing'`
|
||||
const yo = 'bing'
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('comments at the start and end', () => {
|
||||
const code = `
|
||||
// this is a comment
|
||||
|
||||
const code = `// this is a comment
|
||||
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' } } } /* block
|
||||
comment */
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
|
||||
/* block
|
||||
comment */
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
}`
|
||||
}
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -244,7 +249,7 @@ const myFn = () => {
|
||||
].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 = `
|
||||
@ -255,7 +260,7 @@ const mySk1 = startSketchAt([0, 0])
|
||||
// comment here
|
||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||
|> lineTo([1, 1], %) /* and
|
||||
here
|
||||
here
|
||||
*/
|
||||
// a comment between pipe expression statements
|
||||
|> rx(90, %)
|
||||
@ -266,10 +271,26 @@ const mySk1 = startSketchAt([0, 0])
|
||||
|> rx(45, %)
|
||||
/*
|
||||
one more for good measure
|
||||
*/`
|
||||
*/
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted).toBe(`// comment at start
|
||||
const mySk1 = startSketchAt([0, 0])
|
||||
|> lineTo([1, 1], %)
|
||||
// comment here
|
||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||
|> lineTo([1, 1], %)
|
||||
/* and
|
||||
here
|
||||
|
||||
a comment between pipe expression statements */
|
||||
|> rx(90, %)
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -278,28 +299,28 @@ 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 = [
|
||||
'const part001 = startSketchAt([0, 0])',
|
||||
'|> line([-2.21, -legLen(5, min(3, 999))], %)',
|
||||
' |> line([-2.21, -legLen(5, min(3, 999))], %)',
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
})
|
||||
|
||||
@ -309,12 +330,13 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
||||
|> line({ to: [0.62, 4.15], tag: 'seg01' }, %)
|
||||
|> line([2.77, -1.24], %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 201,
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
angle: 201,
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
|> line([-0.42, -1.72], %)
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -324,7 +346,8 @@ show(part001)`
|
||||
angle: 201,
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)`
|
||||
}, %)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -333,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,9 +1,9 @@
|
||||
import { Program } from './abstractSyntaxTreeTypes'
|
||||
import { recast_js } from '../wasm-lib/pkg/wasm_lib'
|
||||
import { recast_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||
|
||||
export const recast = (ast: Program): string => {
|
||||
try {
|
||||
const s: string = recast_js(JSON.stringify(ast))
|
||||
const s: string = recast_wasm(JSON.stringify(ast))
|
||||
return s
|
||||
} catch (e) {
|
||||
// TODO: do something real with the error.
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,88 +0,0 @@
|
||||
import { InternalFn } from './stdTypes'
|
||||
import {
|
||||
ExtrudeGroup,
|
||||
ExtrudeSurface,
|
||||
SketchGroup,
|
||||
Position,
|
||||
Rotation,
|
||||
} from '../executor'
|
||||
import { clockwiseSign } from './std'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
export const extrude: InternalFn = (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
length: number,
|
||||
sketchVal: SketchGroup
|
||||
): ExtrudeGroup => {
|
||||
const sketch = sketchVal
|
||||
const { position, rotation } = sketchVal
|
||||
|
||||
const id = generateUuidFromHashSeed(
|
||||
JSON.stringify({
|
||||
code,
|
||||
sourceRange,
|
||||
data: {
|
||||
length,
|
||||
sketchVal,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const extrudeSurfaces: ExtrudeSurface[] = []
|
||||
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [
|
||||
{
|
||||
length,
|
||||
extrusionDirection: extrusionDirection,
|
||||
},
|
||||
],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'extrude',
|
||||
target: sketch.id,
|
||||
distance: length,
|
||||
cap: true,
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
type: 'extrudeGroup',
|
||||
id,
|
||||
value: extrudeSurfaces, // TODO, this is just an empty array now, should be deleted.
|
||||
height: length,
|
||||
position,
|
||||
rotation,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
{
|
||||
sourceRange: sketchVal.__meta[0].sourceRange,
|
||||
pathToNode: sketchVal.__meta[0].pathToNode,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const getExtrudeWallTransform: InternalFn = (
|
||||
_,
|
||||
pathName: string,
|
||||
extrudeGroup: ExtrudeGroup
|
||||
): {
|
||||
position: Position
|
||||
quaternion: Rotation
|
||||
} => {
|
||||
const path = extrudeGroup?.value.find((path) => path.name === pathName)
|
||||
if (!path) throw new Error(`Could not find path with name ${pathName}`)
|
||||
return {
|
||||
position: path.position,
|
||||
quaternion: path.rotation,
|
||||
}
|
||||
}
|
@ -97,12 +97,12 @@ describe('testing changeSketchArguments', () => {
|
||||
const lineAfterChange = 'lineTo([2, 3], %)'
|
||||
test('changeSketchArguments', async () => {
|
||||
// Enable rotations #152
|
||||
const genCode = (line: string) => `
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
const ast = parser_wasm(code)
|
||||
@ -160,13 +160,13 @@ show(mySketch001)`
|
||||
],
|
||||
})
|
||||
// Enable rotations #152
|
||||
const expectedCode = `
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
const 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)
|
||||
})
|
||||
})
|
||||
@ -175,12 +175,12 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
it('needs to be in it', async () => {
|
||||
const originalLine = 'lineTo([-1.59, -1.54], %)'
|
||||
// Enable rotations #152
|
||||
const genCode = (line: string) => `
|
||||
const mySketch001 = startSketchAt([0, 0])
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)`
|
||||
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
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,16 +20,12 @@ import {
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
import {
|
||||
SketchLineHelper,
|
||||
ModifyAstBase,
|
||||
InternalFn,
|
||||
TransformCallback,
|
||||
} from './stdTypes'
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
|
||||
import {
|
||||
createLiteral,
|
||||
@ -42,10 +39,7 @@ import {
|
||||
} from '../modifyAst'
|
||||
import { roundOff, getLength, getAngle } from '../../lib/utils'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import {
|
||||
intersectionWithParallelLine,
|
||||
perpendicularDistance,
|
||||
} from 'sketch-helpers'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
|
||||
export type Coords2d = [number, number]
|
||||
|
||||
@ -115,45 +109,6 @@ function makeId(seed: string | any) {
|
||||
}
|
||||
|
||||
export const lineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
to: [number, number]
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
): SketchGroup => {
|
||||
if (!previousSketch)
|
||||
throw new Error('lineTo must be called after startSketchAt')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const to = 'to' in data ? data.to : data
|
||||
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
sourceRange,
|
||||
id,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if ('tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -221,77 +176,6 @@ export const lineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const line: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| 'default'
|
||||
| {
|
||||
to: [number, number] | 'default'
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
): SketchGroup => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
let args: [number, number] = [0.2, 1]
|
||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
||||
args = data.to
|
||||
} else if (data !== 'default' && !('to' in data)) {
|
||||
args = data
|
||||
}
|
||||
|
||||
const to: [number, number] = [from[0] + args[0], from[1] + args[1]]
|
||||
const lineData: LineData = {
|
||||
from: [...from, 0],
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [lineData, previousSketch],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'extend_path',
|
||||
path: sketchGroup.id,
|
||||
segment: {
|
||||
type: 'line',
|
||||
end: {
|
||||
x: lineData.to[0],
|
||||
y: lineData.to[1],
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if (data !== 'default' && 'tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -303,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'
|
||||
@ -315,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],
|
||||
@ -338,7 +222,11 @@ export const line: SketchLineHelper = {
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -356,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)
|
||||
@ -385,25 +261,6 @@ export const line: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const xLineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
to: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [xVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
||||
return lineTo.fn(meta, { to: [xVal, from[1]], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -438,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')
|
||||
@ -452,25 +309,6 @@ export const xLineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const yLineTo: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
to: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [yVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data]
|
||||
return lineTo.fn(meta, { to: [from[0], yVal], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -505,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')
|
||||
@ -519,21 +357,6 @@ export const yLineTo: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const xLine: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const [xVal, tag] =
|
||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
||||
return line.fn(meta, { to: [xVal, 0], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -570,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')
|
||||
@ -584,22 +407,6 @@ export const xLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const yLine: SketchLineHelper = {
|
||||
fn: (
|
||||
meta,
|
||||
data:
|
||||
| number
|
||||
| {
|
||||
length: number
|
||||
// name?: string
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('bad bad bad')
|
||||
const [yVal, tag] =
|
||||
typeof data !== 'number' ? [data.length, data.tag] : [data]
|
||||
return line.fn(meta, { to: [0, yVal], tag }, previousSketch)
|
||||
},
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
@ -630,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')
|
||||
@ -644,48 +451,6 @@ export const yLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLine: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const sketchGroup = { ...previousSketch }
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const [angle, length] = 'angle' in data ? [data.angle, data.length] : data
|
||||
const to: [number, number] = [
|
||||
from[0] + length * Math.cos((angle * Math.PI) / 180),
|
||||
from[1] + length * Math.sin((angle * Math.PI) / 180),
|
||||
]
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if ('tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: [...sketchGroup.value, currentPath],
|
||||
}
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -753,26 +518,6 @@ export const angledLine: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineOfXLength: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const [angle, length, tag] =
|
||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
||||
return line.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: getYComponent(angle, length), tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -795,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
|
||||
@ -846,26 +591,6 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineOfYLength: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
length: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const [angle, length, tag] =
|
||||
'angle' in data ? [data.angle, data.length, data.tag] : data
|
||||
return line.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: getXComponent(angle, length), tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
@ -888,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)
|
||||
@ -940,33 +665,6 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineToX: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
to: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [angle, xTo, tag] =
|
||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
||||
const xComponent = xTo - from[0]
|
||||
const yComponent = xComponent * Math.tan((angle * Math.PI) / 180)
|
||||
const yTo = from[1] + yComponent
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: [xTo, yTo], tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1036,33 +734,6 @@ export const angledLineToX: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineToY: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| {
|
||||
angle: number
|
||||
to: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const [angle, yTo, tag] =
|
||||
'angle' in data ? [data.angle, data.to, data.tag] : data
|
||||
const yComponent = yTo - from[1]
|
||||
const xComponent = yComponent / Math.tan((angle * Math.PI) / 180)
|
||||
const xTo = from[0] + xComponent
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to: [xTo, yTo], tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1133,37 +804,6 @@ export const angledLineToY: SketchLineHelper = {
|
||||
}
|
||||
|
||||
export const angledLineThatIntersects: SketchLineHelper = {
|
||||
fn: (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data: {
|
||||
angle: number
|
||||
intersectTag: string
|
||||
offset?: number
|
||||
tag?: string
|
||||
},
|
||||
previousSketch: SketchGroup
|
||||
) => {
|
||||
if (!previousSketch) throw new Error('lineTo must be called after lineTo')
|
||||
const intersectPath = previousSketch.value.find(
|
||||
({ name }) => name === data.intersectTag
|
||||
)
|
||||
if (!intersectPath) throw new Error('intersectTag must match a line')
|
||||
const from = getCoordsFromPaths(
|
||||
previousSketch,
|
||||
previousSketch.value.length - 1
|
||||
)
|
||||
const to = intersectionWithParallelLine({
|
||||
line1: [intersectPath.from, intersectPath.to],
|
||||
line1Offset: data.offset || 0,
|
||||
line2Point: from,
|
||||
line2Angle: data.angle,
|
||||
})
|
||||
return lineTo.fn(
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
{ to, tag: data.tag },
|
||||
previousSketch
|
||||
)
|
||||
},
|
||||
add: ({
|
||||
node,
|
||||
pathToNode,
|
||||
@ -1230,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) {
|
||||
@ -1322,60 +962,14 @@ 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({
|
||||
@ -1443,10 +1037,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
|
||||
}
|
||||
|
||||
@ -1526,142 +1121,6 @@ function addTagWithTo(
|
||||
}
|
||||
}
|
||||
|
||||
export const close: InternalFn = (
|
||||
{ sourceRange, engineCommandManager, code },
|
||||
sketchGroup: SketchGroup
|
||||
): SketchGroup => {
|
||||
const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1)
|
||||
const to = sketchGroup.start
|
||||
? sketchGroup.start.from
|
||||
: getCoordsFromPaths(sketchGroup, 0)
|
||||
|
||||
const lineData: LineData = {
|
||||
from: [...from, 0],
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data: sketchGroup,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id,
|
||||
params: [lineData],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'close_path',
|
||||
path_id: sketchGroup.id,
|
||||
},
|
||||
cmd_id: id,
|
||||
},
|
||||
})
|
||||
|
||||
const currentPath: Path = {
|
||||
type: 'toPoint',
|
||||
to,
|
||||
from,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
const newValue = [...sketchGroup.value]
|
||||
newValue.push(currentPath)
|
||||
return {
|
||||
...sketchGroup,
|
||||
value: newValue,
|
||||
}
|
||||
}
|
||||
|
||||
export const startSketchAt: InternalFn = (
|
||||
{ sourceRange, programMemory, engineCommandManager, code },
|
||||
data:
|
||||
| [number, number]
|
||||
| 'default'
|
||||
| {
|
||||
to: [number, number] | 'default'
|
||||
// name?: string
|
||||
tag?: string
|
||||
}
|
||||
): SketchGroup => {
|
||||
let to: [number, number] = [0, 0]
|
||||
if (data !== 'default' && 'to' in data && data.to !== 'default') {
|
||||
to = data.to
|
||||
} else if (data !== 'default' && !('to' in data)) {
|
||||
to = data
|
||||
}
|
||||
|
||||
const lineData: { to: [number, number, number] } = {
|
||||
to: [...to, 0],
|
||||
}
|
||||
const id = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
})
|
||||
const pathId = makeId({
|
||||
code,
|
||||
sourceRange,
|
||||
data,
|
||||
isPath: true,
|
||||
})
|
||||
engineCommandManager.sendModellingCommand({
|
||||
id: pathId,
|
||||
params: [lineData],
|
||||
range: sourceRange,
|
||||
command: {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
cmd_id: pathId,
|
||||
},
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'move_path_pen',
|
||||
path: pathId,
|
||||
to: {
|
||||
x: lineData.to[0],
|
||||
y: lineData.to[1],
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
cmd_id: id,
|
||||
})
|
||||
const currentPath: Path = {
|
||||
type: 'base',
|
||||
to,
|
||||
from: to,
|
||||
__geoMeta: {
|
||||
id,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
}
|
||||
if (data !== 'default' && 'tag' in data) {
|
||||
currentPath.name = data.tag
|
||||
}
|
||||
return {
|
||||
type: 'sketchGroup',
|
||||
start: currentPath,
|
||||
value: [],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
id: pathId,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function getYComponent(
|
||||
angleDegree: number,
|
||||
xComponent: number
|
||||
@ -1688,14 +1147,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]] }
|
||||
}
|
||||
@ -1705,8 +1156,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')
|
||||
|
@ -59,20 +59,20 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
||||
` |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
|
||||
` |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
|
||||
` |> angledLine({`,
|
||||
` angle: 157,`,
|
||||
` length: 1.69,`,
|
||||
` tag: 'abc3'`,
|
||||
` }, %)`,
|
||||
` angle: 157,`,
|
||||
` length: 1.69,`,
|
||||
` tag: 'abc3'`,
|
||||
` }, %)`,
|
||||
` |> angledLineOfXLength({`,
|
||||
` angle: 217,`,
|
||||
` length: 0.86,`,
|
||||
` tag: 'abc4'`,
|
||||
` }, %)`,
|
||||
` angle: 217,`,
|
||||
` length: 0.86,`,
|
||||
` tag: 'abc4'`,
|
||||
` }, %)`,
|
||||
` |> angledLineOfYLength({`,
|
||||
` angle: 104,`,
|
||||
` length: 1.58,`,
|
||||
` tag: 'abc5'`,
|
||||
` }, %)`,
|
||||
` angle: 104,`,
|
||||
` length: 1.58,`,
|
||||
` tag: 'abc5'`,
|
||||
` }, %)`,
|
||||
` |> angledLineToX({ angle: 55, to: -2.89, tag: 'abc6' }, %)`,
|
||||
` |> angledLineToY({ angle: 330, to: 2.53, tag: 'abc7' }, %)`,
|
||||
` |> xLine({ length: 1.47, tag: 'abc8' }, %)`,
|
||||
@ -144,10 +144,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
||||
inputCode: bigExample,
|
||||
callToSwap: [
|
||||
`angledLine({`,
|
||||
` angle: 157,`,
|
||||
` length: 1.69,`,
|
||||
` tag: 'abc3'`,
|
||||
` }, %)`,
|
||||
` angle: 157,`,
|
||||
` length: 1.69,`,
|
||||
` tag: 'abc3'`,
|
||||
` }, %)`,
|
||||
].join('\n'),
|
||||
constraintType: 'horizontal',
|
||||
})
|
||||
@ -172,10 +172,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
||||
inputCode: bigExample,
|
||||
callToSwap: [
|
||||
`angledLineOfXLength({`,
|
||||
` angle: 217,`,
|
||||
` length: 0.86,`,
|
||||
` tag: 'abc4'`,
|
||||
` }, %)`,
|
||||
` angle: 217,`,
|
||||
` length: 0.86,`,
|
||||
` tag: 'abc4'`,
|
||||
` }, %)`,
|
||||
].join('\n'),
|
||||
constraintType: 'horizontal',
|
||||
})
|
||||
@ -201,10 +201,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
|
||||
inputCode: bigExample,
|
||||
callToSwap: [
|
||||
`angledLineOfYLength({`,
|
||||
` angle: 104,`,
|
||||
` length: 1.58,`,
|
||||
` tag: 'abc5'`,
|
||||
` }, %)`,
|
||||
` angle: 104,`,
|
||||
` length: 1.58,`,
|
||||
` tag: 'abc5'`,
|
||||
` }, %)`,
|
||||
].join('\n'),
|
||||
constraintType: 'vertical',
|
||||
})
|
||||
@ -391,6 +391,7 @@ show(part001)`
|
||||
type: 'toPoint',
|
||||
to: [5.62, 1.79],
|
||||
from: [3.48, 0.44],
|
||||
name: '',
|
||||
})
|
||||
})
|
||||
it('verify it works when the segment is in the `start` property', async () => {
|
||||
@ -400,6 +401,11 @@ show(part001)`
|
||||
programMemory.root['part001'] as SketchGroup,
|
||||
[index, index]
|
||||
).segment
|
||||
expect(segment).toEqual({ type: 'base', to: [0, 0.04], from: [0, 0.04] })
|
||||
expect(segment).toEqual({
|
||||
to: [0, 0.04],
|
||||
from: [0, 0.04],
|
||||
name: '',
|
||||
type: 'base',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { getAngle } from '../../lib/utils'
|
||||
import { TooTip, toolTips } from '../../useStore'
|
||||
import {
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
CallExpression,
|
||||
} from '../abstractSyntaxTreeTypes'
|
||||
import { SketchGroup, SourceRange } from '../executor'
|
||||
import { InternalFn } from './stdTypes'
|
||||
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -22,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]
|
||||
@ -36,79 +34,6 @@ export function getSketchSegmentFromSourceRange(
|
||||
}
|
||||
}
|
||||
|
||||
export const segLen: InternalFn = (
|
||||
_,
|
||||
segName: string,
|
||||
sketchGroup: SketchGroup
|
||||
): number => {
|
||||
const line = sketchGroup?.value.find((seg) => seg?.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
|
||||
return Math.sqrt(
|
||||
(line.from[1] - line.to[1]) ** 2 + (line.from[0] - line.to[0]) ** 2
|
||||
)
|
||||
}
|
||||
|
||||
export const segAng: InternalFn = (
|
||||
_,
|
||||
segName: string,
|
||||
sketchGroup: SketchGroup
|
||||
): number => {
|
||||
const line = sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
return getAngle(line.from, line.to)
|
||||
}
|
||||
|
||||
function segEndFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, segName: string, sketchGroup: SketchGroup): number => {
|
||||
const line =
|
||||
sketchGroup?.start?.name === segName
|
||||
? sketchGroup?.start
|
||||
: sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!line) return 0
|
||||
return which === 'x' ? line.to[0] : line.to[1]
|
||||
}
|
||||
}
|
||||
|
||||
export const segEndX: InternalFn = segEndFactory('x')
|
||||
export const segEndY: InternalFn = segEndFactory('y')
|
||||
|
||||
function lastSegFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, sketchGroup: SketchGroup): number => {
|
||||
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
|
||||
return which === 'x' ? lastLine.to[0] : lastLine.to[1]
|
||||
}
|
||||
}
|
||||
|
||||
export const lastSegX: InternalFn = lastSegFactory('x')
|
||||
export const lastSegY: InternalFn = lastSegFactory('y')
|
||||
|
||||
function angleToMatchLengthFactory(which: 'x' | 'y'): InternalFn {
|
||||
return (_, segName: string, to: number, sketchGroup: SketchGroup): number => {
|
||||
const isX = which === 'x'
|
||||
const lineToMatch = sketchGroup?.value.find((seg) => seg.name === segName)
|
||||
// maybe this should throw, but the language doesn't have a way to handle errors yet
|
||||
if (!lineToMatch) return 0
|
||||
const lengthToMatch = Math.sqrt(
|
||||
(lineToMatch.from[1] - lineToMatch.to[1]) ** 2 +
|
||||
(lineToMatch.from[0] - lineToMatch.to[0]) ** 2
|
||||
)
|
||||
|
||||
const lastLine = sketchGroup?.value[sketchGroup.value.length - 1]
|
||||
const diff = Math.abs(to - (isX ? lastLine.to[0] : lastLine.to[1]))
|
||||
|
||||
const angleR = Math[isX ? 'acos' : 'asin'](diff / lengthToMatch)
|
||||
|
||||
return diff > lengthToMatch ? 0 : (angleR * 180) / Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
export const angleToMatchLengthX: InternalFn = angleToMatchLengthFactory('x')
|
||||
export const angleToMatchLengthY: InternalFn = angleToMatchLengthFactory('y')
|
||||
|
||||
export function isSketchVariablesLinked(
|
||||
secondaryVarDec: VariableDeclarator,
|
||||
primaryVarDec: VariableDeclarator,
|
||||
|
@ -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
|
||||
@ -133,69 +134,70 @@ const myAng2 = 134
|
||||
const part001 = startSketchAt([0, 0])
|
||||
|> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
|
||||
|> angledLineToX([
|
||||
-angleToMatchLengthX('seg01', myVar, %),
|
||||
myVar
|
||||
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|
||||
-angleToMatchLengthX('seg01', myVar, %),
|
||||
myVar
|
||||
], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|
||||
|> angledLineToY([
|
||||
-angleToMatchLengthY('seg01', myVar, %),
|
||||
myVar
|
||||
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|
||||
-angleToMatchLengthY('seg01', myVar, %),
|
||||
myVar
|
||||
], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|
||||
|> angledLine([45, segLen('seg01', %)], %) // ln-lineTo-free should become angledLine
|
||||
|> angledLine([45, segLen('seg01', %)], %) // ln-angledLineToX-free should become angledLine
|
||||
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineToX-angle should become angledLine
|
||||
|> angledLineToX([
|
||||
angleToMatchLengthX('seg01', myVar2, %),
|
||||
myVar2
|
||||
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
|
||||
angleToMatchLengthX('seg01', myVar2, %),
|
||||
myVar2
|
||||
], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
|
||||
|> angledLine([-45, segLen('seg01', %)], %) // ln-angledLineToY-free should become angledLine
|
||||
|> angledLine([myAng2, segLen('seg01', %)], %) // ln-angledLineToY-angle should become angledLine
|
||||
|> angledLineToY([
|
||||
angleToMatchLengthY('seg01', myVar3, %),
|
||||
myVar3
|
||||
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
|
||||
angleToMatchLengthY('seg01', myVar3, %),
|
||||
myVar3
|
||||
], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
|
||||
|> line([
|
||||
min(segLen('seg01', %), myVar),
|
||||
legLen(segLen('seg01', %), myVar)
|
||||
], %) // ln-should use legLen for y
|
||||
min(segLen('seg01', %), myVar),
|
||||
legLen(segLen('seg01', %), myVar)
|
||||
], %) // ln-should use legLen for y
|
||||
|> line([
|
||||
min(segLen('seg01', %), myVar),
|
||||
-legLen(segLen('seg01', %), myVar)
|
||||
], %) // ln-legLen but negative
|
||||
min(segLen('seg01', %), myVar),
|
||||
-legLen(segLen('seg01', %), myVar)
|
||||
], %) // ln-legLen but negative
|
||||
|> angledLine([-112, segLen('seg01', %)], %) // ln-should become angledLine
|
||||
|> angledLine([myVar, segLen('seg01', %)], %) // ln-use segLen for secound arg
|
||||
|> angledLine([45, segLen('seg01', %)], %) // ln-segLen again
|
||||
|> angledLine([54, segLen('seg01', %)], %) // ln-should be transformed to angledLine
|
||||
|> angledLineOfXLength([
|
||||
legAngX(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-should use legAngX to calculate angle
|
||||
legAngX(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-should use legAngX to calculate angle
|
||||
|> angledLineOfXLength([
|
||||
180 + legAngX(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-same as above but should have + 180 to match original quadrant
|
||||
180 + legAngX(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-same as above but should have + 180 to match original quadrant
|
||||
|> line([
|
||||
legLen(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-legLen again but yRelative
|
||||
legLen(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-legLen again but yRelative
|
||||
|> line([
|
||||
-legLen(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-negative legLen yRelative
|
||||
-legLen(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-negative legLen yRelative
|
||||
|> angledLine([58, segLen('seg01', %)], %) // ln-angledLineOfYLength-free should become angledLine
|
||||
|> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineOfYLength-angle should become angledLine
|
||||
|> angledLineOfXLength([
|
||||
legAngY(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-angledLineOfYLength-yRelative use legAngY
|
||||
legAngY(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-angledLineOfYLength-yRelative use legAngY
|
||||
|> angledLineOfXLength([
|
||||
270 + legAngY(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
|
||||
270 + legAngY(segLen('seg01', %), myVar),
|
||||
min(segLen('seg01', %), myVar)
|
||||
], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
|
||||
|> xLine(segLen('seg01', %), %) // ln-xLine-free should sub in segLen
|
||||
|> 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,
|
||||
@ -406,9 +412,9 @@ show(part001)`
|
||||
'setVertDistance'
|
||||
)
|
||||
expect(expectedCode).toContain(`|> lineTo([
|
||||
lastSegX(%) + myVar,
|
||||
segEndY('seg01', %) + 2.93
|
||||
], %) // xRelative`)
|
||||
lastSegX(%) + myVar,
|
||||
segEndY('seg01', %) + 2.93
|
||||
], %) // xRelative`)
|
||||
})
|
||||
it('testing for yRelative to horizontal distance', async () => {
|
||||
const expectedCode = await helperThing(
|
||||
@ -417,9 +423,9 @@ show(part001)`
|
||||
'setHorzDistance'
|
||||
)
|
||||
expect(expectedCode).toContain(`|> lineTo([
|
||||
segEndX('seg01', %) + 2.6,
|
||||
lastSegY(%) + myVar
|
||||
], %) // yRelative`)
|
||||
segEndX('seg01', %) + 2.6,
|
||||
lastSegY(%) + myVar
|
||||
], %) // yRelative`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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'
|
||||
@ -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') {
|
||||
return transformInfo
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return transformInfo
|
||||
}
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
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
|
||||
}
|
||||
@ -1187,25 +1179,12 @@ function getTransformMapPath(
|
||||
|
||||
// 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') {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (firstArg.val?.type !== 'Literal') {
|
||||
return false
|
||||
}
|
||||
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||
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 {
|
||||
@ -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')
|
||||
@ -1452,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
|
||||
@ -1538,23 +1517,46 @@ export function getConstraintLevelFromSourceRange(
|
||||
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,170 +0,0 @@
|
||||
import {
|
||||
lineTo,
|
||||
xLineTo,
|
||||
yLineTo,
|
||||
line,
|
||||
xLine,
|
||||
yLine,
|
||||
angledLine,
|
||||
angledLineOfXLength,
|
||||
angledLineToX,
|
||||
angledLineOfYLength,
|
||||
angledLineToY,
|
||||
close,
|
||||
startSketchAt,
|
||||
angledLineThatIntersects,
|
||||
} from './sketch'
|
||||
import {
|
||||
segLen,
|
||||
segAng,
|
||||
angleToMatchLengthX,
|
||||
angleToMatchLengthY,
|
||||
segEndX,
|
||||
segEndY,
|
||||
lastSegX,
|
||||
lastSegY,
|
||||
} from './sketchConstraints'
|
||||
import { getExtrudeWallTransform, extrude } from './extrude'
|
||||
|
||||
import { InternalFn, InternalFnNames } from './stdTypes'
|
||||
|
||||
// const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
|
||||
// { sourceRange }: InternalFirstArg,
|
||||
// transformInfo: {
|
||||
// position: Position
|
||||
// quaternion: Rotation
|
||||
// },
|
||||
// sketch: T
|
||||
// ): T => {
|
||||
// const quaternionToApply = new Quaternion(...transformInfo?.quaternion)
|
||||
// const newQuaternion = new Quaternion(...sketch.rotation).multiply(
|
||||
// quaternionToApply.invert()
|
||||
// )
|
||||
|
||||
// const oldPosition = new Vector3(...sketch?.position)
|
||||
// const newPosition = oldPosition
|
||||
// .applyQuaternion(quaternionToApply)
|
||||
// .add(new Vector3(...transformInfo?.position))
|
||||
// return {
|
||||
// ...sketch,
|
||||
// position: newPosition.toArray(),
|
||||
// rotation: newQuaternion.toArray(),
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
// const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
|
||||
// { sourceRange }: InternalFirstArg,
|
||||
// vec3: [number, number, number],
|
||||
// sketch: T
|
||||
// ): T => {
|
||||
// const oldPosition = new Vector3(...sketch.position)
|
||||
// const newPosition = oldPosition.add(new Vector3(...vec3))
|
||||
// return {
|
||||
// ...sketch,
|
||||
// position: newPosition.toArray(),
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
const min: InternalFn = (_, a: number, b: number): number => Math.min(a, b)
|
||||
|
||||
const legLen: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
Math.sqrt(
|
||||
hypotenuse ** 2 - Math.min(Math.abs(leg), Math.abs(hypotenuse)) ** 2
|
||||
)
|
||||
|
||||
const legAngX: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
(Math.acos(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
|
||||
|
||||
const legAngY: InternalFn = (_, hypotenuse: number, leg: number): number =>
|
||||
(Math.asin(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI
|
||||
|
||||
export const internalFns: { [key in InternalFnNames]: InternalFn } = {
|
||||
// TODO - re-enable these
|
||||
// rx: rotateOnAxis([1, 0, 0]), // Enable rotations #152
|
||||
// ry: rotateOnAxis([0, 1, 0]),
|
||||
// rz: rotateOnAxis([0, 0, 1]),
|
||||
extrude,
|
||||
// translate,
|
||||
// transform,
|
||||
getExtrudeWallTransform,
|
||||
min,
|
||||
legLen,
|
||||
legAngX,
|
||||
legAngY,
|
||||
segEndX,
|
||||
segEndY,
|
||||
lastSegX,
|
||||
lastSegY,
|
||||
segLen,
|
||||
segAng,
|
||||
angleToMatchLengthX,
|
||||
angleToMatchLengthY,
|
||||
lineTo: lineTo.fn,
|
||||
xLineTo: xLineTo.fn,
|
||||
yLineTo: yLineTo.fn,
|
||||
line: line.fn,
|
||||
xLine: xLine.fn,
|
||||
yLine: yLine.fn,
|
||||
angledLine: angledLine.fn,
|
||||
angledLineOfXLength: angledLineOfXLength.fn,
|
||||
angledLineToX: angledLineToX.fn,
|
||||
angledLineOfYLength: angledLineOfYLength.fn,
|
||||
angledLineToY: angledLineToY.fn,
|
||||
angledLineThatIntersects: angledLineThatIntersects.fn,
|
||||
startSketchAt,
|
||||
close,
|
||||
}
|
||||
|
||||
// function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
|
||||
// axisMultiplier: [number, number, number]
|
||||
// ): InternalFn {
|
||||
// return ({ sourceRange }, rotationD: number, sketch: T): T => {
|
||||
// const rotationR = rotationD * (Math.PI / 180)
|
||||
// const rotateVec = new Vector3(...axisMultiplier)
|
||||
// const quaternion = new Quaternion()
|
||||
// quaternion.setFromAxisAngle(rotateVec, rotationR)
|
||||
|
||||
// const position = new Vector3(...sketch.position)
|
||||
// .applyQuaternion(quaternion)
|
||||
// .toArray()
|
||||
|
||||
// const existingQuat = new Quaternion(...sketch.rotation)
|
||||
// const rotation = quaternion.multiply(existingQuat).toArray()
|
||||
// return {
|
||||
// ...sketch,
|
||||
// rotation,
|
||||
// position,
|
||||
// __meta: [
|
||||
// ...sketch.__meta,
|
||||
// {
|
||||
// sourceRange,
|
||||
// pathToNode: [], // TODO
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export function clockwiseSign(points: [number, number][]): number {
|
||||
let sum = 0
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const currentPoint = points[i]
|
||||
const nextPoint = points[(i + 1) % points.length]
|
||||
sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1])
|
||||
}
|
||||
return sum >= 0 ? 1 : -1
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user