Compare commits
93 Commits
Author | SHA1 | Date | |
---|---|---|---|
caddac5059 | |||
54751aa7bb | |||
7b7d5e5f5e | |||
f7971bddef | |||
e4f2e66029 | |||
663c396128 | |||
8db86a6783 | |||
d7ad7c749e | |||
6e3c642d22 | |||
4d7433ff3a | |||
4e93146559 | |||
731a9bfbdb | |||
cdb4c36cf5 | |||
66ba60dc8e | |||
8fcc8cdd17 | |||
bba9bdc563 | |||
760a180f56 | |||
0eeff8cb45 | |||
3c76721159 | |||
6ac79ae645 | |||
90d7c33c92 | |||
e02bc76bdb | |||
0466f04d82 | |||
f8ed830b60 | |||
b7ca91bf6d | |||
2261f92b0b | |||
bbe9e621b1 | |||
bf087d760b | |||
a4353c63fd | |||
c438d11c3d | |||
43284e33c8 | |||
77dce7f0dd | |||
d559862051 | |||
7382ed87ba | |||
3324ed31de | |||
ba9dbc2205 | |||
b0028d4874 | |||
9e6be9651c | |||
b145ab0106 | |||
84e0fbb70f | |||
990605bbea | |||
d075c4ad13 | |||
a3f41f5519 | |||
cb173e2850 | |||
87cd3b67f4 | |||
fe3ee3806e | |||
c9ed6c724c | |||
a5fa259d55 | |||
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
8071eb6f8a | |||
11f789e980 | |||
3f82522fe9 | |||
c5cb0e2fd4 | |||
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c | |||
3da6fc3b7e | |||
34dd15ead7 | |||
b3d441e9d6 | |||
4b3dc3756c | |||
10027b98b5 | |||
da17dad63b | |||
fba6c422a8 | |||
0b4b93932d | |||
f42900ec46 | |||
eeca624ba6 | |||
84d08bad16 | |||
1181f33e9d | |||
797e200d08 | |||
d2f231066b | |||
86d40c964f | |||
2604449239 | |||
e992a96d3b | |||
22c4406105 | |||
ad3f0fda6a |
@ -1,7 +1,6 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
||||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
|
||||||
VITE_KC_SENTRY_DSN=
|
VITE_KC_SENTRY_DSN=
|
||||||
|
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.kittycad.io
|
|||||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
|
|
||||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||||
|
@ -1 +1 @@
|
|||||||
src/wasm-lib/pkg/wasm_lib.js
|
src/wasm-lib/*
|
||||||
|
11
.github/workflows/cargo-clippy.yml
vendored
11
.github/workflows/cargo-clippy.yml
vendored
@ -40,6 +40,17 @@ jobs:
|
|||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
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
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
|
10
.github/workflows/cargo-test.yml
vendored
10
.github/workflows/cargo-test.yml
vendored
@ -41,6 +41,16 @@ jobs:
|
|||||||
- uses: taiki-e/install-action@nextest
|
- uses: taiki-e/install-action@nextest
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
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
|
- name: cargo test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
|
162
.github/workflows/ci.yml
vendored
162
.github/workflows/ci.yml
vendored
@ -13,17 +13,31 @@ jobs:
|
|||||||
check-format:
|
check-format:
|
||||||
runs-on: 'ubuntu-20.04'
|
runs-on: 'ubuntu-20.04'
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- run: yarn fmt-check
|
- 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:
|
build-test-web:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@ -36,12 +50,15 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- run: yarn install
|
- 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
|
- run: yarn simpleserver:ci
|
||||||
|
|
||||||
@ -49,14 +66,12 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn test:cov
|
- run: yarn test:cov
|
||||||
|
|
||||||
- run: yarn test:rust
|
|
||||||
|
|
||||||
- id: export_version
|
- id: export_version
|
||||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
build-apps:
|
build-apps:
|
||||||
needs: [check-format, build-test-web]
|
needs: [check-format, build-test-web, check-types]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -87,6 +102,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: './src-tauri -> target'
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: "./src/wasm-lib"
|
||||||
|
|
||||||
- name: wasm prep
|
- name: wasm prep
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@ -110,22 +129,27 @@ jobs:
|
|||||||
- name: Fix format
|
- name: Fix format
|
||||||
run: yarn fmt
|
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)
|
- name: Build the app for the current platform (no upload)
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
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:
|
sign-windows-msi:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: windows-latest
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
needs: [build-test-web, build-apps]
|
needs: [build-test-web, build-apps]
|
||||||
env:
|
env:
|
||||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||||
@ -133,29 +157,89 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- 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
|
- name: Generate the update static endpoint
|
||||||
run: |
|
run: |
|
||||||
ls -l artifact
|
ls -l artifact/*/*itty*
|
||||||
ls -l artifact/*
|
|
||||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.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}
|
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "v${VERSION_NO_V}" \
|
--arg version "v${VERSION_NO_V}" \
|
||||||
|
--arg pub_date "${PUB_DATE}" \
|
||||||
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_sig "$DARWIN_SIG" \
|
--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_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_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,
|
"version": $version,
|
||||||
|
"pub_date": $pub_date,
|
||||||
|
"notes": $notes,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"darwin-x86_64": {
|
"darwin-x86_64": {
|
||||||
"signature": $darwin_sig,
|
"signature": $darwin_sig,
|
||||||
"url": $darwin_url
|
"url": $darwin_url
|
||||||
},
|
},
|
||||||
|
"darwin-aarch64": {
|
||||||
|
"signature": $darwin_sig,
|
||||||
|
"url": $darwin_url
|
||||||
|
},
|
||||||
"linux-x86_64": {
|
"linux-x86_64": {
|
||||||
"signature": $linux_sig,
|
"signature": $linux_sig,
|
||||||
"url": $linux_url
|
"url": $linux_url
|
||||||
@ -168,6 +252,34 @@ jobs:
|
|||||||
}' > last_update.json
|
}' > last_update.json
|
||||||
cat 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
|
- name: Authenticate to Google Cloud
|
||||||
uses: 'google-github-actions/auth@v1.1.1'
|
uses: 'google-github-actions/auth@v1.1.1'
|
||||||
with:
|
with:
|
||||||
@ -182,7 +294,7 @@ jobs:
|
|||||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||||
with:
|
with:
|
||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/kittycad-modeling-app*'
|
glob: '*/*itty*'
|
||||||
parent: false
|
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 }}
|
||||||
|
|
||||||
@ -192,7 +304,13 @@ jobs:
|
|||||||
path: last_update.json
|
path: last_update.json
|
||||||
destination: dl.kittycad.io/releases/modeling-app
|
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
|
- name: Upload release files to Github
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: artifact/*/kittycad-modeling-app*
|
files: artifact/*/*itty*
|
||||||
|
@ -5,3 +5,5 @@ coverage
|
|||||||
# Ignore Rust projects:
|
# Ignore Rust projects:
|
||||||
*.rs
|
*.rs
|
||||||
target
|
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/)
|
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
|
yarn install
|
||||||
```
|
```
|
||||||
then
|
|
||||||
|
followed by:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn build:wasm
|
yarn build:wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||||
|
|
||||||
finally
|
finally, to run the web app only, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
|
|
||||||
```
|
|
||||||
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
|
## Developing in Chrome
|
||||||
|
|
||||||
Chrome is in the process of rolling out a new default which
|
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
|
the eye with a slash through it in the URL bar, and clicking on "Enable
|
||||||
Third-Party Cookies".
|
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
|
## Tauri
|
||||||
|
|
||||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn tauri dev
|
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.
|
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.)
|
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 😉
|
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
|
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
|
||||||
|
|
||||||
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
|
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
|
||||||
|
|
||||||
|
## Before submitting a PR
|
||||||
|
|
||||||
|
Before you submit a contribution PR to this repo, please ensure that:
|
||||||
|
|
||||||
|
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
|
||||||
|
- You have separated out refactoring commits from feature commits as much as possible
|
||||||
|
- You have run all of the following commands locally:
|
||||||
|
- `yarn fmt`
|
||||||
|
- `yarn tsc`
|
||||||
|
- `yarn test`
|
||||||
|
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
|
||||||
|
|
||||||
## Release a new version
|
## 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
|
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
|
```bash
|
||||||
VERSION=x.y.z yarn run bump-jsons
|
VERSION=x.y.z yarn run bump-jsons
|
||||||
```
|
```
|
||||||
|
|
||||||
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||||
|
|
||||||
2. Merge the PR
|
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}`
|
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
|
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).
|
||||||
|
File diff suppressed because it is too large
Load Diff
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).
|
18
package.json
18
package.json
@ -1,31 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.3.0",
|
"version": "0.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.35",
|
"@kittycad/lib": "^0.0.37",
|
||||||
|
"@lezer/javascript": "^1.4.7",
|
||||||
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
"@sentry/react": "^7.65.0",
|
"@sentry/react": "^7.65.0",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
"@uiw/react-codemirror": "^4.21.13",
|
||||||
"@uiw/react-codemirror": "^4.15.1",
|
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -43,6 +48,8 @@
|
|||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vitest": "^0.34.1",
|
"vitest": "^0.34.1",
|
||||||
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.3",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
@ -64,7 +71,7 @@
|
|||||||
"fmt": "prettier --write ./src",
|
"fmt": "prettier --write ./src",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src",
|
||||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
||||||
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
"remove-importmeta": "sed -i 's/import.meta.url/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",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
||||||
@ -92,6 +99,7 @@
|
|||||||
"@babel/preset-env": "^7.22.9",
|
"@babel/preset-env": "^7.22.9",
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/debounce": "^1.2.1",
|
||||||
"@types/isomorphic-fetch": "^0.0.36",
|
"@types/isomorphic-fetch": "^0.0.36",
|
||||||
"@types/react-modal": "^3.16.0",
|
"@types/react-modal": "^3.16.0",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
|
42
public/expectations.md
Normal file
42
public/expectations.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## Alpha Users Expectations
|
||||||
|
|
||||||
|
### Welcome
|
||||||
|
|
||||||
|
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
|
||||||
|
|
||||||
|
### KittyCAD Modeling App (KCMA)
|
||||||
|
|
||||||
|
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
|
||||||
|
|
||||||
|
### Why Code?
|
||||||
|
|
||||||
|
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isn’t as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
|
||||||
|
|
||||||
|
- Automation and parametric design
|
||||||
|
- Customization and flexibility
|
||||||
|
- Algorithmic and generative design
|
||||||
|
- Reproducibility
|
||||||
|
- Easier integration with other tools
|
||||||
|
|
||||||
|
### Before You Use KCMA
|
||||||
|
|
||||||
|
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
||||||
|
|
||||||
|
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
|
||||||
|
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
|
||||||
|
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure it’s not a duplicate or it’s feasible for the current state of the app.
|
||||||
|
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
|
||||||
|
- **High:** The bug is blocking you from continuing.
|
||||||
|
- Example: Every time I click the extrude button with two faces selected, the app crashes.
|
||||||
|
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
|
||||||
|
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
|
||||||
|
- **Low:** The bug is annoying but doesn’t affect workflow or block you from continuing (usually you can say “It would be nice if ___, but it’s not needed”)
|
||||||
|
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
|
||||||
|
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
|
||||||
|
|
||||||
|
### Thank You!
|
||||||
|
|
||||||
|
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!
|
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)
|
||||||
|
|
945
src-tauri/Cargo.lock
generated
945
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -16,13 +16,14 @@ tauri-build = { version = "1.4.0", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
oauth2 = "4.4.1"
|
kittycad = "0.2.25"
|
||||||
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
|
||||||
tokio = { version = "1.29.1", features = ["time"] }
|
|
||||||
toml = "0.6.0"
|
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
tokio = { version = "1.32.0", features = ["time"] }
|
||||||
|
toml = "0.8.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
@ -85,6 +85,24 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///This command returns the KittyCAD user info given a token.
|
||||||
|
/// The string returned from this method is the user info as a json string.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_user(token: Option<String>) -> Result<kittycad::types::User, InvokeError> {
|
||||||
|
println!("Getting user info...");
|
||||||
|
|
||||||
|
// use kittycad library to fetch the user info from /user/me
|
||||||
|
let client = kittycad::Client::new(token.unwrap());
|
||||||
|
|
||||||
|
let user_info: kittycad::types::User = client
|
||||||
|
.users()
|
||||||
|
.get_self()
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
|
||||||
|
Ok(user_info)
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
@ -97,7 +115,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_user,
|
||||||
|
login,
|
||||||
|
read_toml,
|
||||||
|
read_txt_file
|
||||||
|
])
|
||||||
.plugin(tauri_plugin_fs_extra::init())
|
.plugin(tauri_plugin_fs_extra::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "kittycad-modeling-app",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.3.0"
|
"version": "0.7.1"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"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"
|
||||||
|
}
|
||||||
|
}
|
452
src/App.tsx
452
src/App.tsx
@ -1,37 +1,17 @@
|
|||||||
import {
|
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useCallback,
|
|
||||||
MouseEventHandler,
|
|
||||||
} from 'react'
|
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { asyncParser } from './lang/abstractSyntaxTree'
|
|
||||||
import { _executor } from './lang/executor'
|
import { _executor } from './lang/executor'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import { PaneType, useStore } from './useStore'
|
||||||
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, useStore } from './useStore'
|
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
import { MemoryPanel } from './components/MemoryPanel'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
import { Stream } from './components/Stream'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
import {
|
import { EngineCommand } from './lang/std/engineConnection'
|
||||||
EngineCommand,
|
import { throttle } from './lib/utils'
|
||||||
EngineCommandManager,
|
|
||||||
} from './lang/std/engineConnection'
|
|
||||||
import { isOverlap, throttle } from './lib/utils'
|
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
|
||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import {
|
import {
|
||||||
faCode,
|
faCode,
|
||||||
@ -39,93 +19,44 @@ import {
|
|||||||
faSquareRootVariable,
|
faSquareRootVariable,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TEST } from './env'
|
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { Themes, getSystemTheme } from './lib/theme'
|
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
import { useLoaderData, useParams } from 'react-router-dom'
|
import { useLoaderData } from 'react-router-dom'
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
|
||||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding'
|
import { onboardingPaths } from 'routes/Onboarding'
|
||||||
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import { CodeMenu } from 'components/CodeMenu'
|
||||||
|
import { TextEditor } from 'components/TextEditor'
|
||||||
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
const pathParams = useParams()
|
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
editorView,
|
|
||||||
setEditorView,
|
|
||||||
setSelectionRanges,
|
|
||||||
selectionRanges,
|
|
||||||
addLog,
|
|
||||||
addKCLError,
|
|
||||||
code,
|
|
||||||
setCode,
|
setCode,
|
||||||
setAst,
|
|
||||||
setError,
|
|
||||||
setProgramMemory,
|
|
||||||
resetLogs,
|
|
||||||
resetKCLErrors,
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
setArtifactMap,
|
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
setEngineCommandManager,
|
buttonDownInStream,
|
||||||
setHighlightRange,
|
|
||||||
setCursor2,
|
|
||||||
sourceRangeMap,
|
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
isStreamReady,
|
|
||||||
isMouseDownInStream,
|
|
||||||
cmdId,
|
|
||||||
setCmdId,
|
|
||||||
formatCode,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
|
||||||
setStreamDimensions,
|
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
guiMode: s.guiMode,
|
||||||
setEditorView: s.setEditorView,
|
|
||||||
setSelectionRanges: s.setSelectionRanges,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
setGuiMode: s.setGuiMode,
|
setGuiMode: s.setGuiMode,
|
||||||
addLog: s.addLog,
|
|
||||||
code: s.code,
|
|
||||||
setCode: s.setCode,
|
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,
|
engineCommandManager: s.engineCommandManager,
|
||||||
setEngineCommandManager: s.setEngineCommandManager,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
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,
|
|
||||||
addKCLError: s.addKCLError,
|
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -134,7 +65,7 @@ export function App() {
|
|||||||
context: { token },
|
context: { token },
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
context: { showDebugPanel, theme, onboardingStatus },
|
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||||
},
|
},
|
||||||
} = useGlobalStateContext()
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
@ -153,6 +84,38 @@ export function App() {
|
|||||||
useHotkeys('shift + l', () => togglePane('logs'))
|
useHotkeys('shift + l', () => togglePane('logs'))
|
||||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||||
useHotkeys('shift + d', () => togglePane('debug'))
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
|
useHotkeys('esc', () => {
|
||||||
|
if (guiMode.mode === 'sketch') {
|
||||||
|
if (guiMode.sketchMode === 'selectFace') return
|
||||||
|
if (guiMode.sketchMode === 'sketchEdit') {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
} else {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'set_tool',
|
||||||
|
tool: 'select',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
rotation: guiMode.rotation,
|
||||||
|
position: guiMode.position,
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const paneOpacity =
|
const paneOpacity =
|
||||||
onboardingStatus === onboardingPaths.CAMERA
|
onboardingStatus === onboardingPaths.CAMERA
|
||||||
@ -175,238 +138,35 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}, [loadedCode, setCode])
|
}, [loadedCode, setCode])
|
||||||
|
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
useSetupEngineManager(streamRef, token)
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
useEngineConnectionSubscriptions()
|
||||||
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 : 0
|
|
||||||
const quadWidth = Math.round(width / 4) * 4
|
|
||||||
const height = streamHeight ? streamHeight : 0
|
|
||||||
const quadHeight = Math.round(height / 4) * 4
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setStreamDimensions({
|
|
||||||
streamWidth: quadWidth,
|
|
||||||
streamHeight: quadHeight,
|
|
||||||
})
|
|
||||||
if (!width || !height) return
|
|
||||||
const eng = new EngineCommandManager({
|
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
width: quadWidth,
|
|
||||||
height: quadHeight,
|
|
||||||
token,
|
|
||||||
})
|
|
||||||
setEngineCommandManager(eng)
|
|
||||||
return () => {
|
|
||||||
eng?.tearDown()
|
|
||||||
}
|
|
||||||
}, [quadWidth, quadHeight])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isStreamReady) return
|
|
||||||
if (!engineCommandManager) return
|
|
||||||
let unsubFn: any[] = []
|
|
||||||
const asyncWrap = async () => {
|
|
||||||
try {
|
|
||||||
if (!code) {
|
|
||||||
setAst(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const _ast = await asyncParser(code)
|
|
||||||
setAst(_ast)
|
|
||||||
resetLogs()
|
|
||||||
resetKCLErrors()
|
|
||||||
engineCommandManager.endSession()
|
|
||||||
engineCommandManager.startNewSession()
|
|
||||||
const programMemory = await _executor(
|
|
||||||
_ast,
|
|
||||||
{
|
|
||||||
root: {
|
|
||||||
_0: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 0,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_90: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 90,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_180: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 180,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
_270: {
|
|
||||||
type: 'userVal',
|
|
||||||
value: 270,
|
|
||||||
__meta: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engineCommandManager
|
|
||||||
)
|
|
||||||
|
|
||||||
const { artifactMap, sourceRangeMap } =
|
|
||||||
await engineCommandManager.waitForAllCommands()
|
|
||||||
|
|
||||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
|
||||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
|
||||||
event: 'highlight_set_entity',
|
|
||||||
callback: ({ data }) => {
|
|
||||||
if (!data?.entity_id) {
|
|
||||||
setHighlightRange([0, 0])
|
|
||||||
} else {
|
|
||||||
const sourceRange = sourceRangeMap[data.entity_id]
|
|
||||||
setHighlightRange(sourceRange)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
|
||||||
event: 'select_with_point',
|
|
||||||
callback: ({ data }) => {
|
|
||||||
if (!data?.entity_id) {
|
|
||||||
setCursor2()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sourceRange = sourceRangeMap[data.entity_id]
|
|
||||||
setCursor2({ range: sourceRange, type: 'default' })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
unsubFn.push(unSubHover, unSubClick)
|
|
||||||
if (programMemory !== undefined) {
|
|
||||||
setProgramMemory(programMemory)
|
|
||||||
}
|
|
||||||
|
|
||||||
setError()
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof KCLError) {
|
|
||||||
addKCLError(e)
|
|
||||||
} else {
|
|
||||||
setError('problem')
|
|
||||||
console.log(e)
|
|
||||||
addLog(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncWrap()
|
|
||||||
return () => {
|
|
||||||
unsubFn.forEach((fn) => fn())
|
|
||||||
}
|
|
||||||
}, [code, isStreamReady, engineCommandManager])
|
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager?.sendSceneCommand(message)
|
engineCommandManager?.sendSceneCommand(message)
|
||||||
}, 16)
|
}, 16)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
clientX,
|
e.nativeEvent.preventDefault()
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
shiftKey,
|
|
||||||
currentTarget,
|
|
||||||
nativeEvent,
|
|
||||||
}) => {
|
|
||||||
nativeEvent.preventDefault()
|
|
||||||
if (isMouseDownInStream) {
|
|
||||||
setDidDragInStream(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX: e.clientX,
|
||||||
clientY,
|
clientY: e.clientY,
|
||||||
el: currentTarget,
|
el: e.currentTarget,
|
||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
setCmdId(newCmdId)
|
if (buttonDownInStream === undefined) {
|
||||||
|
if (
|
||||||
if (cmdId && isMouseDownInStream) {
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any)
|
||||||
|
) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: newCmdId,
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_move',
|
type: 'mouse_move',
|
||||||
interaction,
|
|
||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
@ -418,17 +178,45 @@ export function App() {
|
|||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||||
|
debounceSocketSend({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: newCmdId,
|
||||||
|
cmd: {
|
||||||
|
type: 'handle_mouse_drag_move',
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||||
|
let interaction: CameraDragInteractionType_type
|
||||||
|
|
||||||
|
const eWithButton = { ...e, button: buttonDownInStream }
|
||||||
|
|
||||||
|
if (interactionGuards.pan.callback(eWithButton)) {
|
||||||
|
interaction = 'pan'
|
||||||
|
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||||
|
interaction = 'rotate'
|
||||||
|
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||||
|
interaction = 'zoom'
|
||||||
|
} else {
|
||||||
|
console.log('none')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraExtensions = useMemo(() => {
|
debounceSocketSend({
|
||||||
if (TEST) return []
|
type: 'modeling_cmd_req',
|
||||||
return [
|
cmd: {
|
||||||
lintGutter(),
|
type: 'camera_drag_move',
|
||||||
linter((_view) => {
|
interaction,
|
||||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
window: { x, y },
|
||||||
}),
|
},
|
||||||
]
|
cmd_id: newCmdId,
|
||||||
}, [])
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -440,7 +228,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={project}
|
project={project}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
@ -449,17 +237,17 @@ export function App() {
|
|||||||
<Resizable
|
<Resizable
|
||||||
className={
|
className={
|
||||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
'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 '
|
? ' pointer-events-none '
|
||||||
: ' ') +
|
: ' ') +
|
||||||
paneOpacity
|
paneOpacity
|
||||||
}
|
}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '400px',
|
width: '550px',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
}}
|
}}
|
||||||
minWidth={200}
|
minWidth={200}
|
||||||
maxWidth={600}
|
maxWidth={800}
|
||||||
minHeight={'auto'}
|
minHeight={'auto'}
|
||||||
maxHeight={'auto'}
|
maxHeight={'auto'}
|
||||||
handleClasses={{
|
handleClasses={{
|
||||||
@ -473,31 +261,9 @@ export function App() {
|
|||||||
icon={faCode}
|
icon={faCode}
|
||||||
className="open:!mb-2"
|
className="open:!mb-2"
|
||||||
open={openPanes.includes('code')}
|
open={openPanes.includes('code')}
|
||||||
|
menu={<CodeMenu />}
|
||||||
>
|
>
|
||||||
<div className="px-2 py-1">
|
<TextEditor theme={editorTheme} />
|
||||||
<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>
|
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<MemoryPanel
|
<MemoryPanel
|
||||||
@ -528,7 +294,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
open={openPanes.includes('debug')}
|
open={openPanes.includes('debug')}
|
||||||
/>
|
/>
|
||||||
|
@ -47,6 +47,15 @@
|
|||||||
@apply hover:bg-cool-20;
|
@apply hover:bg-cool-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smallScrollbar::-webkit-scrollbar {
|
||||||
|
@apply h-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallScrollbar {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.dark) .popoverToggle {
|
:global(.dark) .popoverToggle {
|
||||||
@apply hover:bg-cool-90;
|
@apply hover:bg-cool-90;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useStore, toolTips } from './useStore'
|
import { useStore, toolTips, Selections } from './useStore'
|
||||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||||
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
|||||||
import { Intersect } from './components/Toolbar/Intersect'
|
import { Intersect } from './components/Toolbar/Intersect'
|
||||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
@ -16,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import styles from './Toolbar.module.css'
|
import styles from './Toolbar.module.css'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useAppMode } from 'hooks/useAppMode'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const {
|
const {
|
||||||
@ -25,6 +26,8 @@ export const Toolbar = () => {
|
|||||||
ast,
|
ast,
|
||||||
updateAst,
|
updateAst,
|
||||||
programMemory,
|
programMemory,
|
||||||
|
engineCommandManager,
|
||||||
|
executeAst,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
guiMode: s.guiMode,
|
guiMode: s.guiMode,
|
||||||
setGuiMode: s.setGuiMode,
|
setGuiMode: s.setGuiMode,
|
||||||
@ -32,7 +35,10 @@ export const Toolbar = () => {
|
|||||||
ast: s.ast,
|
ast: s.ast,
|
||||||
updateAst: s.updateAst,
|
updateAst: s.updateAst,
|
||||||
programMemory: s.programMemory,
|
programMemory: s.programMemory,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
executeAst: s.executeAst,
|
||||||
}))
|
}))
|
||||||
|
useAppMode()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('guiMode', guiMode)
|
console.log('guiMode', guiMode)
|
||||||
@ -40,7 +46,7 @@ export const Toolbar = () => {
|
|||||||
|
|
||||||
function ToolbarButtons() {
|
function ToolbarButtons() {
|
||||||
return (
|
return (
|
||||||
<>
|
<span className={styles.smallScrollbar}>
|
||||||
{guiMode.mode === 'default' && (
|
{guiMode.mode === 'default' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -66,15 +72,24 @@ export const Toolbar = () => {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
programMemory
|
programMemory
|
||||||
)
|
)
|
||||||
updateAst(modifiedAst)
|
updateAst(modifiedAst, true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
SketchOnFace
|
SketchOnFace
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
{guiMode.mode === 'canEditSketch' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
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({
|
setGuiMode({
|
||||||
mode: 'sketch',
|
mode: 'sketch',
|
||||||
sketchMode: 'sketchEdit',
|
sketchMode: 'sketchEdit',
|
||||||
@ -100,7 +115,7 @@ export const Toolbar = () => {
|
|||||||
ast,
|
ast,
|
||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ExtrudeSketch
|
ExtrudeSketch
|
||||||
@ -117,7 +132,7 @@ export const Toolbar = () => {
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
|
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
ExtrudeSketch (w/o pipe)
|
ExtrudeSketch (w/o pipe)
|
||||||
@ -126,14 +141,30 @@ export const Toolbar = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{guiMode.mode === 'sketch' && (
|
{guiMode.mode === 'sketch' && (
|
||||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
|
||||||
|
setGuiMode({ mode: 'default' })
|
||||||
|
executeAst()
|
||||||
|
}}
|
||||||
|
>
|
||||||
Exit sketch
|
Exit sketch
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{toolTips
|
{toolTips
|
||||||
.filter(
|
.filter(
|
||||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||||
)
|
)
|
||||||
.map((sketchFnName) => {
|
.map((sketchFnName) => {
|
||||||
if (
|
if (
|
||||||
@ -144,7 +175,18 @@ export const Toolbar = () => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={sketchFnName}
|
key={sketchFnName}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'set_tool',
|
||||||
|
tool:
|
||||||
|
guiMode.sketchMode === sketchFnName
|
||||||
|
? 'select'
|
||||||
|
: (sketchFnName as any),
|
||||||
|
},
|
||||||
|
})
|
||||||
setGuiMode({
|
setGuiMode({
|
||||||
...guiMode,
|
...guiMode,
|
||||||
...(guiMode.sketchMode === sketchFnName
|
...(guiMode.sketchMode === sketchFnName
|
||||||
@ -154,17 +196,17 @@ export const Toolbar = () => {
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
sketchMode: sketchFnName,
|
sketchMode: sketchFnName,
|
||||||
|
waitingFirstClick: true,
|
||||||
isTooltip: true,
|
isTooltip: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
{sketchFnName}
|
{sketchFnName}
|
||||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<ConvertToVariable />
|
|
||||||
<HorzVert horOrVert="horizontal" />
|
<HorzVert horOrVert="horizontal" />
|
||||||
<HorzVert horOrVert="vertical" />
|
<HorzVert horOrVert="vertical" />
|
||||||
<EqualLength />
|
<EqualLength />
|
||||||
@ -182,7 +224,7 @@ export const Toolbar = () => {
|
|||||||
<Intersect />
|
<Intersect />
|
||||||
<RemoveConstrainingValues />
|
<RemoveConstrainingValues />
|
||||||
<SetAngleBetween />
|
<SetAngleBetween />
|
||||||
</>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
|
|||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
|
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -43,7 +44,8 @@ export const AppHeader = ({
|
|||||||
)}
|
)}
|
||||||
{/* If there are children, show them, otherwise show User menu */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || (
|
{children || (
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<NetworkHealthIndicator />
|
||||||
<UserSidebarMenu user={user} />
|
<UserSidebarMenu user={user} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
182
src/components/AstExplorer.tsx
Normal file
182
src/components/AstExplorer.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
|
export function AstExplorer() {
|
||||||
|
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
||||||
|
ast: s.ast,
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
}))
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections?.[0]?.range
|
||||||
|
)
|
||||||
|
const node = getNodeFromPath(ast, pathToNode).node
|
||||||
|
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" style={{ width: '300px' }}>
|
||||||
|
<div className="">
|
||||||
|
filter out keys:<div className="w-2 inline-block"></div>
|
||||||
|
{['start', 'end', 'type'].map((key) => {
|
||||||
|
return (
|
||||||
|
<label key={key} className="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-checkbox"
|
||||||
|
checked={filterKeys.includes(key)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (filterKeys.includes(key)) {
|
||||||
|
setFilterKeys(filterKeys.filter((k) => k !== key))
|
||||||
|
} else {
|
||||||
|
setFilterKeys([...filterKeys, key])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="mr-2">{key}</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="h-full relative"
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
setHighlightRange([0, 0])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||||
|
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayBody({
|
||||||
|
body,
|
||||||
|
filterKeys,
|
||||||
|
node,
|
||||||
|
}: {
|
||||||
|
body: { start: number; end: number; [key: string]: any }[]
|
||||||
|
filterKeys: string[]
|
||||||
|
node: any
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{body.map((b, index) => {
|
||||||
|
return (
|
||||||
|
<div className="my-2" key={index}>
|
||||||
|
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayObj({
|
||||||
|
obj,
|
||||||
|
filterKeys,
|
||||||
|
node,
|
||||||
|
}: {
|
||||||
|
obj: { start: number; end: number; [key: string]: any }
|
||||||
|
filterKeys: string[]
|
||||||
|
node: any
|
||||||
|
}) {
|
||||||
|
const { setHighlightRange, setCursor2 } = useStore((s) => ({
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
setCursor2: s.setCursor2,
|
||||||
|
}))
|
||||||
|
const ref = useRef<HTMLPreElement>(null)
|
||||||
|
const [hasCursor, setHasCursor] = useState(false)
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
node?.start === obj?.start &&
|
||||||
|
node?.end === obj?.end &&
|
||||||
|
node.type === obj?.type
|
||||||
|
) {
|
||||||
|
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
|
||||||
|
setHasCursor(true)
|
||||||
|
} else {
|
||||||
|
setHasCursor(false)
|
||||||
|
}
|
||||||
|
}, [node.start, node.end, node.type])
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
ref={ref}
|
||||||
|
className={`ml-2 border-l border-violet-600 pl-1 ${
|
||||||
|
hasCursor ? 'bg-violet-100/25' : ''
|
||||||
|
}`}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setHighlightRange([obj?.start || 0, obj.end])
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<button
|
||||||
|
className="m-0 p-0 border-0"
|
||||||
|
onClick={() => setIsCollapsed(false)}
|
||||||
|
>
|
||||||
|
{'>'}type: {obj.type}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="flex">
|
||||||
|
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
|
||||||
|
<ul className="inline-block">
|
||||||
|
{Object.entries(obj).map(([key, value]) => {
|
||||||
|
if (filterKeys.includes(key)) {
|
||||||
|
return null
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{`${key}: [`}
|
||||||
|
<DisplayBody
|
||||||
|
body={value}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
{']'}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
value?.end
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{key}:
|
||||||
|
<DisplayObj
|
||||||
|
obj={value}
|
||||||
|
filterKeys={filterKeys}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'number'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{key}: {value}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
@ -144,7 +144,7 @@ export function useCalc({
|
|||||||
try {
|
try {
|
||||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
const _programMem: any = { root: {} }
|
const _programMem: any = { root: {}, return: null }
|
||||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||||
})
|
})
|
||||||
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
shouldCreateVariable,
|
shouldCreateVariable,
|
||||||
setShouldCreateVariable,
|
setShouldCreateVariable = () => {},
|
||||||
showCheckbox = true,
|
showCheckbox = true,
|
||||||
}: {
|
}: {
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
shouldCreateVariable: boolean
|
shouldCreateVariable?: boolean
|
||||||
setShouldCreateVariable: (a: boolean) => void
|
setShouldCreateVariable?: (a: boolean) => void
|
||||||
showCheckbox?: boolean
|
showCheckbox?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label
|
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||||
htmlFor="create-new-variable"
|
|
||||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
|
||||||
>
|
|
||||||
Create new variable
|
Create new variable
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex flex-1">
|
<div className="mt-1 flex gap-2 items-center">
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
|
|
||||||
checked={shouldCreateVariable}
|
checked={shouldCreateVariable}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setShouldCreateVariable(e.target.checked)
|
setShouldCreateVariable(e.target.checked)
|
||||||
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
|
|||||||
disabled={!shouldCreateVariable}
|
disabled={!shouldCreateVariable}
|
||||||
name="create-new-variable"
|
name="create-new-variable"
|
||||||
id="create-new-variable"
|
id="create-new-variable"
|
||||||
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
|
autoFocus={true}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
|
||||||
!shouldCreateVariable ? 'opacity-50' : ''
|
!shouldCreateVariable ? 'opacity-50' : ''
|
||||||
}`}
|
}`}
|
||||||
value={newVariableName}
|
value={newVariableName}
|
||||||
|
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;
|
||||||
|
}
|
81
src/components/CodeMenu.tsx
Normal file
81
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Menu } from '@headlessui/react'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faEllipsis,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
import styles from './CodeMenu.module.css'
|
||||||
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
|
import { editorShortcutMeta } from './TextEditor'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
const { formatCode } = useStore((s) => ({
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
}))
|
||||||
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
|
useConvertToVariable()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
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.Item>
|
||||||
|
<a
|
||||||
|
className={styles.button}
|
||||||
|
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span>Read the KCL docs</span>
|
||||||
|
<small>
|
||||||
|
On GitHub
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1 align-text-top"
|
||||||
|
width={12}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
.panel {
|
.panel {
|
||||||
@apply relative overflow-auto z-0;
|
@apply relative z-0;
|
||||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
@apply sticky top-0 z-10 cursor-pointer;
|
@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 font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||||
@apply bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
|||||||
title: string
|
title: string
|
||||||
icon?: IconDefinition
|
icon?: IconDefinition
|
||||||
open?: boolean
|
open?: boolean
|
||||||
|
menu?: React.ReactNode
|
||||||
iconClassNames?: {
|
iconClassNames?: {
|
||||||
bg?: string
|
bg?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@ -18,9 +19,11 @@ export const PanelHeader = ({
|
|||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
|
<div className="flex gap-2 align-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
bgClassName={
|
bgClassName={
|
||||||
@ -33,6 +36,10 @@ export const PanelHeader = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{title}
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
...props
|
...props
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
className={styles.panel + ' group ' + (className || '')}
|
||||||
>
|
>
|
||||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
<PanelHeader
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
iconClassNames={iconClassNames}
|
||||||
|
menu={menu}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
|
@ -196,7 +196,7 @@ const CommandBar = () => {
|
|||||||
setCommandBarOpen(false)
|
setCommandBarOpen(false)
|
||||||
clearState()
|
clearState()
|
||||||
}}
|
}}
|
||||||
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
|
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||||
>
|
>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
enter="duration-100 ease-out"
|
enter="duration-100 ease-out"
|
||||||
@ -207,7 +207,7 @@ const CommandBar = () => {
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
enter="duration-100 ease-out"
|
enter="duration-100 ease-out"
|
||||||
@ -221,7 +221,7 @@ const CommandBar = () => {
|
|||||||
<Combobox
|
<Combobox
|
||||||
value={selectedCommand}
|
value={selectedCommand}
|
||||||
onChange={handleCommandSelection}
|
onChange={handleCommandSelection}
|
||||||
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
className="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"
|
as="div"
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
@ -6,6 +6,7 @@ import { useState } from 'react'
|
|||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { isReducedMotion } from 'lang/util'
|
import { isReducedMotion } from 'lang/util'
|
||||||
|
import { AstExplorer } from './AstExplorer'
|
||||||
|
|
||||||
type SketchModeCmd = Extract<
|
type SketchModeCmd = Extract<
|
||||||
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
|
||||||
@ -94,6 +95,9 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
|||||||
>
|
>
|
||||||
Send sketch mode command
|
Send sketch mode command
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||||
|
<AstExplorer />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
)
|
)
|
||||||
|
@ -39,7 +39,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
const initialValues: OutputFormat = {
|
const initialValues: OutputFormat = {
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
storage: 'embedded',
|
storage: 'embedded',
|
||||||
presentation: 'compact',
|
presentation: 'pretty',
|
||||||
}
|
}
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues,
|
initialValues,
|
||||||
@ -83,8 +83,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const yo = formik.values
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -24,6 +24,9 @@ import {
|
|||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { invoke } from '@tauri-apps/api'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -108,6 +111,7 @@ export const GlobalStateProvider = ({
|
|||||||
actions: {
|
actions: {
|
||||||
goToSignInPage: () => {
|
goToSignInPage: () => {
|
||||||
navigate(paths.SIGN_IN)
|
navigate(paths.SIGN_IN)
|
||||||
|
|
||||||
logout()
|
logout()
|
||||||
},
|
},
|
||||||
goToIndexPage: () => {
|
goToIndexPage: () => {
|
||||||
@ -149,10 +153,12 @@ export const GlobalStateProvider = ({
|
|||||||
export default GlobalStateProvider
|
export default GlobalStateProvider
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {
|
||||||
const url = withBaseUrl('/logout')
|
|
||||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
return fetch(url, {
|
return (
|
||||||
|
!isTauri() &&
|
||||||
|
fetch(withBaseUrl('/logout'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ describe('processMemory', () => {
|
|||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const code = `
|
const code = `
|
||||||
const myVar = 5
|
const myVar = 5
|
||||||
const myFn = (a) => {
|
fn myFn = (a) => {
|
||||||
return a - 2
|
return a - 2
|
||||||
}
|
}
|
||||||
const otherVar = myFn(5)
|
const otherVar = myFn(5)
|
||||||
@ -29,6 +29,7 @@ describe('processMemory', () => {
|
|||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast, {
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
root: {},
|
root: {},
|
||||||
|
return: null,
|
||||||
})
|
})
|
||||||
const output = processMemory(programMemory)
|
const output = processMemory(programMemory)
|
||||||
expect(output.myVar).toEqual(5)
|
expect(output.myVar).toEqual(5)
|
||||||
|
@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
|
|||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory } from '../lang/executor'
|
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||||
import { Themes } from '../lib/theme'
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
Object.keys(programMemory.root).forEach((key) => {
|
Object.keys(programMemory.root).forEach((key) => {
|
||||||
const val = programMemory.root[key]
|
const val = programMemory.root[key]
|
||||||
if (typeof val.value !== 'function') {
|
if (typeof val.value !== 'function') {
|
||||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
if (val.type === 'SketchGroup') {
|
||||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
} else if (val.type === 'ExtrudeGroup') {
|
||||||
|
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||||
return rest
|
return rest
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
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,6 +1,9 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
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 = ({
|
export const SetVarNameModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -19,21 +22,23 @@ export const SetVarNameModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<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
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0 translate-y-4"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-75"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
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>
|
</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
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@ -43,43 +48,39 @@ export const SetVarNameModal = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.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">
|
||||||
<Dialog.Title
|
<form
|
||||||
as="h3"
|
onSubmit={(e) => {
|
||||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
e.preventDefault()
|
||||||
|
onResolve({
|
||||||
|
variableName: newVariableName,
|
||||||
|
})
|
||||||
|
toast.success(`Added variable ${newVariableName}`)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Set {valueName}
|
|
||||||
</Dialog.Title>
|
|
||||||
|
|
||||||
<CreateNewVariable
|
<CreateNewVariable
|
||||||
setNewVariableName={setNewVariableName}
|
setNewVariableName={setNewVariableName}
|
||||||
newVariableName={newVariableName}
|
newVariableName={newVariableName}
|
||||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||||
shouldCreateVariable={true}
|
shouldCreateVariable={true}
|
||||||
setShouldCreateVariable={() => {}}
|
showCheckbox={false}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className="mt-8 flex justify-between">
|
||||||
<button
|
<ActionButton
|
||||||
type="button"
|
Element="button"
|
||||||
|
type="submit"
|
||||||
disabled={!isNewVariableNameUnique}
|
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 ${
|
icon={{ icon: faPlus }}
|
||||||
!isNewVariableNameUnique
|
|
||||||
? 'opacity-50 cursor-not-allowed'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
onClick={() =>
|
|
||||||
onResolve({
|
|
||||||
variableName: newVariableName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
</button>
|
</ActionButton>
|
||||||
|
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition>
|
</Transition>
|
||||||
)
|
)
|
||||||
|
@ -7,31 +7,58 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { getNormalisedCoordinates } from '../lib/utils'
|
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { addStartSketch } from 'lang/modifyAst'
|
||||||
|
import {
|
||||||
|
addCloseToPipe,
|
||||||
|
addNewSketchLn,
|
||||||
|
compareVec2Epsilon,
|
||||||
|
} from 'lang/std/sketch'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }) => {
|
export const Stream = ({ className = '' }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const {
|
const {
|
||||||
mediaStream,
|
mediaStream,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
setIsMouseDownInStream,
|
setButtonDownInStream,
|
||||||
setCmdId,
|
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
isExecuting,
|
||||||
|
guiMode,
|
||||||
|
ast,
|
||||||
|
updateAst,
|
||||||
|
setGuiMode,
|
||||||
|
programMemory,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
mediaStream: s.mediaStream,
|
mediaStream: s.mediaStream,
|
||||||
engineCommandManager: s.engineCommandManager,
|
engineCommandManager: s.engineCommandManager,
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
setButtonDownInStream: s.setButtonDownInStream,
|
||||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
|
||||||
fileId: s.fileId,
|
fileId: s.fileId,
|
||||||
setCmdId: s.setCmdId,
|
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
streamDimensions: s.streamDimensions,
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -44,25 +71,52 @@ export const Stream = ({ className = '' }) => {
|
|||||||
videoRef.current.srcObject = mediaStream
|
videoRef.current.srcObject = mediaStream
|
||||||
}, [mediaStream, engineCommandManager])
|
}, [mediaStream, engineCommandManager])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||||
clientX,
|
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
}) => {
|
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX: e.clientX,
|
||||||
clientY,
|
clientY: e.clientY,
|
||||||
el: videoRef.current,
|
el: videoRef.current,
|
||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
console.log('click', x, y)
|
|
||||||
|
|
||||||
const newId = uuidv4()
|
const newId = uuidv4()
|
||||||
setCmdId(newId)
|
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||||
|
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||||
|
|
||||||
|
if (
|
||||||
|
interactionGuards.pan.callback(e) ||
|
||||||
|
interactionGuards.pan.lenientDragStartButton === e.button
|
||||||
|
) {
|
||||||
|
interaction = 'pan'
|
||||||
|
} else if (
|
||||||
|
interactionGuards.rotate.callback(e) ||
|
||||||
|
interactionGuards.rotate.lenientDragStartButton === e.button
|
||||||
|
) {
|
||||||
|
interaction = 'rotate'
|
||||||
|
} else if (
|
||||||
|
interactionGuards.zoom.dragCallback(e) ||
|
||||||
|
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||||
|
) {
|
||||||
|
interaction = 'zoom'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'handle_mouse_drag_start',
|
||||||
|
window: { x, y },
|
||||||
|
},
|
||||||
|
cmd_id: newId,
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
!(
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any)
|
||||||
|
)
|
||||||
|
) {
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -72,12 +126,15 @@ export const Stream = ({ className = '' }) => {
|
|||||||
},
|
},
|
||||||
cmd_id: newId,
|
cmd_id: newId,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setIsMouseDownInStream(true)
|
setButtonDownInStream(e.button)
|
||||||
|
setClickCoords({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||||
e.preventDefault()
|
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||||
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -94,6 +151,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
ctrlKey,
|
ctrlKey,
|
||||||
}) => {
|
}) => {
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
|
setButtonDownInStream(undefined)
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
@ -104,7 +162,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||||
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
const command: Models['WebSocketRequest_type'] = {
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_end',
|
type: 'camera_drag_end',
|
||||||
@ -112,9 +170,8 @@ export const Stream = ({ className = '' }) => {
|
|||||||
window: { x, y },
|
window: { x, y },
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
})
|
}
|
||||||
|
|
||||||
setIsMouseDownInStream(false)
|
|
||||||
if (!didDragInStream) {
|
if (!didDragInStream) {
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -126,7 +183,160 @@ export const Stream = ({ className = '' }) => {
|
|||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!didDragInStream && guiMode.mode === 'default') {
|
||||||
|
command.cmd = {
|
||||||
|
type: 'select_with_point',
|
||||||
|
selection_type: 'add',
|
||||||
|
selected_at_window: { x, y },
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
(!didDragInStream &&
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||||
|
(guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any))
|
||||||
|
) {
|
||||||
|
command.cmd = {
|
||||||
|
type: 'mouse_click',
|
||||||
|
window: { x, y },
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('move' as any)
|
||||||
|
) {
|
||||||
|
command.cmd = {
|
||||||
|
type: 'handle_mouse_drag_end',
|
||||||
|
window: { x, y },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
|
||||||
|
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === ('sketch_line' as any as 'line')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Check if the sketch group already exists.
|
||||||
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
guiMode.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
).node
|
||||||
|
const variableName = varDec?.id?.name
|
||||||
|
const sketchGroup = programMemory.root[variableName]
|
||||||
|
const isEditingExistingSketch =
|
||||||
|
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
|
||||||
|
|
||||||
|
if (
|
||||||
|
resp?.data?.data?.entities_modified?.length &&
|
||||||
|
guiMode.waitingFirstClick &&
|
||||||
|
!isEditingExistingSketch
|
||||||
|
) {
|
||||||
|
const curve = await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: resp?.data?.data?.entities_modified[0],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const coords: { x: number; y: number }[] =
|
||||||
|
curve.data.data.control_points
|
||||||
|
const _addStartSketch = addStartSketch(
|
||||||
|
ast,
|
||||||
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||||
|
[
|
||||||
|
roundOff(coords[1].x - coords[0].x),
|
||||||
|
roundOff(coords[1].y - coords[0].y),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const _modifiedAst = _addStartSketch.modifiedAst
|
||||||
|
const _pathToNode = _addStartSketch.pathToNode
|
||||||
|
|
||||||
|
setGuiMode({
|
||||||
|
...guiMode,
|
||||||
|
pathToNode: _pathToNode,
|
||||||
|
waitingFirstClick: false,
|
||||||
|
})
|
||||||
|
updateAst(_modifiedAst, false)
|
||||||
|
} else if (
|
||||||
|
resp?.data?.data?.entities_modified?.length &&
|
||||||
|
(!guiMode.waitingFirstClick || isEditingExistingSketch)
|
||||||
|
) {
|
||||||
|
const curve = await engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: resp?.data?.data?.entities_modified[0],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const coords: { x: number; y: number }[] =
|
||||||
|
curve.data.data.control_points
|
||||||
|
|
||||||
|
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
guiMode.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
const variableName = varDec.id.name
|
||||||
|
const sketchGroup = programMemory.root[variableName]
|
||||||
|
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||||
|
const initialCoords = sketchGroup.value[0].from
|
||||||
|
|
||||||
|
const isClose = compareVec2Epsilon(initialCoords, [
|
||||||
|
coords[1].x,
|
||||||
|
coords[1].y,
|
||||||
|
])
|
||||||
|
|
||||||
|
let _modifiedAst: Program
|
||||||
|
if (!isClose) {
|
||||||
|
_modifiedAst = addNewSketchLn({
|
||||||
|
node: ast,
|
||||||
|
programMemory,
|
||||||
|
to: [coords[1].x, coords[1].y],
|
||||||
|
fnName: 'line',
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
}).modifiedAst
|
||||||
|
updateAst(_modifiedAst, false)
|
||||||
|
} else {
|
||||||
|
_modifiedAst = addCloseToPipe({
|
||||||
|
node: ast,
|
||||||
|
programMemory,
|
||||||
|
pathToNode: guiMode.pathToNode,
|
||||||
|
})
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'default',
|
||||||
|
})
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sketch_mode_disable',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
updateAst(_modifiedAst, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
setDidDragInStream(false)
|
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 (
|
return (
|
||||||
@ -142,7 +352,9 @@ export const Stream = ({ className = '' }) => {
|
|||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
onWheel={handleScroll}
|
onWheel={handleScroll}
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
className="w-full h-full"
|
onMouseMoveCapture={handleMouseMove}
|
||||||
|
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||||
|
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
296
src/components/TextEditor.tsx
Normal file
296
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import ReactCodeMirror, {
|
||||||
|
Extension,
|
||||||
|
ViewUpdate,
|
||||||
|
keymap,
|
||||||
|
} from '@uiw/react-codemirror'
|
||||||
|
import { FromServer, IntoServer } from 'editor/lsp/codec'
|
||||||
|
import Server from '../editor/lsp/server'
|
||||||
|
import Client from '../editor/lsp/client'
|
||||||
|
import { TEST } from 'env'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
|
import { Themes } from 'lib/theme'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
|
import { Selections, useStore } from 'useStore'
|
||||||
|
import { LanguageServerClient } from 'editor/lsp'
|
||||||
|
import kclLanguage from 'editor/lsp/language'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||||
|
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
addLineHighlight,
|
||||||
|
lineHighlightField,
|
||||||
|
} from 'editor/highlightextension'
|
||||||
|
import { isOverlap, roundOff } from 'lib/utils'
|
||||||
|
import { kclErrToDiagnostic } from 'lang/errors'
|
||||||
|
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||||
|
import interact from '@replit/codemirror-interact'
|
||||||
|
|
||||||
|
export const editorShortcutMeta = {
|
||||||
|
formatCode: {
|
||||||
|
codeMirror: 'Alt-Shift-f',
|
||||||
|
display: 'Alt + Shift + F',
|
||||||
|
},
|
||||||
|
convertToVariable: {
|
||||||
|
codeMirror: 'Ctrl-Shift-c',
|
||||||
|
display: 'Ctrl + Shift + C',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextEditor = ({
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
theme: Themes.Light | Themes.Dark
|
||||||
|
}) => {
|
||||||
|
const pathParams = useParams()
|
||||||
|
const {
|
||||||
|
code,
|
||||||
|
deferredSetCode,
|
||||||
|
editorView,
|
||||||
|
engineCommandManager,
|
||||||
|
formatCode,
|
||||||
|
isLSPServerReady,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
setEditorView,
|
||||||
|
setIsLSPServerReady,
|
||||||
|
setSelectionRanges,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
code: s.code,
|
||||||
|
deferredSetCode: s.deferredSetCode,
|
||||||
|
editorView: s.editorView,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
isLSPServerReady: s.isLSPServerReady,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||||
|
setEditorView: s.setEditorView,
|
||||||
|
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||||
|
setSelectionRanges: s.setSelectionRanges,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings: {
|
||||||
|
context: { textWrapping },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
const { setCommandBarOpen } = useCommandsContext()
|
||||||
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
|
useConvertToVariable()
|
||||||
|
|
||||||
|
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||||
|
// But the server happens async so we break this into two parts.
|
||||||
|
// Below is the client and server promise.
|
||||||
|
const { lspClient } = useMemo(() => {
|
||||||
|
const intoServer: IntoServer = new IntoServer()
|
||||||
|
const fromServer: FromServer = FromServer.create()
|
||||||
|
const client = new Client(fromServer, intoServer)
|
||||||
|
if (!TEST) {
|
||||||
|
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||||
|
lspServer.start()
|
||||||
|
setIsLSPServerReady(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const lspClient = new LanguageServerClient({ client })
|
||||||
|
return { lspClient }
|
||||||
|
}, [setIsLSPServerReady])
|
||||||
|
|
||||||
|
// Here we initialize the plugin which will start the client.
|
||||||
|
// When we have multi-file support the name of the file will be a dep of
|
||||||
|
// this use memo, as well as the directory structure, which I think is
|
||||||
|
// a good setup becuase it will restart the client but not the server :)
|
||||||
|
// We do not want to restart the server, its just wasteful.
|
||||||
|
const kclLSP = useMemo(() => {
|
||||||
|
let plugin = null
|
||||||
|
if (isLSPServerReady && !TEST) {
|
||||||
|
// Set up the lsp plugin.
|
||||||
|
const lsp = kclLanguage({
|
||||||
|
// When we have more than one file, we'll need to change this.
|
||||||
|
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||||
|
workspaceFolders: null,
|
||||||
|
client: lspClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin = lsp
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}, [lspClient, isLSPServerReady])
|
||||||
|
|
||||||
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
|
deferredSetCode(value)
|
||||||
|
if (isTauri() && pathParams.id) {
|
||||||
|
// Save the file to disk
|
||||||
|
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||||
|
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||||
|
(err) => {
|
||||||
|
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||||
|
console.error('error saving file', err)
|
||||||
|
toast.error('Error saving file, please check file permissions')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (editorView) {
|
||||||
|
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||||
|
}
|
||||||
|
} //, []);
|
||||||
|
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||||
|
if (!editorView) {
|
||||||
|
setEditorView(viewUpdate.view)
|
||||||
|
}
|
||||||
|
const ranges = viewUpdate.state.selection.ranges
|
||||||
|
|
||||||
|
const isChange =
|
||||||
|
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||||
|
ranges.some(({ from, to }, i) => {
|
||||||
|
return (
|
||||||
|
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||||
|
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isChange) return
|
||||||
|
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||||
|
({ from, to }) => {
|
||||||
|
if (selectionRangeTypeMap[to]) {
|
||||||
|
return {
|
||||||
|
type: selectionRangeTypeMap[to],
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const idBasedSelections = codeBasedSelections
|
||||||
|
.map(({ type, range }) => {
|
||||||
|
const hasOverlap = Object.entries(
|
||||||
|
engineCommandManager?.sourceRangeMap || {}
|
||||||
|
).filter(([_, sourceRange]) => {
|
||||||
|
return isOverlap(sourceRange, range)
|
||||||
|
})
|
||||||
|
if (hasOverlap.length) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id: hasOverlap[0][0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as any
|
||||||
|
|
||||||
|
engineCommandManager?.cusorsSelected({
|
||||||
|
otherSelections: [],
|
||||||
|
idBasedSelections,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSelectionRanges({
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorExtensions = useMemo(() => {
|
||||||
|
const extensions = [
|
||||||
|
lineHighlightField,
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Meta-k',
|
||||||
|
run: () => {
|
||||||
|
setCommandBarOpen(true)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: editorShortcutMeta.formatCode.codeMirror,
|
||||||
|
run: () => {
|
||||||
|
formatCode()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: editorShortcutMeta.convertToVariable.codeMirror,
|
||||||
|
run: () => {
|
||||||
|
if (convertEnabled) {
|
||||||
|
convertCallback()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
] as Extension[]
|
||||||
|
|
||||||
|
if (kclLSP) extensions.push(kclLSP)
|
||||||
|
|
||||||
|
// These extensions have proven to mess with vitest
|
||||||
|
if (!TEST) {
|
||||||
|
extensions.push(
|
||||||
|
lintGutter(),
|
||||||
|
linter((_view) => {
|
||||||
|
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||||
|
}),
|
||||||
|
interact({
|
||||||
|
rules: [
|
||||||
|
// a rule for a number dragger
|
||||||
|
{
|
||||||
|
// the regexp matching the value
|
||||||
|
regexp: /-?\b\d+\.?\d*\b/g,
|
||||||
|
// set cursor to "ew-resize" on hover
|
||||||
|
cursor: 'ew-resize',
|
||||||
|
// change number value based on mouse X movement on drag
|
||||||
|
onDrag: (text, setText, e) => {
|
||||||
|
const multiplier =
|
||||||
|
e.shiftKey && e.metaKey
|
||||||
|
? 0.01
|
||||||
|
: e.metaKey
|
||||||
|
? 0.1
|
||||||
|
: e.shiftKey
|
||||||
|
? 10
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const delta = e.movementX * multiplier
|
||||||
|
|
||||||
|
const newVal = roundOff(
|
||||||
|
Number(text) + delta,
|
||||||
|
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isNaN(newVal)) return
|
||||||
|
setText(newVal.toString())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
}, [kclLSP, textWrapping])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="code-mirror-override"
|
||||||
|
className="full-height-subtract"
|
||||||
|
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
||||||
|
>
|
||||||
|
<ReactCodeMirror
|
||||||
|
className="h-full"
|
||||||
|
value={code}
|
||||||
|
extensions={editorExtensions}
|
||||||
|
onChange={onChange}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
theme={theme}
|
||||||
|
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { create } from 'react-modal-promise'
|
|
||||||
import { useStore } from '../../useStore'
|
|
||||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
|
|
||||||
import { SetVarNameModal } from '../SetVarNameModal'
|
|
||||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
|
|
||||||
|
|
||||||
const getModalInfo = create(SetVarNameModal as any)
|
|
||||||
|
|
||||||
export const ConvertToVariable = () => {
|
|
||||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
|
||||||
(s) => ({
|
|
||||||
guiMode: s.guiMode,
|
|
||||||
ast: s.ast,
|
|
||||||
updateAst: s.updateAst,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
programMemory: s.programMemory,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ast) return
|
|
||||||
|
|
||||||
const { isSafe, value } = isNodeSafeToReplace(
|
|
||||||
ast,
|
|
||||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
|
||||||
)
|
|
||||||
const canReplace = isSafe && value.type !== 'Identifier'
|
|
||||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
|
||||||
|
|
||||||
const _enableHorz = canReplace && isOnlyOneSelection
|
|
||||||
setEnableAngLen(_enableHorz)
|
|
||||||
}, [guiMode, selectionRanges])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
if (!ast) return
|
|
||||||
try {
|
|
||||||
const { variableName } = await getModalInfo({
|
|
||||||
valueName: 'var',
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
|
||||||
ast,
|
|
||||||
programMemory,
|
|
||||||
selectionRanges.codeBasedSelections[0].range,
|
|
||||||
variableName
|
|
||||||
)
|
|
||||||
|
|
||||||
updateAst(_modifiedAst)
|
|
||||||
} catch (e) {
|
|
||||||
console.log('e', e)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!enableAngLen}
|
|
||||||
>
|
|
||||||
ConvertToVariable
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
@ -82,7 +82,7 @@ export const EqualAngle = () => {
|
|||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory,
|
||||||
})
|
})
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -82,7 +82,7 @@ export const EqualLength = () => {
|
|||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory,
|
programMemory,
|
||||||
})
|
})
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -61,7 +61,7 @@ export const HorzVert = ({
|
|||||||
programMemory,
|
programMemory,
|
||||||
referenceSegName: '',
|
referenceSegName: '',
|
||||||
})
|
})
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -154,7 +154,7 @@ export const Intersect = () => {
|
|||||||
initialVariableName: 'offset',
|
initialVariableName: 'offset',
|
||||||
} as any)
|
} as any)
|
||||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -182,7 +182,7 @@ export const Intersect = () => {
|
|||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
_modifiedAst.body = newBody
|
||||||
}
|
}
|
||||||
updateAst(_modifiedAst, {
|
updateAst(_modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export const RemoveConstrainingValues = () => {
|
|||||||
programMemory,
|
programMemory,
|
||||||
referenceSegName: '',
|
referenceSegName: '',
|
||||||
})
|
})
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -124,7 +124,7 @@ export const SetAbsDistance = ({
|
|||||||
_modifiedAst.body = newBody
|
_modifiedAst.body = newBody
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAst(_modifiedAst, {
|
updateAst(_modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -113,7 +113,7 @@ export const SetAngleBetween = () => {
|
|||||||
initialVariableName: 'angle',
|
initialVariableName: 'angle',
|
||||||
} as any)
|
} as any)
|
||||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -141,7 +141,7 @@ export const SetAngleBetween = () => {
|
|||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
_modifiedAst.body = newBody
|
||||||
}
|
}
|
||||||
updateAst(_modifiedAst, {
|
updateAst(_modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ export const SetHorzVertDistance = ({
|
|||||||
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||||
} as any))
|
} as any))
|
||||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||||
updateAst(modifiedAst, {
|
updateAst(modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -163,7 +163,7 @@ export const SetHorzVertDistance = ({
|
|||||||
)
|
)
|
||||||
_modifiedAst.body = newBody
|
_modifiedAst.body = newBody
|
||||||
}
|
}
|
||||||
updateAst(_modifiedAst, {
|
updateAst(_modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ export const SetAngleLength = ({
|
|||||||
_modifiedAst.body = newBody
|
_modifiedAst.body = newBody
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAst(_modifiedAst, {
|
updateAst(_modifiedAst, true, {
|
||||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
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,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
|||||||
.VITE_KC_API_WS_MODELING_URL
|
.VITE_KC_API_WS_MODELING_URL
|
||||||
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
||||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||||
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
|
|
||||||
.VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
|
|
||||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||||
|
239
src/hooks/useAppMode.ts
Normal file
239
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
// needed somewhere to dump this logic,
|
||||||
|
// Once we have xState this should be removed
|
||||||
|
|
||||||
|
import { useStore, Selections } from 'useStore'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import { isReducedMotion } from 'lang/util'
|
||||||
|
import { isOverlap } from 'lib/utils'
|
||||||
|
|
||||||
|
interface DefaultPlanes {
|
||||||
|
xy: string
|
||||||
|
// TODO re-enable
|
||||||
|
// yz: string
|
||||||
|
// xz: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppMode() {
|
||||||
|
const {
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
selectionRanges,
|
||||||
|
engineCommandManager,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||||
|
}))
|
||||||
|
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
guiMode.mode === 'sketch' &&
|
||||||
|
guiMode.sketchMode === 'selectFace' &&
|
||||||
|
engineCommandManager
|
||||||
|
) {
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
// TODO re-enable
|
||||||
|
// const yz = createPlane(engineCommandManager, {
|
||||||
|
// x_axis: { x: 0, y: 1, z: 0 },
|
||||||
|
// y_axis: { x: 0, y: 0, z: 1 },
|
||||||
|
// color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||||
|
// })
|
||||||
|
// const xz = createPlane(engineCommandManager, {
|
||||||
|
// x_axis: { x: 1, y: 0, z: 0 },
|
||||||
|
// y_axis: { x: 0, y: 0, z: 1 },
|
||||||
|
// color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||||
|
// })
|
||||||
|
setDefaultPlanes({ xy })
|
||||||
|
} else {
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||||
|
}
|
||||||
|
if (guiMode.mode === 'default') {
|
||||||
|
const pathId =
|
||||||
|
engineCommandManager &&
|
||||||
|
isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
if (pathId) {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'canEditSketch',
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathToNode: [],
|
||||||
|
pathId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (guiMode.mode === 'canEditSketch') {
|
||||||
|
if (
|
||||||
|
!engineCommandManager ||
|
||||||
|
!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'default',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
guiMode,
|
||||||
|
guiMode.mode,
|
||||||
|
engineCommandManager,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unSub = engineCommandManager?.subscribeTo({
|
||||||
|
event: 'select_with_point',
|
||||||
|
callback: async ({ data }) => {
|
||||||
|
if (!data.entity_id) return
|
||||||
|
if (!defaultPlanes) return
|
||||||
|
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||||
|
// user clicked something else in the scene
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
|
||||||
|
{
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sketch_mode_enable',
|
||||||
|
plane_id: data.entity_id,
|
||||||
|
ortho: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||||
|
const sketchUuid = uuidv4()
|
||||||
|
const proms: any[] = []
|
||||||
|
proms.push(
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: sketchUuid,
|
||||||
|
cmd: {
|
||||||
|
type: 'start_path',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
proms.push(
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'edit_mode_enter',
|
||||||
|
target: sketchUuid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const res = await Promise.all(proms)
|
||||||
|
console.log('res', res)
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'sketchEdit',
|
||||||
|
rotation: [0, 0, 0, 1],
|
||||||
|
position: [0, 0, 0],
|
||||||
|
pathToNode: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
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 setDefaultPlanesHidden(
|
||||||
|
engineCommandManager: EngineCommandManager | undefined,
|
||||||
|
defaultPlanes: DefaultPlanes,
|
||||||
|
hidden: boolean
|
||||||
|
) {
|
||||||
|
Object.values(defaultPlanes).forEach((planeId) => {
|
||||||
|
engineCommandManager?.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'object_visible',
|
||||||
|
object_id: planeId,
|
||||||
|
hidden: hidden,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCursorInSketchCommandRange(
|
||||||
|
artifactMap: ArtifactMap,
|
||||||
|
selectionRanges: Selections
|
||||||
|
): string | false {
|
||||||
|
const overlapingEntries = Object.entries(artifactMap || {}).filter(
|
||||||
|
([id, artifact]) =>
|
||||||
|
selectionRanges.codeBasedSelections.some(
|
||||||
|
(selection) =>
|
||||||
|
Array.isArray(selection?.range) &&
|
||||||
|
Array.isArray(artifact?.range) &&
|
||||||
|
isOverlap(selection.range, artifact.range) &&
|
||||||
|
(artifact.commandType === 'start_path' ||
|
||||||
|
artifact.commandType === 'extend_path' ||
|
||||||
|
artifact.commandType === 'close_path')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return overlapingEntries.length && overlapingEntries[0][1].parentId
|
||||||
|
? overlapingEntries[0][1].parentId
|
||||||
|
: overlapingEntries.find(
|
||||||
|
([, artifact]) => artifact.commandType === 'start_path'
|
||||||
|
)?.[0] || false
|
||||||
|
}
|
50
src/hooks/useEngineConnectionSubscriptions.ts
Normal file
50
src/hooks/useEngineConnectionSubscriptions.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
|
export function useEngineConnectionSubscriptions() {
|
||||||
|
const {
|
||||||
|
engineCommandManager,
|
||||||
|
setCursor2,
|
||||||
|
setHighlightRange,
|
||||||
|
highlightRange,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
setCursor2: s.setCursor2,
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
highlightRange: s.highlightRange,
|
||||||
|
}))
|
||||||
|
useEffect(() => {
|
||||||
|
if (!engineCommandManager) return
|
||||||
|
|
||||||
|
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||||
|
event: 'highlight_set_entity',
|
||||||
|
callback: ({ data }) => {
|
||||||
|
if (data?.entity_id) {
|
||||||
|
const sourceRange =
|
||||||
|
engineCommandManager.sourceRangeMap[data.entity_id]
|
||||||
|
setHighlightRange(sourceRange)
|
||||||
|
} else if (
|
||||||
|
!highlightRange ||
|
||||||
|
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
|
||||||
|
) {
|
||||||
|
setHighlightRange([0, 0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
|
event: 'select_with_point',
|
||||||
|
callback: ({ data }) => {
|
||||||
|
if (!data?.entity_id) {
|
||||||
|
setCursor2()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
|
||||||
|
setCursor2({ range: sourceRange, type: 'default' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unSubHover()
|
||||||
|
unSubClick()
|
||||||
|
}
|
||||||
|
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
|
||||||
|
}
|
53
src/hooks/useSetupEngineManager.ts
Normal file
53
src/hooks/useSetupEngineManager.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useLayoutEffect } from 'react'
|
||||||
|
import { _executor } from '../lang/executor'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { EngineCommandManager } from '../lang/std/engineConnection'
|
||||||
|
|
||||||
|
export function useSetupEngineManager(
|
||||||
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
|
token?: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
setEngineCommandManager,
|
||||||
|
setMediaStream,
|
||||||
|
setIsStreamReady,
|
||||||
|
setStreamDimensions,
|
||||||
|
executeCode,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
setEngineCommandManager: s.setEngineCommandManager,
|
||||||
|
setMediaStream: s.setMediaStream,
|
||||||
|
setIsStreamReady: s.setIsStreamReady,
|
||||||
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
|
executeCode: s.executeCode,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const streamWidth = streamRef?.current?.offsetWidth
|
||||||
|
const streamHeight = streamRef?.current?.offsetHeight
|
||||||
|
|
||||||
|
const width = streamWidth ? streamWidth : 0
|
||||||
|
const quadWidth = Math.round(width / 4) * 4
|
||||||
|
const height = streamHeight ? streamHeight : 0
|
||||||
|
const quadHeight = Math.round(height / 4) * 4
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setStreamDimensions({
|
||||||
|
streamWidth: quadWidth,
|
||||||
|
streamHeight: quadHeight,
|
||||||
|
})
|
||||||
|
if (!width || !height) return
|
||||||
|
const eng = new EngineCommandManager({
|
||||||
|
setMediaStream,
|
||||||
|
setIsStreamReady,
|
||||||
|
width: quadWidth,
|
||||||
|
height: quadHeight,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
setEngineCommandManager(eng)
|
||||||
|
eng.waitForReady.then(() => {
|
||||||
|
executeCode()
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
eng?.tearDown()
|
||||||
|
}
|
||||||
|
}, [quadWidth, quadHeight])
|
||||||
|
}
|
56
src/hooks/useToolbarGuards.ts
Normal file
56
src/hooks/useToolbarGuards.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||||
|
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||||
|
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { create } from 'react-modal-promise'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
|
const getModalInfo = create(SetVarNameModal as any)
|
||||||
|
|
||||||
|
export function useConvertToVariable() {
|
||||||
|
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||||
|
(s) => ({
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
ast: s.ast,
|
||||||
|
updateAst: s.updateAst,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
programMemory: s.programMemory,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const [enable, setEnabled] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ast) return
|
||||||
|
|
||||||
|
const { isSafe, value } = isNodeSafeToReplace(
|
||||||
|
ast,
|
||||||
|
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||||
|
)
|
||||||
|
const canReplace = isSafe && value.type !== 'Identifier'
|
||||||
|
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||||
|
|
||||||
|
const _enableHorz = canReplace && isOnlyOneSelection
|
||||||
|
setEnabled(_enableHorz)
|
||||||
|
}, [guiMode, selectionRanges])
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
if (!ast) return
|
||||||
|
try {
|
||||||
|
const { variableName } = await getModalInfo({
|
||||||
|
valueName: 'var',
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||||
|
ast,
|
||||||
|
programMemory,
|
||||||
|
selectionRanges.codeBasedSelections[0].range,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
|
||||||
|
updateAst(_modifiedAst, true)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('e', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { enable, handleClick }
|
||||||
|
}
|
@ -82,8 +82,22 @@ code {
|
|||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-height-subtract {
|
||||||
|
--height-subtract: 2.25rem;
|
||||||
|
height: 100%;
|
||||||
|
max-height: calc(100% - var(--height-subtract));
|
||||||
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-editor {
|
#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-activeLine,
|
||||||
@ -132,3 +146,45 @@ code {
|
|||||||
.react-json-view {
|
.react-json-view {
|
||||||
@apply bg-transparent !important;
|
@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;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { parser_wasm } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { KCLUnexpectedError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
@ -179,6 +179,9 @@ const newVar = myVar + 1
|
|||||||
name: 'aIdentifier',
|
name: 'aIdentifier',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
function: {
|
||||||
|
type: 'InMemory',
|
||||||
|
},
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -211,7 +214,6 @@ describe('testing function declaration', () => {
|
|||||||
type: 'FunctionExpression',
|
type: 'FunctionExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
end: 19,
|
end: 19,
|
||||||
id: null,
|
|
||||||
params: [],
|
params: [],
|
||||||
body: {
|
body: {
|
||||||
start: 17,
|
start: 17,
|
||||||
@ -250,7 +252,6 @@ describe('testing function declaration', () => {
|
|||||||
type: 'FunctionExpression',
|
type: 'FunctionExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
end: 39,
|
end: 39,
|
||||||
id: null,
|
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
@ -326,7 +327,6 @@ const myVar = funcN(1, 2)`
|
|||||||
type: 'FunctionExpression',
|
type: 'FunctionExpression',
|
||||||
start: 11,
|
start: 11,
|
||||||
end: 37,
|
end: 37,
|
||||||
id: null,
|
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
@ -416,6 +416,9 @@ const myVar = funcN(1, 2)`
|
|||||||
raw: '2',
|
raw: '2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
function: {
|
||||||
|
type: 'InMemory',
|
||||||
|
},
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -485,6 +488,7 @@ describe('testing pipe operator special', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -521,6 +525,7 @@ describe('testing pipe operator special', () => {
|
|||||||
},
|
},
|
||||||
{ type: 'PipeSubstitution', start: 59, end: 60 },
|
{ type: 'PipeSubstitution', start: 59, end: 60 },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -593,6 +598,7 @@ describe('testing pipe operator special', () => {
|
|||||||
},
|
},
|
||||||
{ type: 'PipeSubstitution', start: 105, end: 106 },
|
{ type: 'PipeSubstitution', start: 105, end: 106 },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -629,6 +635,7 @@ describe('testing pipe operator special', () => {
|
|||||||
},
|
},
|
||||||
{ type: 'PipeSubstitution', start: 128, end: 129 },
|
{ type: 'PipeSubstitution', start: 128, end: 129 },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -651,6 +658,9 @@ describe('testing pipe operator special', () => {
|
|||||||
},
|
},
|
||||||
{ type: 'PipeSubstitution', start: 143, end: 144 },
|
{ type: 'PipeSubstitution', start: 143, end: 144 },
|
||||||
],
|
],
|
||||||
|
function: {
|
||||||
|
type: 'InMemory',
|
||||||
|
},
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -730,6 +740,9 @@ describe('testing pipe operator special', () => {
|
|||||||
end: 35,
|
end: 35,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
function: {
|
||||||
|
type: 'InMemory',
|
||||||
|
},
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -1550,7 +1563,10 @@ const key = 'c'`
|
|||||||
type: 'NoneCodeNode',
|
type: 'NoneCodeNode',
|
||||||
start: code.indexOf('\n// this is a comment'),
|
start: code.indexOf('\n// this is a comment'),
|
||||||
end: code.indexOf('const key'),
|
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)
|
const { nonCodeMeta } = parser_wasm(code)
|
||||||
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||||
@ -1560,7 +1576,9 @@ const key = 'c'`
|
|||||||
const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
|
||||||
codeWithExtraStartWhitespace
|
codeWithExtraStartWhitespace
|
||||||
)
|
)
|
||||||
expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
|
expect(nonCodeMeta2.noneCodeNodes[0].value).toStrictEqual(
|
||||||
|
nonCodeMetaInstance.value
|
||||||
|
)
|
||||||
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
|
||||||
nonCodeMetaInstance.start
|
nonCodeMetaInstance.start
|
||||||
)
|
)
|
||||||
@ -1583,7 +1601,10 @@ const key = 'c'`
|
|||||||
type: 'NoneCodeNode',
|
type: 'NoneCodeNode',
|
||||||
start: 106,
|
start: 106,
|
||||||
end: 166,
|
end: 166,
|
||||||
value: ' /* this is\n a comment\n spanning a few lines */\n ',
|
value: {
|
||||||
|
type: 'blockComment',
|
||||||
|
value: 'this is\n a comment\n spanning a few lines',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('comments in a pipe expression', () => {
|
it('comments in a pipe expression', () => {
|
||||||
@ -1603,7 +1624,10 @@ const key = 'c'`
|
|||||||
type: 'NoneCodeNode',
|
type: 'NoneCodeNode',
|
||||||
start: 125,
|
start: 125,
|
||||||
end: 141,
|
end: 141,
|
||||||
value: '\n// a comment\n ',
|
value: {
|
||||||
|
type: 'blockComment',
|
||||||
|
value: 'a comment',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1627,6 +1651,7 @@ describe('test UnaryExpression', () => {
|
|||||||
{ type: 'Literal', start: 19, end: 20, value: 4, raw: '4' },
|
{ type: 'Literal', start: 19, end: 20, value: 4, raw: '4' },
|
||||||
{ type: 'Literal', start: 22, end: 25, value: 100, raw: '100' },
|
{ type: 'Literal', start: 22, end: 25, value: 100, raw: '100' },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1660,10 +1685,12 @@ describe('testing nested call expressions', () => {
|
|||||||
{ type: 'Literal', start: 34, end: 35, value: 5, raw: '5' },
|
{ type: 'Literal', start: 34, end: 35, value: 5, raw: '5' },
|
||||||
{ type: 'Literal', start: 37, end: 38, value: 3, raw: '3' },
|
{ type: 'Literal', start: 37, end: 38, value: 3, raw: '3' },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1695,6 +1722,7 @@ describe('should recognise callExpresions in binaryExpressions', () => {
|
|||||||
},
|
},
|
||||||
{ type: 'PipeSubstitution', start: 25, end: 26 },
|
{ type: 'PipeSubstitution', start: 25, end: 26 },
|
||||||
],
|
],
|
||||||
|
function: expect.any(Object),
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
right: { type: 'Literal', value: 1, raw: '1', start: 30, end: 31 },
|
right: { type: 'Literal', value: 1, raw: '1', start: 30, end: 31 },
|
||||||
@ -1716,6 +1744,12 @@ describe('parsing errors', () => {
|
|||||||
_theError = e
|
_theError = e
|
||||||
}
|
}
|
||||||
const theError = _theError as any
|
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]]
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -21,7 +21,7 @@ show(mySketch001)`
|
|||||||
)
|
)
|
||||||
expect(shown).toEqual([
|
expect(shown).toEqual([
|
||||||
{
|
{
|
||||||
type: 'sketchGroup',
|
type: 'SketchGroup',
|
||||||
start: {
|
start: {
|
||||||
to: [0, 0],
|
to: [0, 0],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
@ -77,7 +77,7 @@ show(mySketch001)`
|
|||||||
)
|
)
|
||||||
expect(shown).toEqual([
|
expect(shown).toEqual([
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'ExtrudeGroup',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
|
|||||||
)
|
)
|
||||||
expect(geos).toEqual([
|
expect(geos).toEqual([
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'ExtrudeGroup',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
|
|||||||
__meta: [{ sourceRange: [13, 34] }],
|
__meta: [{ sourceRange: [13, 34] }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'extrudeGroup',
|
type: 'ExtrudeGroup',
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
value: [],
|
value: [],
|
||||||
height: 2,
|
height: 2,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { parser_wasm } from './abstractSyntaxTree'
|
import { parser_wasm } from './abstractSyntaxTree'
|
||||||
import { ProgramMemory } from './executor'
|
import { ProgramMemory, SketchGroup } from './executor'
|
||||||
import { initPromise } from './rust'
|
import { initPromise } from './rust'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
@ -117,10 +117,10 @@ show(mySketch)
|
|||||||
// ].join('\n')
|
// ].join('\n')
|
||||||
// const { root } = await exe(code)
|
// const { root } = await exe(code)
|
||||||
// expect(root.mySk1.value).toHaveLength(3)
|
// expect(root.mySk1.value).toHaveLength(3)
|
||||||
// expect(root?.rotated?.type).toBe('sketchGroup')
|
// expect(root?.rotated?.type).toBe('SketchGroup')
|
||||||
// if (
|
// if (
|
||||||
// root?.mySk1?.type !== 'sketchGroup' ||
|
// root?.mySk1?.type !== 'SketchGroup' ||
|
||||||
// root?.rotated?.type !== 'sketchGroup'
|
// root?.rotated?.type !== 'SketchGroup'
|
||||||
// )
|
// )
|
||||||
// throw new Error('not a sketch group')
|
// throw new Error('not a sketch group')
|
||||||
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
||||||
@ -143,7 +143,7 @@ show(mySketch)
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
expect(root.mySk1).toEqual({
|
expect(root.mySk1).toEqual({
|
||||||
type: 'sketchGroup',
|
type: 'SketchGroup',
|
||||||
start: {
|
start: {
|
||||||
to: [0, 0],
|
to: [0, 0],
|
||||||
from: [0, 0],
|
from: [0, 0],
|
||||||
@ -199,7 +199,7 @@ show(mySketch)
|
|||||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||||
expect(root).toEqual({
|
expect(root).toEqual({
|
||||||
three: {
|
three: {
|
||||||
type: 'userVal',
|
type: 'UserVal',
|
||||||
value: 3,
|
value: 3,
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -208,7 +208,7 @@ show(mySketch)
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
yo: {
|
yo: {
|
||||||
type: 'userVal',
|
type: 'UserVal',
|
||||||
value: [1, '2', 3, 9],
|
value: [1, '2', 3, 9],
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -225,7 +225,7 @@ show(mySketch)
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
expect(root.yo).toEqual({
|
expect(root.yo).toEqual({
|
||||||
type: 'userVal',
|
type: 'UserVal',
|
||||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -240,7 +240,7 @@ show(mySketch)
|
|||||||
)
|
)
|
||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
expect(root.myVar).toEqual({
|
expect(root.myVar).toEqual({
|
||||||
type: 'userVal',
|
type: 'UserVal',
|
||||||
value: '123',
|
value: '123',
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -338,7 +338,7 @@ describe('testing math operators', () => {
|
|||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
const sketch = root.part001
|
const sketch = root.part001
|
||||||
// result of `-legLen(5, min(3, 999))` should be -4
|
// 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)
|
expect(yVal).toBe(-4)
|
||||||
})
|
})
|
||||||
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
||||||
@ -356,8 +356,8 @@ describe('testing math operators', () => {
|
|||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
const sketch = root.part001
|
const sketch = root.part001
|
||||||
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
|
// 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 as SketchGroup).value?.[1]?.from).toEqual([3, 4])
|
||||||
expect(sketch.value?.[1]?.to).toEqual([6, 0])
|
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
|
||||||
const removedUnaryExp = code.replace(
|
const removedUnaryExp = code.replace(
|
||||||
`-legLen(segLen('seg01', %), myVar)`,
|
`-legLen(segLen('seg01', %), myVar)`,
|
||||||
`legLen(segLen('seg01', %), myVar)`
|
`legLen(segLen('seg01', %), myVar)`
|
||||||
@ -366,7 +366,9 @@ describe('testing math operators', () => {
|
|||||||
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
||||||
|
|
||||||
// without the minus sign, the y value should be 8
|
// 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 () => {
|
it('with nested callExpression and binaryExpression', async () => {
|
||||||
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
||||||
@ -397,7 +399,10 @@ show(theExtrude)`
|
|||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
async function exe(
|
||||||
|
code: string,
|
||||||
|
programMemory: ProgramMemory = { root: {}, return: null }
|
||||||
|
) {
|
||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
|
|
||||||
const result = await enginelessExecutor(ast, programMemory)
|
const result = await enginelessExecutor(ast, programMemory)
|
||||||
|
@ -5,96 +5,21 @@ import {
|
|||||||
SourceRangeMap,
|
SourceRangeMap,
|
||||||
} from './std/engineConnection'
|
} from './std/engineConnection'
|
||||||
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||||
|
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||||
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
import { execute_wasm } from '../wasm-lib/pkg/wasm_lib'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
import { rangeTypeFix } from './abstractSyntaxTree'
|
import { rangeTypeFix } from './abstractSyntaxTree'
|
||||||
|
|
||||||
export type SourceRange = [number, number]
|
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
|
||||||
export type PathToNode = [string | number, string][] // [pathKey, nodeType][]
|
export type { Position } from '../wasm-lib/kcl/bindings/Position'
|
||||||
export type Metadata = {
|
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
|
||||||
sourceRange: SourceRange
|
export type { Path } from '../wasm-lib/kcl/bindings/Path'
|
||||||
}
|
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||||
export type Position = [number, number, number]
|
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||||
export type Rotation = [number, number, number, number]
|
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||||
|
|
||||||
interface BasePath {
|
export type PathToNode = [string | number, string][]
|
||||||
from: [number, number]
|
|
||||||
to: [number, number]
|
|
||||||
name?: string
|
|
||||||
__geoMeta: {
|
|
||||||
id: string
|
|
||||||
sourceRange: SourceRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToPoint extends BasePath {
|
|
||||||
type: 'toPoint'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Base extends BasePath {
|
|
||||||
type: 'base'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HorizontalLineTo extends BasePath {
|
|
||||||
type: 'horizontalLineTo'
|
|
||||||
x: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AngledLineTo extends BasePath {
|
|
||||||
type: 'angledLineTo'
|
|
||||||
angle: number
|
|
||||||
x?: number
|
|
||||||
y?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeoMeta {
|
|
||||||
__geoMeta: {
|
|
||||||
id: string
|
|
||||||
sourceRange: SourceRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
|
|
||||||
|
|
||||||
export interface SketchGroup {
|
|
||||||
type: 'sketchGroup'
|
|
||||||
id: string
|
|
||||||
value: Path[]
|
|
||||||
start?: Base
|
|
||||||
position: Position
|
|
||||||
rotation: Rotation
|
|
||||||
__meta: Metadata[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtrudePlane {
|
|
||||||
type: 'extrudePlane'
|
|
||||||
position: Position
|
|
||||||
rotation: Rotation
|
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExtrudeSurface = GeoMeta &
|
|
||||||
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
|
|
||||||
|
|
||||||
export interface ExtrudeGroup {
|
|
||||||
type: 'extrudeGroup'
|
|
||||||
id: string
|
|
||||||
value: ExtrudeSurface[]
|
|
||||||
height: number
|
|
||||||
position: Position
|
|
||||||
rotation: Rotation
|
|
||||||
__meta: Metadata[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** UserVal not produced by one of our internal functions */
|
|
||||||
export interface UserVal {
|
|
||||||
type: 'userVal'
|
|
||||||
value: any
|
|
||||||
__meta: Metadata[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
|
||||||
|
|
||||||
interface Memory {
|
interface Memory {
|
||||||
[key: string]: MemoryItem
|
[key: string]: MemoryItem
|
||||||
@ -102,12 +27,12 @@ interface Memory {
|
|||||||
|
|
||||||
export interface ProgramMemory {
|
export interface ProgramMemory {
|
||||||
root: Memory
|
root: Memory
|
||||||
return?: ProgramReturn
|
return: ProgramReturn | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const executor = async (
|
export const executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory = { root: {} },
|
programMemory: ProgramMemory = { root: {}, return: null },
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
// work around while the gemotry is still be stored on the frontend
|
// work around while the gemotry is still be stored on the frontend
|
||||||
// will be removed when the stream UI is added.
|
// will be removed when the stream UI is added.
|
||||||
@ -123,7 +48,7 @@ export const executor = async (
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
const { artifactMap, sourceRangeMap } =
|
const { artifactMap, sourceRangeMap } =
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands(node, _programMemory)
|
||||||
tempMapCallback({ artifactMap, sourceRangeMap })
|
tempMapCallback({ artifactMap, sourceRangeMap })
|
||||||
|
|
||||||
engineCommandManager.endSession()
|
engineCommandManager.endSession()
|
||||||
@ -132,7 +57,7 @@ export const executor = async (
|
|||||||
|
|
||||||
export const _executor = async (
|
export const _executor = async (
|
||||||
node: Program,
|
node: Program,
|
||||||
programMemory: ProgramMemory = { root: {} },
|
programMemory: ProgramMemory = { root: {}, return: null },
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
): Promise<ProgramMemory> => {
|
): Promise<ProgramMemory> => {
|
||||||
try {
|
try {
|
||||||
|
@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
|
|||||||
expect(str).toBe(`const part001 = startSketchAt('default')
|
expect(str).toBe(`const part001 = startSketchAt('default')
|
||||||
|> ry(90, %)
|
|> ry(90, %)
|
||||||
|> line('default', %)
|
|> line('default', %)
|
||||||
show(part001)`)
|
show(part001)
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -175,11 +176,14 @@ show(part001)`
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Testing moveValueIntoNewVariable', () => {
|
describe('Testing moveValueIntoNewVariable', () => {
|
||||||
const fn = (fnName: string) => `const ${fnName} = (x) => {
|
const fn = (fnName: string) => `fn ${fnName} = (x) => {
|
||||||
return 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 abc = 3
|
||||||
const identifierGuy = 5
|
const identifierGuy = 5
|
||||||
const yo = 5 + 6
|
const yo = 5 + 6
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Selection, TooTip } from '../useStore'
|
import { Selection, ToolTip } from '../useStore'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -27,6 +27,48 @@ import {
|
|||||||
getFirstArg,
|
getFirstArg,
|
||||||
createFirstArg,
|
createFirstArg,
|
||||||
} from './std/sketch'
|
} 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(
|
export function addSketchTo(
|
||||||
node: Program,
|
node: Program,
|
||||||
@ -36,14 +78,14 @@ export function addSketchTo(
|
|||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const _name = name || findUniqueName(node, 'part')
|
const _name = name || findUniqueName(node, 'part')
|
||||||
|
|
||||||
const startSketchAt = createCallExpression('startSketchAt', [
|
const startSketchAt = createCallExpressionStdLib('startSketchAt', [
|
||||||
createLiteral('default'),
|
createLiteral('default'),
|
||||||
])
|
])
|
||||||
const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
|
const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
|
||||||
createLiteral(90),
|
createLiteral(90),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
const initialLineTo = createCallExpression('line', [
|
const initialLineTo = createCallExpressionStdLib('line', [
|
||||||
createLiteral('default'),
|
createLiteral('default'),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
@ -112,7 +154,9 @@ function addToShow(node: Program, name: string): Program {
|
|||||||
const dumbyStartend = { start: 0, end: 0 }
|
const dumbyStartend = { start: 0, end: 0 }
|
||||||
const showCallIndex = getShowIndex(_node)
|
const showCallIndex = getShowIndex(_node)
|
||||||
if (showCallIndex === -1) {
|
if (showCallIndex === -1) {
|
||||||
const showCall = createCallExpression('show', [createIdentifier(name)])
|
const showCall = createCallExpressionStdLib('show', [
|
||||||
|
createIdentifier(name),
|
||||||
|
])
|
||||||
const showExpressionStatement: ExpressionStatement = {
|
const showExpressionStatement: ExpressionStatement = {
|
||||||
type: 'ExpressionStatement',
|
type: 'ExpressionStatement',
|
||||||
...dumbyStartend,
|
...dumbyStartend,
|
||||||
@ -124,7 +168,7 @@ function addToShow(node: Program, name: string): Program {
|
|||||||
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
|
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
|
||||||
const showCallArgs = (showCall.expression as CallExpression).arguments
|
const showCallArgs = (showCall.expression as CallExpression).arguments
|
||||||
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
|
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
|
||||||
const newShowExpression = createCallExpression('show', newShowCallArgs)
|
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
|
||||||
|
|
||||||
_node.body[showCallIndex] = {
|
_node.body[showCallIndex] = {
|
||||||
...showCall,
|
...showCall,
|
||||||
@ -149,7 +193,7 @@ export function mutateArrExp(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (node.type === 'ArrayExpression') {
|
if (node.type === 'ArrayExpression') {
|
||||||
node.elements.forEach((element, i) => {
|
node.elements.forEach((element, i) => {
|
||||||
if (element.type === 'Literal') {
|
if (isLiteralArrayOrStatic(element)) {
|
||||||
node.elements[i] = updateWith.elements[i]
|
node.elements[i] = updateWith.elements[i]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -167,8 +211,8 @@ export function mutateObjExpProp(
|
|||||||
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
|
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
|
||||||
if (keyIndex !== -1) {
|
if (keyIndex !== -1) {
|
||||||
if (
|
if (
|
||||||
updateWith.type === 'Literal' &&
|
isLiteralArrayOrStatic(updateWith) &&
|
||||||
node.properties[keyIndex].value.type === 'Literal'
|
isLiteralArrayOrStatic(node.properties[keyIndex].value)
|
||||||
) {
|
) {
|
||||||
node.properties[keyIndex].value = updateWith
|
node.properties[keyIndex].value = updateWith
|
||||||
return true
|
return true
|
||||||
@ -178,7 +222,7 @@ export function mutateObjExpProp(
|
|||||||
) {
|
) {
|
||||||
const arrExp = node.properties[keyIndex].value as ArrayExpression
|
const arrExp = node.properties[keyIndex].value as ArrayExpression
|
||||||
arrExp.elements.forEach((element, i) => {
|
arrExp.elements.forEach((element, i) => {
|
||||||
if (element.type === 'Literal') {
|
if (isLiteralArrayOrStatic(element)) {
|
||||||
arrExp.elements[i] = updateWith.elements[i]
|
arrExp.elements[i] = updateWith.elements[i]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -225,7 +269,7 @@ export function extrudeSketch(
|
|||||||
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpression('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
createLiteral(4),
|
createLiteral(4),
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
@ -261,7 +305,11 @@ export function extrudeSketch(
|
|||||||
}
|
}
|
||||||
const name = findUniqueName(node, 'part')
|
const name = findUniqueName(node, 'part')
|
||||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||||
const showCallIndex = getShowIndex(_node)
|
let showCallIndex = getShowIndex(_node)
|
||||||
|
if (showCallIndex == -1) {
|
||||||
|
// We didn't find a show, so let's just append everything
|
||||||
|
showCallIndex = _node.body.length
|
||||||
|
}
|
||||||
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
||||||
const pathToExtrudeArg: PathToNode = [
|
const pathToExtrudeArg: PathToNode = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
@ -313,15 +361,15 @@ export function sketchOnExtrudedFace(
|
|||||||
const newSketch = createVariableDeclaration(
|
const newSketch = createVariableDeclaration(
|
||||||
newSketchName,
|
newSketchName,
|
||||||
createPipeExpression([
|
createPipeExpression([
|
||||||
createCallExpression('startSketchAt', [
|
createCallExpressionStdLib('startSketchAt', [
|
||||||
createArrayExpression([createLiteral(0), createLiteral(0)]),
|
createArrayExpression([createLiteral(0), createLiteral(0)]),
|
||||||
]),
|
]),
|
||||||
createCallExpression('lineTo', [
|
createCallExpressionStdLib('lineTo', [
|
||||||
createArrayExpression([createLiteral(1), createLiteral(1)]),
|
createArrayExpression([createLiteral(1), createLiteral(1)]),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
]),
|
]),
|
||||||
createCallExpression('transform', [
|
createCallExpression('transform', [
|
||||||
createCallExpression('getExtrudeWallTransform', [
|
createCallExpressionStdLib('getExtrudeWallTransform', [
|
||||||
createLiteral(tag),
|
createLiteral(tag),
|
||||||
createIdentifier(oldSketchName),
|
createIdentifier(oldSketchName),
|
||||||
]),
|
]),
|
||||||
@ -414,6 +462,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(
|
export function createCallExpression(
|
||||||
name: string,
|
name: string,
|
||||||
args: CallExpression['arguments']
|
args: CallExpression['arguments']
|
||||||
@ -428,6 +510,9 @@ export function createCallExpression(
|
|||||||
end: 0,
|
end: 0,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
|
function: {
|
||||||
|
type: 'InMemory',
|
||||||
|
},
|
||||||
optional: false,
|
optional: false,
|
||||||
arguments: args,
|
arguments: args,
|
||||||
}
|
}
|
||||||
@ -554,7 +639,7 @@ export function giveSketchFnCallTag(
|
|||||||
createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal
|
createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal
|
||||||
const tagStr = String(tagValue.value)
|
const tagStr = String(tagValue.value)
|
||||||
const newFirstArg = createFirstArg(
|
const newFirstArg = createFirstArg(
|
||||||
primaryCallExp.callee.name as TooTip,
|
primaryCallExp.callee.name as ToolTip,
|
||||||
firstArg.val,
|
firstArg.val,
|
||||||
tagValue
|
tagValue
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
|
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
|
||||||
import { Selection, TooTip } from '../useStore'
|
import { Selection, ToolTip } from '../useStore'
|
||||||
import {
|
import {
|
||||||
BinaryExpression,
|
BinaryExpression,
|
||||||
Program,
|
Program,
|
||||||
@ -457,7 +457,7 @@ export function isLinesParallelAndConstrained(
|
|||||||
const secondaryFirstArg = getFirstArg(secondaryNode)
|
const secondaryFirstArg = getFirstArg(secondaryNode)
|
||||||
const constraintType = getConstraintType(
|
const constraintType = getConstraintType(
|
||||||
secondaryFirstArg.val,
|
secondaryFirstArg.val,
|
||||||
secondaryNode.callee.name as TooTip
|
secondaryNode.callee.name as ToolTip
|
||||||
)
|
)
|
||||||
const constraintLevel = getConstraintLevelFromSourceRange(
|
const constraintLevel = getConstraintLevelFromSourceRange(
|
||||||
secondaryLine.range,
|
secondaryLine.range,
|
||||||
|
@ -11,26 +11,27 @@ describe('recast', () => {
|
|||||||
const code = '1 + 2'
|
const code = '1 + 2'
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('variable declaration', () => {
|
it('variable declaration', () => {
|
||||||
const code = 'const myVar = 5'
|
const code = 'const myVar = 5'
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it("variable declaration that's binary with string", () => {
|
it("variable declaration that's binary with string", () => {
|
||||||
const code = "const myVar = 5 + 'yo'"
|
const code = "const myVar = 5 + 'yo'"
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
|
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
|
||||||
const { ast: ast2 } = code2ast(codeWithOtherQuotes)
|
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', () => {
|
it('test assigning two variables, the second summing with the first', () => {
|
||||||
const code = `const myVar = 5
|
const code = `const myVar = 5
|
||||||
const newVar = myVar + 1`
|
const newVar = myVar + 1
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
@ -42,12 +43,12 @@ const newVar = myVar + 1`
|
|||||||
)
|
)
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('test with function call', () => {
|
it('test with function call', () => {
|
||||||
const code = `
|
const code = `const myVar = "hello"
|
||||||
const myVar = "hello"
|
log(5, myVar)
|
||||||
log(5, myVar)`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
@ -62,7 +63,7 @@ log(5, myVar)`
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('recast sketch declaration', () => {
|
it('recast sketch declaration', () => {
|
||||||
let code = `const mySketch = startSketchAt([0, 0])
|
let code = `const mySketch = startSketchAt([0, 0])
|
||||||
@ -75,7 +76,7 @@ show(mySketch)
|
|||||||
`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('sketch piped into callExpression', () => {
|
it('sketch piped into callExpression', () => {
|
||||||
const code = [
|
const code = [
|
||||||
@ -87,7 +88,7 @@ show(mySketch)
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('recast BinaryExpression piped into CallExpression', () => {
|
it('recast BinaryExpression piped into CallExpression', () => {
|
||||||
const code = [
|
const code = [
|
||||||
@ -99,37 +100,37 @@ show(mySketch)
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('recast nested binary expression', () => {
|
it('recast nested binary expression', () => {
|
||||||
const code = ['const myVar = 1 + 2 * 5'].join('\n')
|
const code = ['const myVar = 1 + 2 * 5'].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('recast nested binary expression with parans', () => {
|
it('recast nested binary expression with parans', () => {
|
||||||
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
|
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('unnecessary paran wrap will be remove', () => {
|
it('unnecessary paran wrap will be remove', () => {
|
||||||
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
|
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.replace('(', '').replace(')', ''))
|
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
|
||||||
})
|
})
|
||||||
it('complex nested binary expression', () => {
|
it('complex nested binary expression', () => {
|
||||||
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
|
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('multiplied paren expressions', () => {
|
it('multiplied paren expressions', () => {
|
||||||
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
|
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('recast array declaration', () => {
|
it('recast array declaration', () => {
|
||||||
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
||||||
@ -137,7 +138,7 @@ show(mySketch)
|
|||||||
)
|
)
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('recast long array declaration', () => {
|
it('recast long array declaration', () => {
|
||||||
const code = [
|
const code = [
|
||||||
@ -152,7 +153,7 @@ show(mySketch)
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted.trim()).toBe(code.trim())
|
||||||
})
|
})
|
||||||
it('recast long object exectution', () => {
|
it('recast long object exectution', () => {
|
||||||
const code = `const three = 3
|
const code = `const three = 3
|
||||||
@ -161,35 +162,38 @@ const yo = {
|
|||||||
anum: 2,
|
anum: 2,
|
||||||
identifier: three,
|
identifier: three,
|
||||||
binExp: 4 + 5
|
binExp: 4 + 5
|
||||||
}`
|
}
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('recast short object exectution', () => {
|
it('recast short object exectution', () => {
|
||||||
const code = `const yo = { key: 'val' }`
|
const code = `const yo = { key: 'val' }
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('recast object execution with member expression', () => {
|
it('recast object execution with member expression', () => {
|
||||||
const code = `const yo = { a: { b: { c: '123' } } }
|
const code = `const yo = { a: { b: { c: '123' } } }
|
||||||
const key = 'c'
|
const key = 'c'
|
||||||
const myVar = yo.a['b'][key]
|
const myVar = yo.a['b'][key]
|
||||||
const key2 = 'b'
|
const key2 = 'b'
|
||||||
const myVar2 = yo['a'][key2].c`
|
const myVar2 = yo['a'][key2].c
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code.trim())
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing recasting with comments and whitespace', () => {
|
describe('testing recasting with comments and whitespace', () => {
|
||||||
it('code with comments', () => {
|
it('code with comments', () => {
|
||||||
const code = `
|
const code = `const yo = { a: { b: { c: '123' } } }
|
||||||
const yo = { a: { b: { c: '123' } } }
|
|
||||||
// this is a comment
|
// this is a comment
|
||||||
const key = 'c'`
|
const key = 'c'
|
||||||
|
`
|
||||||
|
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
@ -197,38 +201,39 @@ const key = 'c'`
|
|||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('code with comment and extra lines', () => {
|
it('code with comment and extra lines', () => {
|
||||||
const code = `
|
const code = `const yo = 'c'
|
||||||
const yo = 'c' /* this is
|
|
||||||
|
/* this is
|
||||||
a
|
a
|
||||||
comment */
|
comment */
|
||||||
|
const yo = 'bing'
|
||||||
const yo = 'bing'`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('comments at the start and end', () => {
|
it('comments at the start and end', () => {
|
||||||
const code = `
|
const code = `// this is a comment
|
||||||
// this is a comment
|
|
||||||
|
|
||||||
const yo = { a: { b: { c: '123' } } }
|
const yo = { a: { b: { c: '123' } } }
|
||||||
const key = 'c'
|
const key = 'c'
|
||||||
|
|
||||||
// this is also a comment`
|
// this is also a comment
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
it('comments in a fn block', () => {
|
it('comments in a fn block', () => {
|
||||||
const code = `
|
const code = `fn myFn = () => {
|
||||||
const myFn = () => {
|
|
||||||
// this is a comment
|
// this is a comment
|
||||||
const yo = { a: { b: { c: '123' } } } /* block
|
const yo = { a: { b: { c: '123' } } }
|
||||||
comment */
|
|
||||||
|
|
||||||
|
/* block
|
||||||
|
comment */
|
||||||
const key = 'c'
|
const key = 'c'
|
||||||
// this is also a comment
|
// this is also a comment
|
||||||
}`
|
}
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
@ -244,7 +249,7 @@ const myFn = () => {
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('comments sprinkled in all over the place', () => {
|
it('comments sprinkled in all over the place', () => {
|
||||||
const code = `
|
const code = `
|
||||||
@ -266,10 +271,26 @@ const mySk1 = startSketchAt([0, 0])
|
|||||||
|> rx(45, %)
|
|> rx(45, %)
|
||||||
/*
|
/*
|
||||||
one more for good measure
|
one more for good measure
|
||||||
*/`
|
*/
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
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,19 +299,19 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
|||||||
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('nested callExpression in unaryExpression', () => {
|
it('nested callExpression in unaryExpression', () => {
|
||||||
const code = 'const myVar = -min(100, legLen(5, 3))'
|
const code = 'const myVar = -min(100, legLen(5, 3))'
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('with unaryExpression in callExpression', () => {
|
it('with unaryExpression in callExpression', () => {
|
||||||
const code = 'const myVar = min(5, -legLen(5, 4))'
|
const code = 'const myVar = min(5, -legLen(5, 4))'
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
it('with unaryExpression in sketch situation', () => {
|
it('with unaryExpression in sketch situation', () => {
|
||||||
const code = [
|
const code = [
|
||||||
@ -299,7 +320,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted.trim()).toBe(code)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -314,7 +335,8 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
|||||||
intersectTag: 'seg01'
|
intersectTag: 'seg01'
|
||||||
}, %)
|
}, %)
|
||||||
|> line([-0.42, -1.72], %)
|
|> line([-0.42, -1.72], %)
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
@ -324,7 +346,8 @@ show(part001)`
|
|||||||
angle: 201,
|
angle: 201,
|
||||||
offset: -1.35,
|
offset: -1.35,
|
||||||
intersectTag: 'seg01'
|
intersectTag: 'seg01'
|
||||||
}, %)`
|
}, %)
|
||||||
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
@ -333,7 +356,8 @@ show(part001)`
|
|||||||
|
|
||||||
describe('it recasts binary expression using brackets where needed', () => {
|
describe('it recasts binary expression using brackets where needed', () => {
|
||||||
it('when there are two minus in a row', () => {
|
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)
|
const recasted = recast(code2ast(code).ast)
|
||||||
expect(recasted).toBe(code)
|
expect(recasted).toBe(code)
|
||||||
})
|
})
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
import { SourceRange } from 'lang/executor'
|
import { ProgramMemory, SourceRange } from 'lang/executor'
|
||||||
import { Selections } from 'useStore'
|
import { Selections } from 'useStore'
|
||||||
import {
|
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||||
VITE_KC_API_WS_MODELING_URL,
|
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS,
|
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
|
|
||||||
} from 'env'
|
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { exportSave } from 'lib/exportSave'
|
import { exportSave } from 'lib/exportSave'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
|
||||||
|
|
||||||
interface ResultCommand {
|
let lastMessage = ''
|
||||||
|
|
||||||
|
interface CommandInfo {
|
||||||
|
commandType: CommandTypes
|
||||||
|
range: SourceRange
|
||||||
|
parentId?: string
|
||||||
|
}
|
||||||
|
interface ResultCommand extends CommandInfo {
|
||||||
type: 'result'
|
type: 'result'
|
||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
interface PendingCommand {
|
interface PendingCommand extends CommandInfo {
|
||||||
type: 'pending'
|
type: 'pending'
|
||||||
promise: Promise<any>
|
promise: Promise<any>
|
||||||
resolve: (val: any) => void
|
resolve: (val: any) => void
|
||||||
@ -34,6 +39,8 @@ interface NewTrackArgs {
|
|||||||
|
|
||||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||||
|
|
||||||
|
type ClientMetrics = Models['ClientMetrics_type']
|
||||||
|
|
||||||
// EngineConnection encapsulates the connection(s) to the Engine
|
// EngineConnection encapsulates the connection(s) to the Engine
|
||||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||||
// and WebRTC connections.
|
// and WebRTC connections.
|
||||||
@ -53,6 +60,9 @@ export class EngineConnection {
|
|||||||
private onClose: (engineConnection: EngineConnection) => void
|
private onClose: (engineConnection: EngineConnection) => void
|
||||||
private onNewTrack: (track: NewTrackArgs) => void
|
private onNewTrack: (track: NewTrackArgs) => void
|
||||||
|
|
||||||
|
// TODO: actual type is ClientMetrics
|
||||||
|
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
@ -188,6 +198,7 @@ export class EngineConnection {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldTrace()) {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
handshakeSpan.promise,
|
handshakeSpan.promise,
|
||||||
iceSpan.promise,
|
iceSpan.promise,
|
||||||
@ -197,6 +208,7 @@ export class EngineConnection {
|
|||||||
console.log('All spans finished, reporting')
|
console.log('All spans finished, reporting')
|
||||||
webrtcMediaTransaction?.finish()
|
webrtcMediaTransaction?.finish()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.onWebsocketOpen(this)
|
this.onWebsocketOpen(this)
|
||||||
})
|
})
|
||||||
@ -297,8 +309,10 @@ export class EngineConnection {
|
|||||||
|
|
||||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
this.pc.addEventListener('connectionstatechange', (event) => {
|
||||||
if (this.pc?.iceConnectionState === 'connected') {
|
if (this.pc?.iceConnectionState === 'connected') {
|
||||||
|
if (this.shouldTrace()) {
|
||||||
iceSpan.resolve?.()
|
iceSpan.resolve?.()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('icecandidate', (event) => {
|
this.pc.addEventListener('icecandidate', (event) => {
|
||||||
@ -330,6 +344,17 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
} else if (resp.type === 'metrics_request') {
|
||||||
|
if (this.webrtcStatsCollector === undefined) {
|
||||||
|
// TODO: Error message here?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.webrtcStatsCollector().then((client_metrics) => {
|
||||||
|
this.send({
|
||||||
|
type: 'metrics_response',
|
||||||
|
metrics: client_metrics,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(paultag): This ought to be both controllable, as well as something
|
// TODO(paultag): This ought to be both controllable, as well as something
|
||||||
@ -361,127 +386,57 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the background thread to keep an eye on statistical
|
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||||
// information about the WebRTC media stream from the server to
|
return new Promise((resolve, reject) => {
|
||||||
// us. We'll also eventually want more global statistical information,
|
if (mediaStream.getVideoTracks().length !== 1) {
|
||||||
// but this will give us a baseline.
|
reject(new Error('too many video tracks to report'))
|
||||||
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
|
|
||||||
setInterval(() => {
|
|
||||||
if (this.pc === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.shouldTrace()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the WebRTC Statistics API to collect statistical information
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||||
// about the WebRTC connection we're using to report to Sentry.
|
|
||||||
mediaStream.getVideoTracks().forEach((videoTrack) => {
|
|
||||||
let trackStats = new Map<string, any>()
|
|
||||||
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||||
// Sentry only allows 10 metrics per transaction. We're going
|
let client_metrics: ClientMetrics = {
|
||||||
// to have to pick carefully here, eventually send like a prom
|
rtc_frames_decoded: 0,
|
||||||
// file or something to the peer.
|
rtc_frames_dropped: 0,
|
||||||
|
rtc_frames_received: 0,
|
||||||
|
rtc_frames_per_second: 0,
|
||||||
|
rtc_freeze_count: 0,
|
||||||
|
rtc_jitter_sec: 0.0,
|
||||||
|
rtc_keyframes_decoded: 0,
|
||||||
|
rtc_total_freezes_duration_sec: 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(paultag): Since we can technically have multiple WebRTC
|
||||||
|
// video tracks (even if the Server doesn't at the moment), we
|
||||||
|
// ought to send stats for every video track(?), and add the stream
|
||||||
|
// ID into it. This raises the cardinality of collected metrics
|
||||||
|
// when/if we do, but for now, just report the one stream.
|
||||||
|
|
||||||
const transaction = Sentry.startTransaction({
|
|
||||||
name: 'webrtc-stats',
|
|
||||||
})
|
|
||||||
videoTrackStats.forEach((videoTrackReport) => {
|
videoTrackStats.forEach((videoTrackReport) => {
|
||||||
if (videoTrackReport.type === 'inbound-rtp') {
|
if (videoTrackReport.type === 'inbound-rtp') {
|
||||||
// RTC Stream Info
|
client_metrics.rtc_frames_decoded =
|
||||||
// transaction.setMeasurement(
|
videoTrackReport.framesDecoded
|
||||||
// 'mediaStreamTrack.framesDecoded',
|
client_metrics.rtc_frames_dropped =
|
||||||
// videoTrackReport.framesDecoded,
|
videoTrackReport.framesDropped
|
||||||
// 'frame'
|
client_metrics.rtc_frames_received =
|
||||||
// )
|
videoTrackReport.framesReceived
|
||||||
transaction.setMeasurement(
|
client_metrics.rtc_frames_per_second =
|
||||||
'rtcFramesDropped',
|
videoTrackReport.framesPerSecond || 0
|
||||||
videoTrackReport.framesDropped,
|
client_metrics.rtc_freeze_count =
|
||||||
''
|
videoTrackReport.freezeCount || 0
|
||||||
)
|
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
|
||||||
// transaction.setMeasurement(
|
client_metrics.rtc_keyframes_decoded =
|
||||||
// 'mediaStreamTrack.framesReceived',
|
videoTrackReport.keyFramesDecoded
|
||||||
// videoTrackReport.framesReceived,
|
client_metrics.rtc_total_freezes_duration_sec =
|
||||||
// 'frame'
|
videoTrackReport.totalFreezesDuration || 0
|
||||||
// )
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcFramesPerSecond',
|
|
||||||
videoTrackReport.framesPerSecond,
|
|
||||||
'fps'
|
|
||||||
)
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcFreezeCount',
|
|
||||||
videoTrackReport.freezeCount,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcJitter',
|
|
||||||
videoTrackReport.jitter,
|
|
||||||
'second'
|
|
||||||
)
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.jitterBufferDelay',
|
|
||||||
// videoTrackReport.jitterBufferDelay,
|
|
||||||
// ''
|
|
||||||
// )
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.jitterBufferEmittedCount',
|
|
||||||
// videoTrackReport.jitterBufferEmittedCount,
|
|
||||||
// ''
|
|
||||||
// )
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.jitterBufferMinimumDelay',
|
|
||||||
// videoTrackReport.jitterBufferMinimumDelay,
|
|
||||||
// ''
|
|
||||||
// )
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.jitterBufferTargetDelay',
|
|
||||||
// videoTrackReport.jitterBufferTargetDelay,
|
|
||||||
// ''
|
|
||||||
// )
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcKeyFramesDecoded',
|
|
||||||
videoTrackReport.keyFramesDecoded,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcTotalFreezesDuration',
|
|
||||||
videoTrackReport.totalFreezesDuration,
|
|
||||||
'second'
|
|
||||||
)
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.totalInterFrameDelay',
|
|
||||||
// videoTrackReport.totalInterFrameDelay,
|
|
||||||
// ''
|
|
||||||
// )
|
|
||||||
transaction.setMeasurement(
|
|
||||||
'rtcTotalPausesDuration',
|
|
||||||
videoTrackReport.totalPausesDuration,
|
|
||||||
'second'
|
|
||||||
)
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.totalProcessingDelay',
|
|
||||||
// videoTrackReport.totalProcessingDelay,
|
|
||||||
// 'second'
|
|
||||||
// )
|
|
||||||
} else if (videoTrackReport.type === 'transport') {
|
} else if (videoTrackReport.type === 'transport') {
|
||||||
// // Bytes i/o
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.bytesReceived',
|
|
||||||
// videoTrackReport.bytesReceived,
|
// videoTrackReport.bytesReceived,
|
||||||
// 'byte'
|
|
||||||
// )
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.bytesSent',
|
|
||||||
// videoTrackReport.bytesSent,
|
// videoTrackReport.bytesSent,
|
||||||
// 'byte'
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
transaction?.finish()
|
resolve(client_metrics)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onNewTrack({
|
this.onNewTrack({
|
||||||
@ -490,10 +445,6 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// During startup, we'll track the time from `connect` being called
|
|
||||||
// until the 'done' event fires.
|
|
||||||
let connectionStarted = new Date()
|
|
||||||
|
|
||||||
this.pc.addEventListener('datachannel', (event) => {
|
this.pc.addEventListener('datachannel', (event) => {
|
||||||
this.unreliableDataChannel = event.channel
|
this.unreliableDataChannel = event.channel
|
||||||
|
|
||||||
@ -537,6 +488,7 @@ export class EngineConnection {
|
|||||||
this.websocket = undefined
|
this.websocket = undefined
|
||||||
this.pc = undefined
|
this.pc = undefined
|
||||||
this.unreliableDataChannel = undefined
|
this.unreliableDataChannel = undefined
|
||||||
|
this.webrtcStatsCollector = undefined
|
||||||
|
|
||||||
this.onClose(this)
|
this.onClose(this)
|
||||||
this.ready = false
|
this.ready = false
|
||||||
@ -546,6 +498,8 @@ export class EngineConnection {
|
|||||||
export type EngineCommand = Models['WebSocketRequest_type']
|
export type EngineCommand = Models['WebSocketRequest_type']
|
||||||
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
||||||
|
|
||||||
|
type CommandTypes = Models['ModelingCmd_type']['type']
|
||||||
|
|
||||||
type UnreliableResponses = Extract<
|
type UnreliableResponses = Extract<
|
||||||
Models['OkModelingCmdResponse_type'],
|
Models['OkModelingCmdResponse_type'],
|
||||||
{ type: 'highlight_set_entity' }
|
{ type: 'highlight_set_entity' }
|
||||||
@ -687,15 +641,22 @@ export class EngineCommandManager {
|
|||||||
const resolve = command.resolve
|
const resolve = command.resolve
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
|
range: command.range,
|
||||||
|
commandType: command.commandType,
|
||||||
|
parentId: command.parentId ? command.parentId : undefined,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
id,
|
id,
|
||||||
|
commandType: command.commandType,
|
||||||
|
range: command.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
|
commandType: command?.commandType,
|
||||||
|
range: command?.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -747,8 +708,29 @@ export class EngineCommandManager {
|
|||||||
delete this.unreliableSubscriptions[event][id]
|
delete this.unreliableSubscriptions[event][id]
|
||||||
}
|
}
|
||||||
endSession() {
|
endSession() {
|
||||||
// this.websocket?.close()
|
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||||
// socket.off('command')
|
// we need to loop over them each individualy because if the engine doesn't recognise a single
|
||||||
|
// id the whole command fails.
|
||||||
|
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
|
||||||
|
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
|
||||||
|
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
|
||||||
|
// however all of the segments in the path are its children so there don't need to be deleted.
|
||||||
|
// this fact is very opaque in the api and docs (as to what should can be deleted).
|
||||||
|
// Using an array is the list is likely to grow.
|
||||||
|
'start_path',
|
||||||
|
]
|
||||||
|
if (!artifactTypesToDelete.includes(artifact.commandType)) return
|
||||||
|
|
||||||
|
const deletCmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'remove_scene_objects',
|
||||||
|
object_ids: [id],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.engineConnection?.send(deletCmd)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
cusorsSelected(selections: {
|
cusorsSelected(selections: {
|
||||||
otherSelections: Selections['otherSelections']
|
otherSelections: Selections['otherSelections']
|
||||||
@ -775,6 +757,13 @@ export class EngineCommandManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||||
|
if (
|
||||||
|
command.type === 'modeling_cmd_req' &&
|
||||||
|
command.cmd.type !== lastMessage
|
||||||
|
) {
|
||||||
|
console.log('sending command', command.cmd.type)
|
||||||
|
lastMessage = command.cmd.type
|
||||||
|
}
|
||||||
if (!this.engineConnection?.isReady()) {
|
if (!this.engineConnection?.isReady()) {
|
||||||
console.log('socket not ready')
|
console.log('socket not ready')
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@ -782,7 +771,8 @@ export class EngineCommandManager {
|
|||||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||||
const cmd = command.cmd
|
const cmd = command.cmd
|
||||||
if (
|
if (
|
||||||
cmd.type === 'camera_drag_move' &&
|
(cmd.type === 'camera_drag_move' ||
|
||||||
|
cmd.type === 'handle_mouse_drag_move') &&
|
||||||
this.engineConnection?.unreliableDataChannel
|
this.engineConnection?.unreliableDataChannel
|
||||||
) {
|
) {
|
||||||
cmd.sequence = this.outSequence
|
cmd.sequence = this.outSequence
|
||||||
@ -801,11 +791,20 @@ export class EngineCommandManager {
|
|||||||
JSON.stringify(command)
|
JSON.stringify(command)
|
||||||
)
|
)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
} else if (
|
||||||
|
cmd.type === 'mouse_move' &&
|
||||||
|
this.engineConnection.unreliableDataChannel
|
||||||
|
) {
|
||||||
|
cmd.sequence = this.outSequence
|
||||||
|
this.outSequence++
|
||||||
|
this.engineConnection?.unreliableDataChannel?.send(
|
||||||
|
JSON.stringify(command)
|
||||||
|
)
|
||||||
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
console.log('sending command', command)
|
|
||||||
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||||
this.engineConnection?.send(command)
|
this.engineConnection?.send(command)
|
||||||
return this.handlePendingCommand(command.cmd_id)
|
return this.handlePendingCommand(command.cmd_id, command.cmd)
|
||||||
}
|
}
|
||||||
sendModelingCommand({
|
sendModelingCommand({
|
||||||
id,
|
id,
|
||||||
@ -823,15 +822,35 @@ export class EngineCommandManager {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
this.engineConnection?.send(command)
|
this.engineConnection?.send(command)
|
||||||
return this.handlePendingCommand(id)
|
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||||
|
return this.handlePendingCommand(id, command?.cmd, range)
|
||||||
|
} else if (typeof command === 'string') {
|
||||||
|
const parseCommand: EngineCommand = JSON.parse(command)
|
||||||
|
if (parseCommand.type === 'modeling_cmd_req')
|
||||||
|
return this.handlePendingCommand(id, parseCommand?.cmd, range)
|
||||||
}
|
}
|
||||||
handlePendingCommand(id: string) {
|
throw 'shouldnt reach here'
|
||||||
|
}
|
||||||
|
handlePendingCommand(
|
||||||
|
id: string,
|
||||||
|
command: Models['ModelingCmd_type'],
|
||||||
|
range?: SourceRange
|
||||||
|
) {
|
||||||
let resolve: (val: any) => void = () => {}
|
let resolve: (val: any) => void = () => {}
|
||||||
const promise = new Promise((_resolve, reject) => {
|
const promise = new Promise((_resolve, reject) => {
|
||||||
resolve = _resolve
|
resolve = _resolve
|
||||||
})
|
})
|
||||||
|
const getParentId = (): string | undefined => {
|
||||||
|
if (command.type === 'extend_path') {
|
||||||
|
return command.path
|
||||||
|
}
|
||||||
|
// TODO handle other commands that have a parent
|
||||||
|
}
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
|
range: range || [0, 0],
|
||||||
type: 'pending',
|
type: 'pending',
|
||||||
|
commandType: command.type,
|
||||||
|
parentId: getParentId(),
|
||||||
promise,
|
promise,
|
||||||
resolve,
|
resolve,
|
||||||
}
|
}
|
||||||
@ -865,7 +884,10 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
return command.promise
|
return command.promise
|
||||||
}
|
}
|
||||||
async waitForAllCommands(): Promise<{
|
async waitForAllCommands(
|
||||||
|
ast?: Program,
|
||||||
|
programMemory?: ProgramMemory
|
||||||
|
): Promise<{
|
||||||
artifactMap: ArtifactMap
|
artifactMap: ArtifactMap
|
||||||
sourceRangeMap: SourceRangeMap
|
sourceRangeMap: SourceRangeMap
|
||||||
}> {
|
}> {
|
||||||
@ -874,9 +896,94 @@ export class EngineCommandManager {
|
|||||||
) as PendingCommand[]
|
) as PendingCommand[]
|
||||||
const proms = pendingCommands.map(({ promise }) => promise)
|
const proms = pendingCommands.map(({ promise }) => promise)
|
||||||
await Promise.all(proms)
|
await Promise.all(proms)
|
||||||
|
if (ast && programMemory) {
|
||||||
|
await this.fixIdMappings(ast, programMemory)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artifactMap: this.artifactMap,
|
artifactMap: this.artifactMap,
|
||||||
sourceRangeMap: this.sourceRangeMap,
|
sourceRangeMap: this.sourceRangeMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
|
||||||
|
/* This is a temporary solution since the cmd_ids that are sent through when
|
||||||
|
sending 'extend_path' ids are not used as the segment ids.
|
||||||
|
|
||||||
|
We have a way to back fill them with 'path_get_info', however this relies on one
|
||||||
|
the sketchGroup array and the segements array returned from the server to be in
|
||||||
|
the same length and order. plus it's super hacky, we first use the path_id to get
|
||||||
|
the source range of the pipe expression then use the name of the variable to get
|
||||||
|
the sketchGroup from programMemory.
|
||||||
|
|
||||||
|
I feel queezy about relying on all these steps to always line up.
|
||||||
|
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
|
||||||
|
We should get the cmd_ids to match with the segment ids and delete this method.
|
||||||
|
*/
|
||||||
|
const pathInfoProms = []
|
||||||
|
for (const [id, artifact] of Object.entries(this.artifactMap)) {
|
||||||
|
if (artifact.commandType === 'start_path') {
|
||||||
|
pathInfoProms.push(
|
||||||
|
this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'path_get_info',
|
||||||
|
path_id: id,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => ({
|
||||||
|
originalId: id,
|
||||||
|
segments: data?.data?.segments,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathInfos = await Promise.all(pathInfoProms)
|
||||||
|
pathInfos.forEach(({ originalId, segments }) => {
|
||||||
|
const originalArtifact = this.artifactMap[originalId]
|
||||||
|
if (!originalArtifact || originalArtifact.type === 'pending') {
|
||||||
|
console.log('problem')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const pipeExpPath = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
originalArtifact.range
|
||||||
|
)
|
||||||
|
const pipeExp = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
pipeExpPath,
|
||||||
|
'VariableDeclarator'
|
||||||
|
).node
|
||||||
|
if (pipeExp.type !== 'VariableDeclarator') {
|
||||||
|
console.log('problem', pipeExp, pipeExpPath, ast)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const variableName = pipeExp.id.name
|
||||||
|
const memoryItem = programMemory.root[variableName]
|
||||||
|
if (!memoryItem) {
|
||||||
|
console.log('problem', variableName, programMemory)
|
||||||
|
return
|
||||||
|
} else if (memoryItem.type !== 'SketchGroup') {
|
||||||
|
console.log('problem', memoryItem, programMemory)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const relevantSegments = segments.filter(
|
||||||
|
({ command_id }: { command_id: string | null }) => command_id
|
||||||
|
)
|
||||||
|
if (memoryItem.value.length !== relevantSegments.length) {
|
||||||
|
console.log('problem', memoryItem.value, relevantSegments)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let i = 0; i < relevantSegments.length; i++) {
|
||||||
|
const engineSegment = relevantSegments[i]
|
||||||
|
const memorySegment = memoryItem.value[i]
|
||||||
|
const oldId = memorySegment.__geoMeta.id
|
||||||
|
const artifact = this.artifactMap[oldId]
|
||||||
|
delete this.artifactMap[oldId]
|
||||||
|
delete this.sourceRangeMap[oldId]
|
||||||
|
this.artifactMap[engineSegment.command_id] = artifact
|
||||||
|
this.sourceRangeMap[engineSegment.command_id] = artifact.range
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
addNewSketchLn,
|
addNewSketchLn,
|
||||||
getYComponent,
|
getYComponent,
|
||||||
getXComponent,
|
getXComponent,
|
||||||
|
addCloseToPipe,
|
||||||
} from './sketch'
|
} from './sketch'
|
||||||
import { parser_wasm } from '../abstractSyntaxTree'
|
import { parser_wasm } from '../abstractSyntaxTree'
|
||||||
import { getNodePathFromSourceRange } from '../queryAst'
|
import { getNodePathFromSourceRange } from '../queryAst'
|
||||||
@ -97,12 +98,12 @@ describe('testing changeSketchArguments', () => {
|
|||||||
const lineAfterChange = 'lineTo([2, 3], %)'
|
const lineAfterChange = 'lineTo([2, 3], %)'
|
||||||
test('changeSketchArguments', async () => {
|
test('changeSketchArguments', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const genCode = (line: string) => `
|
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||||
const mySketch001 = startSketchAt([0, 0])
|
|
||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
show(mySketch001)`
|
show(mySketch001)
|
||||||
|
`
|
||||||
const code = genCode(lineToChange)
|
const code = genCode(lineToChange)
|
||||||
const expectedCode = genCode(lineAfterChange)
|
const expectedCode = genCode(lineAfterChange)
|
||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
@ -146,7 +147,7 @@ show(mySketch001)`
|
|||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
expect(sourceStart).toBe(66)
|
expect(sourceStart).toBe(66)
|
||||||
const { modifiedAst } = addNewSketchLn({
|
let { modifiedAst } = addNewSketchLn({
|
||||||
node: ast,
|
node: ast,
|
||||||
programMemory,
|
programMemory,
|
||||||
to: [2, 3],
|
to: [2, 3],
|
||||||
@ -160,13 +161,34 @@ show(mySketch001)`
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const expectedCode = `
|
let expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||||
const mySketch001 = startSketchAt([0, 0])
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
|> lineTo([2, 3], %)
|
|> lineTo([2, 3], %)
|
||||||
show(mySketch001)`
|
show(mySketch001)
|
||||||
|
`
|
||||||
|
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||||
|
|
||||||
|
modifiedAst = addCloseToPipe({
|
||||||
|
node: ast,
|
||||||
|
programMemory,
|
||||||
|
pathToNode: [
|
||||||
|
['body', ''],
|
||||||
|
[0, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
[0, 'index'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedCode = `const mySketch001 = startSketchAt([0, 0])
|
||||||
|
// |> rx(45, %)
|
||||||
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|
|> lineTo([0.46, -5.82], %)
|
||||||
|
|> close(%)
|
||||||
|
show(mySketch001)
|
||||||
|
`
|
||||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -175,12 +197,12 @@ describe('testing addTagForSketchOnFace', () => {
|
|||||||
it('needs to be in it', async () => {
|
it('needs to be in it', async () => {
|
||||||
const originalLine = 'lineTo([-1.59, -1.54], %)'
|
const originalLine = 'lineTo([-1.59, -1.54], %)'
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
const genCode = (line: string) => `
|
const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
|
||||||
const mySketch001 = startSketchAt([0, 0])
|
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)`
|
show(mySketch001)
|
||||||
|
`
|
||||||
const code = genCode(originalLine)
|
const code = genCode(originalLine)
|
||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
SketchGroup,
|
SketchGroup,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
|
MemoryItem,
|
||||||
} from '../executor'
|
} from '../executor'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
@ -19,8 +20,9 @@ import {
|
|||||||
getNodeFromPathCurry,
|
getNodeFromPathCurry,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
} from '../queryAst'
|
} from '../queryAst'
|
||||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
import { GuiModes, toolTips, ToolTip } from '../../useStore'
|
||||||
|
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||||
|
|
||||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||||
@ -55,7 +57,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createFirstArg(
|
export function createFirstArg(
|
||||||
sketchFn: TooTip,
|
sketchFn: ToolTip,
|
||||||
val: Value | [Value, Value] | [Value, Value, Value],
|
val: Value | [Value, Value] | [Value, Value, Value],
|
||||||
tag?: Value
|
tag?: Value
|
||||||
): Value {
|
): Value {
|
||||||
@ -185,7 +187,7 @@ export const line: SketchLineHelper = {
|
|||||||
createCallback,
|
createCallback,
|
||||||
}) => {
|
}) => {
|
||||||
const _node = { ...node }
|
const _node = { ...node }
|
||||||
const { node: pipe } = getNodeFromPath<PipeExpression>(
|
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
|
||||||
_node,
|
_node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
'PipeExpression'
|
'PipeExpression'
|
||||||
@ -197,12 +199,12 @@ export const line: SketchLineHelper = {
|
|||||||
)
|
)
|
||||||
const variableName = varDec.id.name
|
const variableName = varDec.id.name
|
||||||
const sketch = previousProgramMemory?.root?.[variableName]
|
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 newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 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 { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||||
const { callExp, valueUsedInTransform } = createCallback(
|
const { callExp, valueUsedInTransform } = createCallback(
|
||||||
[newXVal, newYVal],
|
[newXVal, newYVal],
|
||||||
@ -220,7 +222,11 @@ export const line: SketchLineHelper = {
|
|||||||
createArrayExpression([newXVal, newYVal]),
|
createArrayExpression([newXVal, newYVal]),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
|
if (pipe.type === 'PipeExpression') {
|
||||||
pipe.body = [...pipe.body, callExp]
|
pipe.body = [...pipe.body, callExp]
|
||||||
|
} else {
|
||||||
|
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: _node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -238,22 +244,10 @@ export const line: SketchLineHelper = {
|
|||||||
createLiteral(roundOff(to[1] - from[1], 2)),
|
createLiteral(roundOff(to[1] - from[1], 2)),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (
|
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||||
callExpression.arguments?.[0].type === 'Literal' &&
|
|
||||||
callExpression.arguments?.[0].value === 'default'
|
|
||||||
) {
|
|
||||||
callExpression.arguments[0] = toArrExp
|
|
||||||
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
|
||||||
const toProp = callExpression.arguments?.[0].properties?.find(
|
const toProp = callExpression.arguments?.[0].properties?.find(
|
||||||
({ key }) => key.name === 'to'
|
({ key }) => key.name === 'to'
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
toProp &&
|
|
||||||
toProp.value.type === 'Literal' &&
|
|
||||||
toProp.value.value === 'default'
|
|
||||||
) {
|
|
||||||
toProp.value = toArrExp
|
|
||||||
}
|
|
||||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||||
} else {
|
} else {
|
||||||
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
||||||
@ -301,7 +295,7 @@ export const xLineTo: SketchLineHelper = {
|
|||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
const newX = createLiteral(roundOff(to[0], 2))
|
const newX = createLiteral(roundOff(to[0], 2))
|
||||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||||
callExpression.arguments[0] = newX
|
callExpression.arguments[0] = newX
|
||||||
} else {
|
} else {
|
||||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
|
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
|
||||||
@ -349,7 +343,7 @@ export const yLineTo: SketchLineHelper = {
|
|||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
const newY = createLiteral(roundOff(to[1], 2))
|
const newY = createLiteral(roundOff(to[1], 2))
|
||||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||||
callExpression.arguments[0] = newY
|
callExpression.arguments[0] = newY
|
||||||
} else {
|
} else {
|
||||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
|
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
|
||||||
@ -399,7 +393,7 @@ export const xLine: SketchLineHelper = {
|
|||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
||||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||||
callExpression.arguments[0] = newX
|
callExpression.arguments[0] = newX
|
||||||
} else {
|
} else {
|
||||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
|
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
|
||||||
@ -443,7 +437,7 @@ export const yLine: SketchLineHelper = {
|
|||||||
pathToNode
|
pathToNode
|
||||||
)
|
)
|
||||||
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
||||||
if (callExpression.arguments?.[0]?.type === 'Literal') {
|
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||||
callExpression.arguments[0] = newY
|
callExpression.arguments[0] = newY
|
||||||
} else {
|
} else {
|
||||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
|
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
|
||||||
@ -546,7 +540,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
|||||||
)
|
)
|
||||||
const variableName = varDec.id.name
|
const variableName = varDec.id.name
|
||||||
const sketch = previousProgramMemory?.root?.[variableName]
|
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 angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||||
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
||||||
const newLine = createCallback
|
const newLine = createCallback
|
||||||
@ -619,7 +613,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
|||||||
)
|
)
|
||||||
const variableName = varDec.id.name
|
const variableName = varDec.id.name
|
||||||
const sketch = previousProgramMemory?.root?.[variableName]
|
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 angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||||
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
||||||
@ -876,7 +870,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
|||||||
const varName = varDec.declarations[0].id.name
|
const varName = varDec.declarations[0].id.name
|
||||||
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
|
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
|
||||||
const intersectPath = sketchGroup.value.find(
|
const intersectPath = sketchGroup.value.find(
|
||||||
({ name }) => name === intersectTagName
|
({ name }: Path) => name === intersectTagName
|
||||||
)
|
)
|
||||||
let offset = 0
|
let offset = 0
|
||||||
if (intersectPath) {
|
if (intersectPath) {
|
||||||
@ -949,17 +943,29 @@ interface CreateLineFnCallArgs {
|
|||||||
programMemory: ProgramMemory
|
programMemory: ProgramMemory
|
||||||
to: [number, number]
|
to: [number, number]
|
||||||
from: [number, number]
|
from: [number, number]
|
||||||
fnName: TooTip
|
fnName: ToolTip
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function compareVec2Epsilon(
|
||||||
|
vec1: [number, number],
|
||||||
|
vec2: [number, number]
|
||||||
|
) {
|
||||||
|
const compareEpsilon = 0.015625 // or 2^-6
|
||||||
|
const xDifference = Math.abs(vec1[0] - vec2[0])
|
||||||
|
const yDifference = Math.abs(vec1[0] - vec2[0])
|
||||||
|
return xDifference < compareEpsilon && yDifference < compareEpsilon
|
||||||
|
}
|
||||||
|
|
||||||
export function addNewSketchLn({
|
export function addNewSketchLn({
|
||||||
node: _node,
|
node: _node,
|
||||||
programMemory: previousProgramMemory,
|
programMemory: previousProgramMemory,
|
||||||
to,
|
to,
|
||||||
fnName,
|
fnName,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
}: Omit<CreateLineFnCallArgs, 'from'>): { modifiedAst: Program } {
|
}: Omit<CreateLineFnCallArgs, 'from'>): {
|
||||||
|
modifiedAst: Program
|
||||||
|
} {
|
||||||
const node = JSON.parse(JSON.stringify(_node))
|
const node = JSON.parse(JSON.stringify(_node))
|
||||||
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
|
||||||
if (!add || !updateArgs) throw new Error('not a sketch line helper')
|
if (!add || !updateArgs) throw new Error('not a sketch line helper')
|
||||||
@ -968,62 +974,15 @@ export function addNewSketchLn({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
const { node: pipeExp, shallowPath: pipePath } =
|
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
||||||
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
|
PipeExpression | CallExpression
|
||||||
const maybeStartSketchAt = pipeExp.body.find(
|
>(node, pathToNode, 'PipeExpression')
|
||||||
(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 variableName = varDec.id.name
|
const variableName = varDec.id.name
|
||||||
const sketch = previousProgramMemory?.root?.[variableName]
|
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 last = sketch.value[sketch.value.length - 1] || sketch.start
|
||||||
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 from = last.to
|
const from = last.to
|
||||||
|
|
||||||
return add({
|
return add({
|
||||||
node,
|
node,
|
||||||
previousProgramMemory,
|
previousProgramMemory,
|
||||||
@ -1034,6 +993,29 @@ export function addNewSketchLn({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addCloseToPipe({
|
||||||
|
node,
|
||||||
|
pathToNode,
|
||||||
|
}: {
|
||||||
|
node: Program
|
||||||
|
programMemory: ProgramMemory
|
||||||
|
pathToNode: PathToNode
|
||||||
|
}) {
|
||||||
|
const _node = { ...node }
|
||||||
|
const closeExpression = createCallExpression('close', [
|
||||||
|
createPipeSubstitution(),
|
||||||
|
])
|
||||||
|
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||||
|
_node,
|
||||||
|
pathToNode,
|
||||||
|
'PipeExpression'
|
||||||
|
).node
|
||||||
|
if (pipeExpression.type !== 'PipeExpression')
|
||||||
|
throw new Error('not a pipe expression')
|
||||||
|
pipeExpression.body = [...pipeExpression.body, closeExpression]
|
||||||
|
return _node
|
||||||
|
}
|
||||||
|
|
||||||
export function replaceSketchLine({
|
export function replaceSketchLine({
|
||||||
node,
|
node,
|
||||||
programMemory,
|
programMemory,
|
||||||
@ -1047,7 +1029,7 @@ export function replaceSketchLine({
|
|||||||
node: Program
|
node: Program
|
||||||
programMemory: ProgramMemory
|
programMemory: ProgramMemory
|
||||||
sourceRange: SourceRange
|
sourceRange: SourceRange
|
||||||
fnName: TooTip
|
fnName: ToolTip
|
||||||
to: [number, number]
|
to: [number, number]
|
||||||
from: [number, number]
|
from: [number, number]
|
||||||
createCallback: TransformCallback
|
createCallback: TransformCallback
|
||||||
@ -1089,10 +1071,11 @@ export function addTagForSketchOnFace(
|
|||||||
|
|
||||||
function isAngleLiteral(lineArugement: Value): boolean {
|
function isAngleLiteral(lineArugement: Value): boolean {
|
||||||
return lineArugement?.type === 'ArrayExpression'
|
return lineArugement?.type === 'ArrayExpression'
|
||||||
? lineArugement.elements[0].type === 'Literal'
|
? isLiteralArrayOrStatic(lineArugement.elements[0])
|
||||||
: lineArugement?.type === 'ObjectExpression'
|
: lineArugement?.type === 'ObjectExpression'
|
||||||
? lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
? isLiteralArrayOrStatic(
|
||||||
.type === 'Literal'
|
lineArugement.properties.find(({ key }) => key.name === 'angle')?.value
|
||||||
|
)
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1198,14 +1181,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
|||||||
} {
|
} {
|
||||||
// used for lineTo, line
|
// used for lineTo, line
|
||||||
const firstArg = callExpression.arguments[0]
|
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') {
|
if (firstArg.type === 'ArrayExpression') {
|
||||||
return { val: [firstArg.elements[0], firstArg.elements[1]] }
|
return { val: [firstArg.elements[0], firstArg.elements[1]] }
|
||||||
}
|
}
|
||||||
@ -1215,8 +1190,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
|||||||
if (to?.type === 'ArrayExpression') {
|
if (to?.type === 'ArrayExpression') {
|
||||||
const [x, y] = to.elements
|
const [x, y] = to.elements
|
||||||
return { val: [x, y], tag }
|
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')
|
throw new Error('expected ArrayExpression or ObjectExpression')
|
||||||
@ -1235,7 +1208,7 @@ function getFirstArgValuesForAngleFns(callExpression: CallExpression): {
|
|||||||
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
|
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
|
||||||
const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value
|
const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value
|
||||||
const secondArgName = ['angledLineToX', 'angledLineToY'].includes(
|
const secondArgName = ['angledLineToX', 'angledLineToY'].includes(
|
||||||
callExpression?.callee?.name as TooTip
|
callExpression?.callee?.name as ToolTip
|
||||||
)
|
)
|
||||||
? 'to'
|
? 'to'
|
||||||
: 'length'
|
: 'length'
|
||||||
|
@ -401,6 +401,11 @@ show(part001)`
|
|||||||
programMemory.root['part001'] as SketchGroup,
|
programMemory.root['part001'] as SketchGroup,
|
||||||
[index, index]
|
[index, index]
|
||||||
).segment
|
).segment
|
||||||
expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' })
|
expect(segment).toEqual({
|
||||||
|
to: [0, 0.04],
|
||||||
|
from: [0, 0.04],
|
||||||
|
name: '',
|
||||||
|
type: 'base',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { TooTip, toolTips } from '../../useStore'
|
import { ToolTip, toolTips } from '../../useStore'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
} from '../abstractSyntaxTreeTypes'
|
} from '../abstractSyntaxTreeTypes'
|
||||||
import { SketchGroup, SourceRange } from '../executor'
|
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||||
|
|
||||||
export function getSketchSegmentFromSourceRange(
|
export function getSketchSegmentFromSourceRange(
|
||||||
sketchGroup: SketchGroup,
|
sketchGroup: SketchGroup,
|
||||||
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
|
|||||||
startSourceRange[1] >= rangeEnd &&
|
startSourceRange[1] >= rangeEnd &&
|
||||||
sketchGroup.start
|
sketchGroup.start
|
||||||
)
|
)
|
||||||
return { segment: sketchGroup.start, index: -1 }
|
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
|
||||||
|
|
||||||
const lineIndex = sketchGroup.value.findIndex(
|
const lineIndex = sketchGroup.value.findIndex(
|
||||||
({ __geoMeta: { sourceRange } }) =>
|
({ __geoMeta: { sourceRange } }: Path) =>
|
||||||
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
|
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
|
||||||
)
|
)
|
||||||
const line = sketchGroup.value[lineIndex]
|
const line = sketchGroup.value[lineIndex]
|
||||||
@ -67,7 +67,10 @@ export function isSketchVariablesLinked(
|
|||||||
return false
|
return false
|
||||||
const firstCallExp = // first in pipe expression or just the call expression
|
const firstCallExp = // first in pipe expression or just the call expression
|
||||||
init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression)
|
init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression)
|
||||||
if (!firstCallExp || !toolTips.includes(firstCallExp?.callee?.name as TooTip))
|
if (
|
||||||
|
!firstCallExp ||
|
||||||
|
!toolTips.includes(firstCallExp?.callee?.name as ToolTip)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
// convention for sketch fns is that the second argument is the sketch group
|
// convention for sketch fns is that the second argument is the sketch group
|
||||||
const secondArg = firstCallExp?.arguments[1]
|
const secondArg = firstCallExp?.arguments[1]
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
getConstraintLevelFromSourceRange,
|
getConstraintLevelFromSourceRange,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
import { initPromise } from '../rust'
|
import { initPromise } from '../rust'
|
||||||
import { Selections, TooTip } from '../../useStore'
|
import { Selections, ToolTip } from '../../useStore'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
import { recast } from '../../lang/recast'
|
import { recast } from '../../lang/recast'
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ function getConstraintTypeFromSourceHelper(
|
|||||||
Value,
|
Value,
|
||||||
Value
|
Value
|
||||||
]
|
]
|
||||||
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
|
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
||||||
return getConstraintType(args, fnName)
|
return getConstraintType(args, fnName)
|
||||||
}
|
}
|
||||||
function getConstraintTypeFromSourceHelper2(
|
function getConstraintTypeFromSourceHelper2(
|
||||||
@ -76,7 +76,7 @@ function getConstraintTypeFromSourceHelper2(
|
|||||||
): ReturnType<typeof getConstraintType> {
|
): ReturnType<typeof getConstraintType> {
|
||||||
const ast = parser_wasm(code)
|
const ast = parser_wasm(code)
|
||||||
const arg = (ast.body[0] as any).expression.arguments[0] as Value
|
const arg = (ast.body[0] as any).expression.arguments[0] as Value
|
||||||
const fnName = (ast.body[0] as any).expression.callee.name as TooTip
|
const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
|
||||||
return getConstraintType(arg, fnName)
|
return getConstraintType(arg, fnName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
const expectModifiedScript = `const myVar = 3
|
const expectModifiedScript = `const myVar = 3
|
||||||
const myVar2 = 5
|
const myVar2 = 5
|
||||||
const myVar3 = 6
|
const myVar3 = 6
|
||||||
@ -195,7 +196,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> yLine(segLen('seg01', %), %) // ln-yLine-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
|
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
||||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
it('should transform the ast', async () => {
|
it('should transform the ast', async () => {
|
||||||
const ast = parser_wasm(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
it('should transform horizontal lines the ast', async () => {
|
it('should transform horizontal lines the ast', async () => {
|
||||||
const expectModifiedScript = `const myVar = 2
|
const expectModifiedScript = `const myVar = 2
|
||||||
const myVar2 = 12
|
const myVar2 = 12
|
||||||
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
const ast = parser_wasm(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
const ast = parser_wasm(inputScript)
|
const ast = parser_wasm(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
|
|||||||
|> line([0.45, 1.46], %) // free
|
|> line([0.45, 1.46], %) // free
|
||||||
|> line([myVar, 0.01], %) // xRelative
|
|> line([myVar, 0.01], %) // xRelative
|
||||||
|> line([0.7, myVar], %) // yRelative
|
|> line([0.7, myVar], %) // yRelative
|
||||||
show(part001)`
|
show(part001)
|
||||||
|
`
|
||||||
it('testing for free to horizontal and vertical distance', async () => {
|
it('testing for free to horizontal and vertical distance', async () => {
|
||||||
const expectedHorizontalCode = await helperThing(
|
const expectedHorizontalCode = await helperThing(
|
||||||
inputScript,
|
inputScript,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TransformCallback } from './stdTypes'
|
import { TransformCallback } from './stdTypes'
|
||||||
import { Selections, toolTips, TooTip, Selection } from '../../useStore'
|
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
Program,
|
Program,
|
||||||
@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
|||||||
import { PathToNode, ProgramMemory } from '../executor'
|
import { PathToNode, ProgramMemory } from '../executor'
|
||||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||||
|
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
|
||||||
|
|
||||||
type LineInputsType =
|
type LineInputsType =
|
||||||
| 'xAbsolute'
|
| 'xAbsolute'
|
||||||
@ -53,7 +54,7 @@ export type ConstraintType =
|
|||||||
| 'setAngleBetween'
|
| 'setAngleBetween'
|
||||||
|
|
||||||
function createCallWrapper(
|
function createCallWrapper(
|
||||||
a: TooTip,
|
a: ToolTip,
|
||||||
val: [Value, Value] | Value,
|
val: [Value, Value] | Value,
|
||||||
tag?: Value,
|
tag?: Value,
|
||||||
valueUsedInTransform?: number
|
valueUsedInTransform?: number
|
||||||
@ -100,7 +101,7 @@ function intersectCallWrapper({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TransformInfo = {
|
export type TransformInfo = {
|
||||||
tooltip: TooTip
|
tooltip: ToolTip
|
||||||
createNode: (a: {
|
createNode: (a: {
|
||||||
varValA: Value // x / angle
|
varValA: Value // x / angle
|
||||||
varValB: Value // y / length or x y for angledLineOfXlength etc
|
varValB: Value // y / length or x y for angledLineOfXlength etc
|
||||||
@ -111,7 +112,7 @@ export type TransformInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TransformMap = {
|
type TransformMap = {
|
||||||
[key in TooTip]?: {
|
[key in ToolTip]?: {
|
||||||
[key in LineInputsType | 'free']?: {
|
[key in LineInputsType | 'free']?: {
|
||||||
[key in ConstraintType]?: TransformInfo
|
[key in ConstraintType]?: TransformInfo
|
||||||
}
|
}
|
||||||
@ -1094,12 +1095,12 @@ export function getRemoveConstraintsTransform(
|
|||||||
sketchFnExp: CallExpression,
|
sketchFnExp: CallExpression,
|
||||||
constraintType: ConstraintType
|
constraintType: ConstraintType
|
||||||
): TransformInfo | false {
|
): TransformInfo | false {
|
||||||
let name = sketchFnExp.callee.name as TooTip
|
let name = sketchFnExp.callee.name as ToolTip
|
||||||
if (!toolTips.includes(name)) {
|
if (!toolTips.includes(name)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const xyLineMap: {
|
const xyLineMap: {
|
||||||
[key in TooTip]?: TooTip
|
[key in ToolTip]?: ToolTip
|
||||||
} = {
|
} = {
|
||||||
xLine: 'line',
|
xLine: 'line',
|
||||||
yLine: 'line',
|
yLine: 'line',
|
||||||
@ -1136,27 +1137,18 @@ export function getRemoveConstraintsTransform(
|
|||||||
|
|
||||||
// check if the function is locked down and so can't be transformed
|
// check if the function is locked down and so can't be transformed
|
||||||
const firstArg = getFirstArg(sketchFnExp)
|
const firstArg = getFirstArg(sketchFnExp)
|
||||||
if (Array.isArray(firstArg.val)) {
|
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||||
const [a, b] = firstArg.val
|
|
||||||
if (a?.type !== 'Literal' || b?.type !== 'Literal') {
|
|
||||||
return transformInfo
|
return transformInfo
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (firstArg.val?.type !== 'Literal') {
|
|
||||||
return transformInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the function has no constraints
|
// check if the function has no constraints
|
||||||
const isTwoValFree =
|
const isTwoValFree =
|
||||||
Array.isArray(firstArg.val) &&
|
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||||
firstArg.val?.[0]?.type === 'Literal' &&
|
|
||||||
firstArg.val?.[1]?.type === 'Literal'
|
|
||||||
if (isTwoValFree) {
|
if (isTwoValFree) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const isOneValFree =
|
const isOneValFree =
|
||||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||||
if (isOneValFree) {
|
if (isOneValFree) {
|
||||||
return transformInfo
|
return transformInfo
|
||||||
}
|
}
|
||||||
@ -1175,37 +1167,24 @@ function getTransformMapPath(
|
|||||||
constraintType: ConstraintType
|
constraintType: ConstraintType
|
||||||
):
|
):
|
||||||
| {
|
| {
|
||||||
toolTip: TooTip
|
toolTip: ToolTip
|
||||||
lineInputType: LineInputsType | 'free'
|
lineInputType: LineInputsType | 'free'
|
||||||
constraintType: ConstraintType
|
constraintType: ConstraintType
|
||||||
}
|
}
|
||||||
| false {
|
| false {
|
||||||
const name = sketchFnExp.callee.name as TooTip
|
const name = sketchFnExp.callee.name as ToolTip
|
||||||
if (!toolTips.includes(name)) {
|
if (!toolTips.includes(name)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the function is locked down and so can't be transformed
|
// check if the function is locked down and so can't be transformed
|
||||||
const firstArg = getFirstArg(sketchFnExp)
|
const firstArg = getFirstArg(sketchFnExp)
|
||||||
if (Array.isArray(firstArg.val)) {
|
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||||
const [a, b] = firstArg.val
|
|
||||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (firstArg.val?.type !== 'Literal') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the function has no constraints
|
// check if the function has no constraints
|
||||||
const isTwoValFree =
|
if (isLiteralArrayOrStatic(firstArg.val)) {
|
||||||
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) {
|
|
||||||
const info = transformMap?.[name]?.free?.[constraintType]
|
const info = transformMap?.[name]?.free?.[constraintType]
|
||||||
if (info)
|
if (info)
|
||||||
return {
|
return {
|
||||||
@ -1246,7 +1225,7 @@ export function getTransformInfo(
|
|||||||
|
|
||||||
export function getConstraintType(
|
export function getConstraintType(
|
||||||
val: Value | [Value, Value] | [Value, Value, Value],
|
val: Value | [Value, Value] | [Value, Value, Value],
|
||||||
fnName: TooTip
|
fnName: ToolTip
|
||||||
): LineInputsType | null {
|
): LineInputsType | null {
|
||||||
// this function assumes that for two val sketch functions that one arg is locked down not both
|
// this function assumes that for two val sketch functions that one arg is locked down not both
|
||||||
// and for one val sketch functions that the arg is NOT locked down
|
// and for one val sketch functions that the arg is NOT locked down
|
||||||
@ -1259,7 +1238,7 @@ export function getConstraintType(
|
|||||||
if (fnName === 'xLineTo') return 'yAbsolute'
|
if (fnName === 'xLineTo') return 'yAbsolute'
|
||||||
if (fnName === 'yLineTo') return 'xAbsolute'
|
if (fnName === 'yLineTo') return 'xAbsolute'
|
||||||
} else {
|
} else {
|
||||||
const isFirstArgLockedDown = val?.[0]?.type !== 'Literal'
|
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
|
||||||
if (fnName === 'line')
|
if (fnName === 'line')
|
||||||
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
|
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
|
||||||
if (fnName === 'lineTo')
|
if (fnName === 'lineTo')
|
||||||
@ -1452,7 +1431,7 @@ export function transformAstSketchLines({
|
|||||||
|
|
||||||
const varName = varDec.id.name
|
const varName = varDec.id.name
|
||||||
const sketchGroup = programMemory.root?.[varName]
|
const sketchGroup = programMemory.root?.[varName]
|
||||||
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
|
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||||
throw new Error('not a sketch group')
|
throw new Error('not a sketch group')
|
||||||
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
||||||
const referencedSegment = referencedSegmentRange
|
const referencedSegment = referencedSegmentRange
|
||||||
@ -1466,7 +1445,7 @@ export function transformAstSketchLines({
|
|||||||
programMemory,
|
programMemory,
|
||||||
sourceRange: range,
|
sourceRange: range,
|
||||||
referencedSegment,
|
referencedSegment,
|
||||||
fnName: transformTo || (callExp.callee.name as TooTip),
|
fnName: transformTo || (callExp.callee.name as ToolTip),
|
||||||
to,
|
to,
|
||||||
from,
|
from,
|
||||||
createCallback: callBack({
|
createCallback: callBack({
|
||||||
@ -1532,29 +1511,52 @@ export function getConstraintLevelFromSourceRange(
|
|||||||
getNodePathFromSourceRange(ast, cursorRange),
|
getNodePathFromSourceRange(ast, cursorRange),
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
)
|
)
|
||||||
const name = sketchFnExp?.callee?.name as TooTip
|
const name = sketchFnExp?.callee?.name as ToolTip
|
||||||
if (!toolTips.includes(name)) return 'free'
|
if (!toolTips.includes(name)) return 'free'
|
||||||
|
|
||||||
const firstArg = getFirstArg(sketchFnExp)
|
const firstArg = getFirstArg(sketchFnExp)
|
||||||
|
|
||||||
// check if the function is fully constrained
|
// check if the function is fully constrained
|
||||||
if (Array.isArray(firstArg.val)) {
|
if (isNotLiteralArrayOrStatic(firstArg.val)) {
|
||||||
const [a, b] = firstArg.val
|
return 'full'
|
||||||
if (a?.type !== 'Literal' && b?.type !== 'Literal') return 'full'
|
|
||||||
} else {
|
|
||||||
if (firstArg.val?.type !== 'Literal') return 'full'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the function has no constraints
|
// check if the function has no constraints
|
||||||
const isTwoValFree =
|
const isTwoValFree =
|
||||||
Array.isArray(firstArg.val) &&
|
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||||
firstArg.val?.[0]?.type === 'Literal' &&
|
|
||||||
firstArg.val?.[1]?.type === 'Literal'
|
|
||||||
const isOneValFree =
|
const isOneValFree =
|
||||||
!Array.isArray(firstArg.val) && firstArg.val?.type === 'Literal'
|
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||||
|
|
||||||
if (isTwoValFree) return 'free'
|
if (isTwoValFree) return 'free'
|
||||||
if (isOneValFree) return 'partial'
|
if (isOneValFree) return 'partial'
|
||||||
|
|
||||||
return 'partial'
|
return 'partial'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLiteralArrayOrStatic(
|
||||||
|
val: Value | [Value, Value] | [Value, Value, Value] | undefined
|
||||||
|
): boolean {
|
||||||
|
if (!val) return false
|
||||||
|
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const [a, b] = val
|
||||||
|
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
val.type === 'Literal' ||
|
||||||
|
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNotLiteralArrayOrStatic(
|
||||||
|
val: Value | [Value, Value] | [Value, Value, Value]
|
||||||
|
): boolean {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const [a, b] = val
|
||||||
|
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
|
||||||
|
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ProgramMemory, Path, SourceRange } from '../executor'
|
import { ProgramMemory, Path, SourceRange } from '../executor'
|
||||||
import { Program, Value } from '../abstractSyntaxTreeTypes'
|
import { Program, Value } from '../abstractSyntaxTreeTypes'
|
||||||
import { TooTip } from '../../useStore'
|
import { ToolTip } from '../../useStore'
|
||||||
import { PathToNode } from '../executor'
|
import { PathToNode } from '../executor'
|
||||||
import { EngineCommandManager } from './engineConnection'
|
import { EngineCommandManager } from './engineConnection'
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export type TransformCallback = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SketchCallTransfromMap = {
|
export type SketchCallTransfromMap = {
|
||||||
[key in TooTip]: TransformCallback
|
[key in ToolTip]: TransformCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SketchLineHelper {
|
export interface SketchLineHelper {
|
||||||
|
@ -110,7 +110,7 @@ const yi=45`
|
|||||||
"brace ')' from 17 to 18",
|
"brace ')' from 17 to 18",
|
||||||
])
|
])
|
||||||
expect(stringSummaryLexer('fn funcName = (param1, param2) => {}')).toEqual([
|
expect(stringSummaryLexer('fn funcName = (param1, param2) => {}')).toEqual([
|
||||||
"word 'fn' from 0 to 2",
|
"keyword 'fn' from 0 to 2",
|
||||||
"whitespace ' ' from 2 to 3",
|
"whitespace ' ' from 2 to 3",
|
||||||
"word 'funcName' from 3 to 11",
|
"word 'funcName' from 3 to 11",
|
||||||
"whitespace ' ' from 11 to 12",
|
"whitespace ' ' from 11 to 12",
|
||||||
@ -131,10 +131,12 @@ const yi=45`
|
|||||||
})
|
})
|
||||||
it('test negative and decimal numbers', () => {
|
it('test negative and decimal numbers', () => {
|
||||||
expect(stringSummaryLexer('-1')).toEqual([
|
expect(stringSummaryLexer('-1')).toEqual([
|
||||||
"number '-1' from 0 to 2",
|
"operator '-' from 0 to 1",
|
||||||
|
"number '1' from 1 to 2",
|
||||||
])
|
])
|
||||||
expect(stringSummaryLexer('-1.5')).toEqual([
|
expect(stringSummaryLexer('-1.5')).toEqual([
|
||||||
"number '-1.5' from 0 to 4",
|
"operator '-' from 0 to 1",
|
||||||
|
"number '1.5' from 1 to 4",
|
||||||
])
|
])
|
||||||
expect(stringSummaryLexer('1.5')).toEqual([
|
expect(stringSummaryLexer('1.5')).toEqual([
|
||||||
"number '1.5' from 0 to 3",
|
"number '1.5' from 0 to 3",
|
||||||
@ -158,10 +160,12 @@ const yi=45`
|
|||||||
"whitespace ' ' from 3 to 4",
|
"whitespace ' ' from 3 to 4",
|
||||||
"operator '+' from 4 to 5",
|
"operator '+' from 4 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"number '-2.5' from 6 to 10",
|
"operator '-' from 6 to 7",
|
||||||
|
"number '2.5' from 7 to 10",
|
||||||
])
|
])
|
||||||
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
|
expect(stringSummaryLexer('-1.5 + 2.5')).toEqual([
|
||||||
"number '-1.5' from 0 to 4",
|
"operator '-' from 0 to 1",
|
||||||
|
"number '1.5' from 1 to 4",
|
||||||
"whitespace ' ' from 4 to 5",
|
"whitespace ' ' from 4 to 5",
|
||||||
"operator '+' from 5 to 6",
|
"operator '+' from 5 to 6",
|
||||||
"whitespace ' ' from 6 to 7",
|
"whitespace ' ' from 6 to 7",
|
||||||
@ -203,7 +207,7 @@ const yi=45`
|
|||||||
it('testing array declaration', () => {
|
it('testing array declaration', () => {
|
||||||
const result = stringSummaryLexer(`const yo = [1, 2]`)
|
const result = stringSummaryLexer(`const yo = [1, 2]`)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
"word 'const' from 0 to 5",
|
"keyword 'const' from 0 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"word 'yo' from 6 to 8",
|
"word 'yo' from 6 to 8",
|
||||||
"whitespace ' ' from 8 to 9",
|
"whitespace ' ' from 8 to 9",
|
||||||
@ -220,7 +224,7 @@ const yi=45`
|
|||||||
it('testing object declaration', () => {
|
it('testing object declaration', () => {
|
||||||
const result = stringSummaryLexer(`const yo = {key: 'value'}`)
|
const result = stringSummaryLexer(`const yo = {key: 'value'}`)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
"word 'const' from 0 to 5",
|
"keyword 'const' from 0 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"word 'yo' from 6 to 8",
|
"word 'yo' from 6 to 8",
|
||||||
"whitespace ' ' from 8 to 9",
|
"whitespace ' ' from 8 to 9",
|
||||||
@ -241,7 +245,7 @@ const prop2 = yo['key']
|
|||||||
const key = 'key'
|
const key = 'key'
|
||||||
const prop3 = yo[key]`)
|
const prop3 = yo[key]`)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
"word 'const' from 0 to 5",
|
"keyword 'const' from 0 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"word 'yo' from 6 to 8",
|
"word 'yo' from 6 to 8",
|
||||||
"whitespace ' ' from 8 to 9",
|
"whitespace ' ' from 8 to 9",
|
||||||
@ -254,7 +258,7 @@ const prop3 = yo[key]`)
|
|||||||
"string ''value'' from 17 to 24",
|
"string ''value'' from 17 to 24",
|
||||||
"brace '}' from 24 to 25",
|
"brace '}' from 24 to 25",
|
||||||
"whitespace '\n' from 25 to 26",
|
"whitespace '\n' from 25 to 26",
|
||||||
"word 'const' from 26 to 31",
|
"keyword 'const' from 26 to 31",
|
||||||
"whitespace ' ' from 31 to 32",
|
"whitespace ' ' from 31 to 32",
|
||||||
"word 'prop' from 32 to 36",
|
"word 'prop' from 32 to 36",
|
||||||
"whitespace ' ' from 36 to 37",
|
"whitespace ' ' from 36 to 37",
|
||||||
@ -264,7 +268,7 @@ const prop3 = yo[key]`)
|
|||||||
"period '.' from 41 to 42",
|
"period '.' from 41 to 42",
|
||||||
"word 'key' from 42 to 45",
|
"word 'key' from 42 to 45",
|
||||||
"whitespace '\n' from 45 to 46",
|
"whitespace '\n' from 45 to 46",
|
||||||
"word 'const' from 46 to 51",
|
"keyword 'const' from 46 to 51",
|
||||||
"whitespace ' ' from 51 to 52",
|
"whitespace ' ' from 51 to 52",
|
||||||
"word 'prop2' from 52 to 57",
|
"word 'prop2' from 52 to 57",
|
||||||
"whitespace ' ' from 57 to 58",
|
"whitespace ' ' from 57 to 58",
|
||||||
@ -275,7 +279,7 @@ const prop3 = yo[key]`)
|
|||||||
"string ''key'' from 63 to 68",
|
"string ''key'' from 63 to 68",
|
||||||
"brace ']' from 68 to 69",
|
"brace ']' from 68 to 69",
|
||||||
"whitespace '\n' from 69 to 70",
|
"whitespace '\n' from 69 to 70",
|
||||||
"word 'const' from 70 to 75",
|
"keyword 'const' from 70 to 75",
|
||||||
"whitespace ' ' from 75 to 76",
|
"whitespace ' ' from 75 to 76",
|
||||||
"word 'key' from 76 to 79",
|
"word 'key' from 76 to 79",
|
||||||
"whitespace ' ' from 79 to 80",
|
"whitespace ' ' from 79 to 80",
|
||||||
@ -283,7 +287,7 @@ const prop3 = yo[key]`)
|
|||||||
"whitespace ' ' from 81 to 82",
|
"whitespace ' ' from 81 to 82",
|
||||||
"string ''key'' from 82 to 87",
|
"string ''key'' from 82 to 87",
|
||||||
"whitespace '\n' from 87 to 88",
|
"whitespace '\n' from 87 to 88",
|
||||||
"word 'const' from 88 to 93",
|
"keyword 'const' from 88 to 93",
|
||||||
"whitespace ' ' from 93 to 94",
|
"whitespace ' ' from 93 to 94",
|
||||||
"word 'prop3' from 94 to 99",
|
"word 'prop3' from 94 to 99",
|
||||||
"whitespace ' ' from 99 to 100",
|
"whitespace ' ' from 99 to 100",
|
||||||
@ -299,7 +303,7 @@ const prop3 = yo[key]`)
|
|||||||
const result = stringSummaryLexer(`const yo = 45 // this is a comment
|
const result = stringSummaryLexer(`const yo = 45 // this is a comment
|
||||||
const yo = 6`)
|
const yo = 6`)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
"word 'const' from 0 to 5",
|
"keyword 'const' from 0 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"word 'yo' from 6 to 8",
|
"word 'yo' from 6 to 8",
|
||||||
"whitespace ' ' from 8 to 9",
|
"whitespace ' ' from 8 to 9",
|
||||||
@ -307,9 +311,9 @@ const yo = 6`)
|
|||||||
"whitespace ' ' from 10 to 11",
|
"whitespace ' ' from 10 to 11",
|
||||||
"number '45' from 11 to 13",
|
"number '45' from 11 to 13",
|
||||||
"whitespace ' ' from 13 to 14",
|
"whitespace ' ' from 13 to 14",
|
||||||
"linecomment '// this is a comment' from 14 to 34",
|
"lineComment '// this is a comment' from 14 to 34",
|
||||||
"whitespace '\n' from 34 to 35",
|
"whitespace '\n' from 34 to 35",
|
||||||
"word 'const' from 35 to 40",
|
"keyword 'const' from 35 to 40",
|
||||||
"whitespace ' ' from 40 to 41",
|
"whitespace ' ' from 40 to 41",
|
||||||
"word 'yo' from 41 to 43",
|
"word 'yo' from 41 to 43",
|
||||||
"whitespace ' ' from 43 to 44",
|
"whitespace ' ' from 43 to 44",
|
||||||
@ -328,9 +332,9 @@ const yo=45`)
|
|||||||
"string ''hi'' from 4 to 8",
|
"string ''hi'' from 4 to 8",
|
||||||
"brace ')' from 8 to 9",
|
"brace ')' from 8 to 9",
|
||||||
"whitespace '\n' from 9 to 10",
|
"whitespace '\n' from 9 to 10",
|
||||||
"linecomment '// comment on a line by itself' from 10 to 40",
|
"lineComment '// comment on a line by itself' from 10 to 40",
|
||||||
"whitespace '\n' from 40 to 41",
|
"whitespace '\n' from 40 to 41",
|
||||||
"word 'const' from 41 to 46",
|
"keyword 'const' from 41 to 46",
|
||||||
"whitespace ' ' from 46 to 47",
|
"whitespace ' ' from 46 to 47",
|
||||||
"word 'yo' from 47 to 49",
|
"word 'yo' from 47 to 49",
|
||||||
"operator '=' from 49 to 50",
|
"operator '=' from 49 to 50",
|
||||||
@ -342,7 +346,7 @@ const yo=45`)
|
|||||||
const ya = 6 */
|
const ya = 6 */
|
||||||
const yi=45`)
|
const yi=45`)
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
"word 'const' from 0 to 5",
|
"keyword 'const' from 0 to 5",
|
||||||
"whitespace ' ' from 5 to 6",
|
"whitespace ' ' from 5 to 6",
|
||||||
"word 'yo' from 6 to 8",
|
"word 'yo' from 6 to 8",
|
||||||
"whitespace ' ' from 8 to 9",
|
"whitespace ' ' from 8 to 9",
|
||||||
@ -350,10 +354,10 @@ const yi=45`)
|
|||||||
"whitespace ' ' from 10 to 11",
|
"whitespace ' ' from 10 to 11",
|
||||||
"number '45' from 11 to 13",
|
"number '45' from 11 to 13",
|
||||||
"whitespace ' ' from 13 to 14",
|
"whitespace ' ' from 13 to 14",
|
||||||
`blockcomment '/* this is a comment
|
`blockComment '/* this is a comment
|
||||||
const ya = 6 */' from 14 to 50`,
|
const ya = 6 */' from 14 to 50`,
|
||||||
"whitespace '\n' from 50 to 51",
|
"whitespace '\n' from 50 to 51",
|
||||||
"word 'const' from 51 to 56",
|
"keyword 'const' from 51 to 56",
|
||||||
"whitespace ' ' from 56 to 57",
|
"whitespace ' ' from 56 to 57",
|
||||||
"word 'yi' from 57 to 59",
|
"word 'yi' from 57 to 59",
|
||||||
"operator '=' from 59 to 60",
|
"operator '=' from 59 to 60",
|
||||||
|
156
src/lib/cameraControls.ts
Normal file
156
src/lib/cameraControls.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||||
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
|
export type CameraSystem =
|
||||||
|
| 'KittyCAD'
|
||||||
|
| 'OnShape'
|
||||||
|
| 'Trackpad Friendly'
|
||||||
|
| 'Solidworks'
|
||||||
|
| 'NX'
|
||||||
|
| 'Creo'
|
||||||
|
| 'AutoCAD'
|
||||||
|
|
||||||
|
export const cameraSystems: CameraSystem[] = [
|
||||||
|
'KittyCAD',
|
||||||
|
'OnShape',
|
||||||
|
'Trackpad Friendly',
|
||||||
|
'Solidworks',
|
||||||
|
'NX',
|
||||||
|
'Creo',
|
||||||
|
'AutoCAD',
|
||||||
|
]
|
||||||
|
|
||||||
|
interface MouseGuardHandler {
|
||||||
|
description: string
|
||||||
|
callback: (e: React.MouseEvent) => boolean
|
||||||
|
lenientDragStartButton?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MouseGuardZoomHandler {
|
||||||
|
description: string
|
||||||
|
dragCallback: (e: React.MouseEvent) => boolean
|
||||||
|
scrollCallback: (e: React.MouseEvent) => boolean
|
||||||
|
lenientDragStartButton?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MouseGuard {
|
||||||
|
pan: MouseGuardHandler
|
||||||
|
zoom: MouseGuardZoomHandler
|
||||||
|
rotate: MouseGuardHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||||
|
KittyCAD: {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Shift + drag or middle click + drag',
|
||||||
|
callback: (e) =>
|
||||||
|
(e.button === 1 && noModifiersPressed(e)) ||
|
||||||
|
(e.button === 2 && e.shiftKey),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Right click + drag',
|
||||||
|
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OnShape: {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Ctrl + drag or middle click + drag',
|
||||||
|
callback: (e) =>
|
||||||
|
(e.button === 2 && e.ctrlKey) ||
|
||||||
|
(e.button === 1 && noModifiersPressed(e)),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel',
|
||||||
|
dragCallback: () => false,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Right click + drag',
|
||||||
|
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Trackpad Friendly': {
|
||||||
|
pan: {
|
||||||
|
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
||||||
|
callback: (e) =>
|
||||||
|
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||||
|
(e.button === 1 && noModifiersPressed(e)),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
||||||
|
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Left click + Alt + drag',
|
||||||
|
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
|
||||||
|
lenientDragStartButton: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Solidworks: {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Ctrl + drag',
|
||||||
|
callback: (e) => e.button === 2 && e.ctrlKey,
|
||||||
|
lenientDragStartButton: 2,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||||
|
dragCallback: (e) => e.button === 1 && e.shiftKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NX: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 1 && e.shiftKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Creo: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 1 && e.shiftKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AutoCAD: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel',
|
||||||
|
dragCallback: () => false,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 1 && e.shiftKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -5,7 +5,7 @@ import {
|
|||||||
readDir,
|
readDir,
|
||||||
writeTextFile,
|
writeTextFile,
|
||||||
} from '@tauri-apps/api/fs'
|
} from '@tauri-apps/api/fs'
|
||||||
import { documentDir } from '@tauri-apps/api/path'
|
import { documentDir, homeDir } from '@tauri-apps/api/path'
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
|
|||||||
return directory
|
return directory
|
||||||
}
|
}
|
||||||
|
|
||||||
const docDirectory = await documentDir()
|
let docDirectory: string
|
||||||
|
try {
|
||||||
|
docDirectory = await documentDir()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
docDirectory = await homeDir() // seems to work better on Linux
|
||||||
|
}
|
||||||
|
|
||||||
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ class MockEngineCommandManager {
|
|||||||
if (commandStr === undefined) {
|
if (commandStr === undefined) {
|
||||||
throw new Error('commandStr is undefined')
|
throw new Error('commandStr is undefined')
|
||||||
}
|
}
|
||||||
console.log('sendModelingCommandFromWasm', id, rangeStr, commandStr)
|
|
||||||
const command: EngineCommand = JSON.parse(commandStr)
|
const command: EngineCommand = JSON.parse(commandStr)
|
||||||
const range: SourceRange = JSON.parse(rangeStr)
|
const range: SourceRange = JSON.parse(rangeStr)
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ class MockEngineCommandManager {
|
|||||||
|
|
||||||
export async function enginelessExecutor(
|
export async function enginelessExecutor(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
pm: ProgramMemory = { root: {} }
|
pm: ProgramMemory = { root: {}, return: null }
|
||||||
): Promise<ProgramMemory> {
|
): Promise<ProgramMemory> {
|
||||||
const mockEngineCommandManager = new MockEngineCommandManager({
|
const mockEngineCommandManager = new MockEngineCommandManager({
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
@ -65,7 +64,7 @@ export async function enginelessExecutor(
|
|||||||
|
|
||||||
export async function executor(
|
export async function executor(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
pm: ProgramMemory = { root: {} }
|
pm: ProgramMemory = { root: {}, return: null }
|
||||||
): Promise<ProgramMemory> {
|
): Promise<ProgramMemory> {
|
||||||
const engineCommandManager = new EngineCommandManager({
|
const engineCommandManager = new EngineCommandManager({
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
@ -76,6 +75,6 @@ export async function executor(
|
|||||||
await engineCommandManager.waitForReady
|
await engineCommandManager.waitForReady
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
const programMemory = await _executor(ast, pm, engineCommandManager)
|
const programMemory = await _executor(ast, pm, engineCommandManager)
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands(ast, programMemory)
|
||||||
return programMemory
|
return programMemory
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,27 @@ export function throttle<T>(
|
|||||||
return throttled
|
return throttled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||||
|
export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null
|
||||||
|
let latestArgs: T
|
||||||
|
|
||||||
|
function later() {
|
||||||
|
timeout = null
|
||||||
|
func(latestArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred(args: T) {
|
||||||
|
latestArgs = args
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(later, wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred
|
||||||
|
}
|
||||||
|
|
||||||
export function getNormalisedCoordinates({
|
export function getNormalisedCoordinates({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
|
@ -2,6 +2,8 @@ import { createMachine, assign } from 'xstate'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { CommandBarMeta } from '../lib/commands'
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { invoke } from '@tauri-apps/api'
|
||||||
|
|
||||||
const SKIP_AUTH =
|
const SKIP_AUTH =
|
||||||
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
||||||
@ -115,19 +117,26 @@ async function getUser(context: UserContext) {
|
|||||||
const headers: { [key: string]: string } = {
|
const headers: { [key: string]: string } = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
if (!context.token && '__TAURI__' in window) throw 'not log in'
|
|
||||||
|
if (!context.token && isTauri()) throw new Error('No token found')
|
||||||
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
||||||
if (SKIP_AUTH) return LOCAL_USER
|
if (SKIP_AUTH) return LOCAL_USER
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
const userPromise = !isTauri()
|
||||||
|
? fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers,
|
headers,
|
||||||
})
|
})
|
||||||
const user = await response.json()
|
.then((res) => res.json())
|
||||||
|
.catch((err) => console.error('error from Browser getUser', err))
|
||||||
|
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
|
||||||
|
token: context.token,
|
||||||
|
}).catch((err) => console.error('error from Tauri getUser', err))
|
||||||
|
|
||||||
|
const user = await userPromise
|
||||||
|
|
||||||
if ('error_code' in user) throw new Error(user.message)
|
if ('error_code' in user) throw new Error(user.message)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,54 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { BaseUnit, baseUnitsUnion } from '../useStore'
|
|
||||||
import { CommandBarMeta } from '../lib/commands'
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||||
|
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
||||||
|
|
||||||
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
|
|
||||||
export enum UnitSystem {
|
export enum UnitSystem {
|
||||||
Imperial = 'imperial',
|
Imperial = 'imperial',
|
||||||
Metric = 'metric',
|
Metric = 'metric',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const baseUnits = {
|
||||||
|
imperial: ['in', 'ft'],
|
||||||
|
metric: ['mm', 'cm', 'm'],
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
|
||||||
|
|
||||||
|
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
|
||||||
|
|
||||||
|
export type Toggle = 'On' | 'Off'
|
||||||
|
|
||||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||||
|
|
||||||
export const settingsCommandBarMeta: CommandBarMeta = {
|
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||||
'Set Theme': {
|
'Set Base Unit': {
|
||||||
displayValue: (args: string[]) => 'Change the app theme',
|
displayValue: (args: string[]) => 'Set your default base unit',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'theme',
|
name: 'baseUnit',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
defaultValue: 'theme',
|
defaultValue: 'baseUnit',
|
||||||
options: Object.values(Themes).map((v) => ({ name: v })) as {
|
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||||
name: string
|
|
||||||
}[],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
'Set Camera Controls': {
|
||||||
|
displayValue: (args: string[]) => 'Set your camera controls',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'cameraControls',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'cameraControls',
|
||||||
|
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Default Directory': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
'Set Default Project Name': {
|
'Set Default Project Name': {
|
||||||
displayValue: (args: string[]) => 'Set a new default project name',
|
displayValue: (args: string[]) => 'Set a new default project name',
|
||||||
hide: 'web',
|
hide: 'web',
|
||||||
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Set Default Directory': {
|
'Set Onboarding Status': {
|
||||||
hide: 'both',
|
hide: 'both',
|
||||||
},
|
},
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'textWrapping',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'textWrapping',
|
||||||
|
options: [{ name: 'On' }, { name: 'Off' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Theme': {
|
||||||
|
displayValue: (args: string[]) => 'Change the app theme',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'theme',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'theme',
|
||||||
|
options: Object.values(Themes).map((v): { name: string } => ({
|
||||||
|
name: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
'Set Unit System': {
|
'Set Unit System': {
|
||||||
displayValue: (args: string[]) => 'Set your default unit system',
|
displayValue: (args: string[]) => 'Set your default unit system',
|
||||||
args: [
|
args: [
|
||||||
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Set Base Unit': {
|
|
||||||
displayValue: (args: string[]) => 'Set your default base unit',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'baseUnit',
|
|
||||||
type: 'select',
|
|
||||||
defaultValue: 'baseUnit',
|
|
||||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Onboarding Status': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsMachine = createMachine(
|
export const settingsMachine = createMachine(
|
||||||
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
|
|||||||
id: 'Settings',
|
id: 'Settings',
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
context: {
|
context: {
|
||||||
theme: Themes.System,
|
|
||||||
defaultProjectName: '',
|
|
||||||
unitSystem: UnitSystem.Imperial,
|
|
||||||
baseUnit: 'in' as BaseUnit,
|
baseUnit: 'in' as BaseUnit,
|
||||||
|
cameraControls: 'KittyCAD' as CameraSystem,
|
||||||
defaultDirectory: '',
|
defaultDirectory: '',
|
||||||
showDebugPanel: false,
|
defaultProjectName: DEFAULT_PROJECT_NAME,
|
||||||
onboardingStatus: '',
|
onboardingStatus: '',
|
||||||
|
showDebugPanel: false,
|
||||||
|
textWrapping: 'On' as Toggle,
|
||||||
|
theme: Themes.System,
|
||||||
|
unitSystem: UnitSystem.Imperial,
|
||||||
},
|
},
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
states: {
|
states: {
|
||||||
idle: {
|
idle: {
|
||||||
entry: ['setThemeClass'],
|
entry: ['setThemeClass'],
|
||||||
on: {
|
on: {
|
||||||
'Set Theme': {
|
'Set Base Unit': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||||
theme: (_, event) => event.data.theme,
|
|
||||||
}),
|
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
'setThemeClass',
|
|
||||||
],
|
],
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Default Project Name': {
|
'Set Camera Controls': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({
|
||||||
defaultProjectName: (_, event) => event.data.defaultProjectName,
|
cameraControls: (_, event) => event.data.cameraControls,
|
||||||
}),
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Unit System': {
|
'Set Default Project Name': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({
|
||||||
unitSystem: (_, event) => event.data.unitSystem,
|
defaultProjectName: (_, event) =>
|
||||||
baseUnit: (_, event) =>
|
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
|
||||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
|
||||||
}),
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Base Unit': {
|
'Set Onboarding Status': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
assign({
|
||||||
|
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
textWrapping: (_, event) => event.data.textWrapping,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Theme': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
theme: (_, event) => event.data.theme,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
'setThemeClass',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Unit System': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
unitSystem: (_, event) => event.data.unitSystem,
|
||||||
|
baseUnit: (_, event) =>
|
||||||
|
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||||
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
],
|
],
|
||||||
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Onboarding Status': {
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
|
||||||
}),
|
|
||||||
'persistSettings',
|
|
||||||
],
|
|
||||||
target: 'idle',
|
|
||||||
internal: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||||
schema: {
|
schema: {
|
||||||
events: {} as
|
events: {} as
|
||||||
| { type: 'Set Theme'; data: { theme: Themes } }
|
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||||
|
| {
|
||||||
|
type: 'Set Camera Controls'
|
||||||
|
data: { cameraControls: CameraSystem }
|
||||||
|
}
|
||||||
|
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||||
| {
|
| {
|
||||||
type: 'Set Default Project Name'
|
type: 'Set Default Project Name'
|
||||||
data: { defaultProjectName: string }
|
data: { defaultProjectName: string }
|
||||||
}
|
}
|
||||||
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||||
|
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
|
||||||
|
| { type: 'Set Theme'; data: { theme: Themes } }
|
||||||
| {
|
| {
|
||||||
type: 'Set Unit System'
|
type: 'Set Unit System'
|
||||||
data: { unitSystem: UnitSystem }
|
data: { unitSystem: UnitSystem }
|
||||||
}
|
}
|
||||||
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
|
||||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
|
||||||
| { type: 'Toggle Debug Panel' },
|
| { type: 'Toggle Debug Panel' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,25 +15,31 @@ export interface Typegen0 {
|
|||||||
eventsCausingActions: {
|
eventsCausingActions: {
|
||||||
persistSettings:
|
persistSettings:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
| 'Set Onboarding Status'
|
| 'Set Onboarding Status'
|
||||||
|
| 'Set Text Wrapping'
|
||||||
| 'Set Theme'
|
| 'Set Theme'
|
||||||
| 'Set Unit System'
|
| 'Set Unit System'
|
||||||
| 'Toggle Debug Panel'
|
| 'Toggle Debug Panel'
|
||||||
setThemeClass:
|
setThemeClass:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
| 'Set Onboarding Status'
|
| 'Set Onboarding Status'
|
||||||
|
| 'Set Text Wrapping'
|
||||||
| 'Set Theme'
|
| 'Set Theme'
|
||||||
| 'Set Unit System'
|
| 'Set Unit System'
|
||||||
| 'Toggle Debug Panel'
|
| 'Toggle Debug Panel'
|
||||||
| 'xstate.init'
|
| 'xstate.init'
|
||||||
toastSuccess:
|
toastSuccess:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
|
| 'Set Text Wrapping'
|
||||||
| 'Set Theme'
|
| 'Set Theme'
|
||||||
| 'Set Unit System'
|
| 'Set Unit System'
|
||||||
| 'Toggle Debug Panel'
|
| 'Toggle Debug Panel'
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
@ -38,6 +39,7 @@ const Home = () => {
|
|||||||
const {
|
const {
|
||||||
settings: {
|
settings: {
|
||||||
context: { defaultDirectory, defaultProjectName },
|
context: { defaultDirectory, defaultProjectName },
|
||||||
|
send: sendToSettings,
|
||||||
},
|
},
|
||||||
} = useGlobalStateContext()
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
@ -71,16 +73,33 @@ const Home = () => {
|
|||||||
context: ContextFrom<typeof homeMachine>,
|
context: ContextFrom<typeof homeMachine>,
|
||||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||||
) => {
|
) => {
|
||||||
let name =
|
let name = (
|
||||||
event.data && 'name' in event.data
|
event.data && 'name' in event.data
|
||||||
? event.data.name
|
? event.data.name
|
||||||
: defaultProjectName
|
: defaultProjectName
|
||||||
|
).trim()
|
||||||
|
let shouldUpdateDefaultProjectName = false
|
||||||
|
|
||||||
|
// If there is no default project name, flag it to be set to the default
|
||||||
|
if (!name) {
|
||||||
|
name = DEFAULT_PROJECT_NAME
|
||||||
|
shouldUpdateDefaultProjectName = true
|
||||||
|
}
|
||||||
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
const nextIndex = await getNextProjectIndex(name, projects)
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNewProject(context.defaultDirectory + '/' + name)
|
await createNewProject(context.defaultDirectory + '/' + name)
|
||||||
|
|
||||||
|
if (shouldUpdateDefaultProjectName) {
|
||||||
|
sendToSettings({
|
||||||
|
type: 'Set Default Project Name',
|
||||||
|
data: { defaultProjectName: DEFAULT_PROJECT_NAME },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
return `Successfully created "${name}"`
|
||||||
},
|
},
|
||||||
renameProject: async (
|
renameProject: async (
|
||||||
|
@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
|
|||||||
import { useStore } from '../../useStore'
|
import { useStore } from '../../useStore'
|
||||||
|
|
||||||
export default function Units() {
|
export default function Units() {
|
||||||
const { isMouseDownInStream } = useStore((s) => ({
|
const { buttonDownInStream } = useStore((s) => ({
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
}))
|
}))
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.SKETCHING)
|
const next = useNextClick(onboardingPaths.SKETCHING)
|
||||||
@ -15,7 +15,7 @@ export default function Units() {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
|
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
|
||||||
(isMouseDownInStream ? '' : ' pointer-events-auto')
|
(buttonDownInStream ? '' : ' pointer-events-auto')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<h1 className="text-2xl font-bold">Camera</h1>
|
<h1 className="text-2xl font-bold">Camera</h1>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { BaseUnit, baseUnits } from '../../useStore'
|
import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
|
||||||
import { ActionButton } from '../../components/ActionButton'
|
import { ActionButton } from '../../components/ActionButton'
|
||||||
import { SettingsSection } from '../Settings'
|
import { SettingsSection } from '../Settings'
|
||||||
import { Toggle } from '../../components/Toggle/Toggle'
|
import { Toggle } from '../../components/Toggle/Toggle'
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user