Compare commits
59 Commits
iterion/en
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
c03357d897 | |||
292f89859f | |||
a00800bddc | |||
78ceba6d20 | |||
6776a350af | |||
dd75f06f77 | |||
394872d84e | |||
f9eef6397f | |||
900bac999c | |||
5b2738f826 | |||
dab96577a7 | |||
25443eba31 | |||
0a72d7a39a | |||
5f8d4f8294 | |||
7c2cfba0ac | |||
5ee43bda22 | |||
a1b6bbac7e | |||
e61516f3c3 | |||
e2eeec37ad | |||
d7fcc128aa | |||
cf266b17c1 | |||
b3a1796da9 | |||
39b9a6b2c4 | |||
6ba4fa305c | |||
1d043899c8 | |||
cb8a087d89 | |||
f2eb7b57b8 | |||
eba653930f | |||
3deb5c689a | |||
11ebe11111 | |||
9538ffb8ec | |||
55d1da226f | |||
2bfde64bf1 | |||
7cb9a2efd9 | |||
57e85d7fd0 | |||
ca4a442cce | |||
46eef39d53 | |||
dbc5f7b11f | |||
6797331c9d | |||
cc80a2da3d | |||
54fb9c903a | |||
e63597458a | |||
e15c38fa23 | |||
906ca65611 | |||
805b9f48e5 | |||
a762d741a5 | |||
4b8ca7f61f | |||
31b0a8af12 | |||
74b4cb9e08 | |||
e7c6dd3698 | |||
aa9abbe83f | |||
b19f3bbdb0 | |||
892e856471 | |||
84fae12cdd | |||
3d67781039 | |||
114c3a2580 | |||
02b4aa0476 | |||
57f4e1b79c | |||
35f9b82a65 |
@ -2,7 +2,9 @@ NODE_ENV=development
|
|||||||
DEV=true
|
DEV=true
|
||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||||
|
BASE_URL=https://api.dev.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
|
||||||
|
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"plugin:css-modules/recommended"
|
"plugin:css-modules/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"never"
|
"never"
|
||||||
@ -24,7 +26,6 @@
|
|||||||
{
|
{
|
||||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
|
||||||
"suggest-no-throw/suggest-no-throw": "off",
|
"suggest-no-throw/suggest-no-throw": "off",
|
||||||
"testing-library/prefer-screen-queries": "off",
|
"testing-library/prefer-screen-queries": "off",
|
||||||
"jest/valid-expect": "off"
|
"jest/valid-expect": "off"
|
||||||
|
121
.github/workflows/build-test-publish-apps.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
|
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
|
||||||
- name: Run build:wasm
|
- name: Run build:wasm
|
||||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
run: "yarn build:wasm"
|
||||||
|
|
||||||
- name: Set nightly version
|
- name: Set nightly version
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
@ -81,8 +81,6 @@ jobs:
|
|||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
CSC_FOR_PULL_REQUEST: true
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
|
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
|
||||||
@ -142,37 +140,12 @@ jobs:
|
|||||||
- name: List artifacts in out/
|
- name: List artifacts in out/
|
||||||
run: ls -R out
|
run: ls -R out
|
||||||
|
|
||||||
- name: Prepare the tauri update bundles (macOS)
|
|
||||||
if: ${{ env.BUILD_RELEASE && matrix.os == 'macos-14' }}
|
|
||||||
run: |
|
|
||||||
for ARCH in arm64 x64; do
|
|
||||||
TAURI_DIR=out/tauri/$VERSION/macos
|
|
||||||
TEMP_DIR=temp/$ARCH
|
|
||||||
mkdir -p $TAURI_DIR
|
|
||||||
mkdir -p $TEMP_DIR
|
|
||||||
unzip out/*-$ARCH-mac.zip -d $TEMP_DIR
|
|
||||||
tar -czvf "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" -C $TEMP_DIR "Zoo Modeling App.app"
|
|
||||||
yarn tauri signer sign "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz"
|
|
||||||
done
|
|
||||||
ls -R out
|
|
||||||
|
|
||||||
- name: Prepare the tauri update bundles (Windows)
|
|
||||||
if: ${{ env.BUILD_RELEASE && matrix.os == 'windows-2022' }}
|
|
||||||
run: |
|
|
||||||
$env:TAURI_DIR="out/tauri/${env:VERSION}/nsis"
|
|
||||||
mkdir -p ${env:TAURI_DIR}
|
|
||||||
$env:OUT_FILE="${env:TAURI_DIR}/Zoo Modeling App_${env:VERSION_NO_V}_x64-setup.nsis.zip"
|
|
||||||
7z a -mm=Copy "${env:OUT_FILE}" ./out/*-x64-win.exe
|
|
||||||
yarn tauri signer sign "${env:OUT_FILE}"
|
|
||||||
ls -R out
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: out-${{ matrix.os }}
|
name: out-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
out/Zoo*.*
|
out/Zoo*.*
|
||||||
out/latest*.yml
|
out/latest*.yml
|
||||||
out/tauri
|
|
||||||
|
|
||||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||||
|
|
||||||
@ -192,8 +165,6 @@ jobs:
|
|||||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
||||||
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
|
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
|
||||||
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||||
BUCKET_DIR_TAURI: 'dl.kittycad.io/releases/modeling-app/tauri-compat'
|
|
||||||
WEBSITE_DIR_TAURI: 'dl.zoo.dev/releases/modeling-app/tauri-compat'
|
|
||||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -212,7 +183,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: out-ubuntu-22.04
|
name: out-ubuntu-22.04
|
||||||
path: out
|
path: out
|
||||||
|
|
||||||
- name: Generate the download static endpoint
|
- name: Generate the download static endpoint
|
||||||
run: |
|
run: |
|
||||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||||
@ -222,8 +193,10 @@ jobs:
|
|||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
|
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
|
||||||
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
|
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
|
||||||
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.msi" \
|
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \
|
||||||
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.msi" \
|
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \
|
||||||
|
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \
|
||||||
|
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \
|
||||||
'{
|
'{
|
||||||
"version": $version,
|
"version": $version,
|
||||||
"pub_date": $pub_date,
|
"pub_date": $pub_date,
|
||||||
@ -235,54 +208,22 @@ jobs:
|
|||||||
"dmg-x64": {
|
"dmg-x64": {
|
||||||
"url": $mac_x64_url
|
"url": $mac_x64_url
|
||||||
},
|
},
|
||||||
"msi-arm64": {
|
"exe-arm64": {
|
||||||
"url": $windows_arm64_url
|
"url": $windows_arm64_url
|
||||||
},
|
},
|
||||||
"msi-x64": {
|
"exe-x64": {
|
||||||
"url": $windows_x64_url
|
"url": $windows_x64_url
|
||||||
|
},
|
||||||
|
"appimage-arm64": {
|
||||||
|
"url": $linux_arm64_url
|
||||||
|
},
|
||||||
|
"appimage-x64": {
|
||||||
|
"url": $linux_x64_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' > last_download.json
|
}' > last_download.json
|
||||||
cat last_download.json
|
cat last_download.json
|
||||||
|
|
||||||
- name: Generate the update static endpoint for tauri
|
|
||||||
run: |
|
|
||||||
TAURI_DIR=out/tauri/$VERSION
|
|
||||||
MAC_ARM64_SIG=`cat $TAURI_DIR/macos/*-arm64.app.tar.gz.sig`
|
|
||||||
MAC_X64_SIG=`cat $TAURI_DIR/macos/*-x64.app.tar.gz.sig`
|
|
||||||
WINDOWS_SIG=`cat $TAURI_DIR/nsis/*.nsis.zip.sig`
|
|
||||||
RELEASE_DIR=https://${WEBSITE_DIR_TAURI}/${VERSION}
|
|
||||||
jq --null-input \
|
|
||||||
--arg version "${VERSION}" \
|
|
||||||
--arg pub_date "${PUB_DATE}" \
|
|
||||||
--arg notes "${NOTES}" \
|
|
||||||
--arg mac_arm64_sig "$MAC_ARM64_SIG" \
|
|
||||||
--arg mac_arm64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-arm64.app.tar.gz" \
|
|
||||||
--arg mac_x64_sig "$MAC_X64_SIG" \
|
|
||||||
--arg mac_x64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-x64.app.tar.gz" \
|
|
||||||
--arg windows_sig "$WINDOWS_SIG" \
|
|
||||||
--arg windows_url "$RELEASE_DIR/nsis/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
|
||||||
'{
|
|
||||||
"version": $version,
|
|
||||||
"pub_date": $pub_date,
|
|
||||||
"notes": $notes,
|
|
||||||
"platforms": {
|
|
||||||
"darwin-x86_64": {
|
|
||||||
"signature": $mac_x64_sig,
|
|
||||||
"url": $mac_x64_url
|
|
||||||
},
|
|
||||||
"darwin-aarch64": {
|
|
||||||
"signature": $mac_arm64_sig,
|
|
||||||
"url": $mac_arm64_url
|
|
||||||
},
|
|
||||||
"windows-x86_64": {
|
|
||||||
"signature": $windows_sig,
|
|
||||||
"url": $windows_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}' > last_update.json
|
|
||||||
cat last_update.json
|
|
||||||
|
|
||||||
- name: List artifacts
|
- name: List artifacts
|
||||||
run: "ls -R out"
|
run: "ls -R out"
|
||||||
|
|
||||||
@ -304,13 +245,31 @@ jobs:
|
|||||||
parent: false
|
parent: false
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
|
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
|
||||||
|
- name: Upload release files to public bucket (test/electron-builder workaround)
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
|
with:
|
||||||
|
path: out
|
||||||
|
glob: 'Zoo*'
|
||||||
|
parent: false
|
||||||
|
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
with:
|
with:
|
||||||
path: out
|
path: out
|
||||||
glob: 'latest*'
|
glob: 'latest*'
|
||||||
parent: false
|
parent: false
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
|
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
|
||||||
|
- name: Upload update endpoint to public bucket (test/electron-builder workaround)
|
||||||
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
|
with:
|
||||||
|
path: out
|
||||||
|
glob: 'latest*'
|
||||||
|
parent: false
|
||||||
|
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||||
@ -318,20 +277,6 @@ jobs:
|
|||||||
path: last_download.json
|
path: last_download.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload release files to public bucket for tauri
|
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
|
||||||
with:
|
|
||||||
path: "out/tauri/${{ env.VERSION }}"
|
|
||||||
glob: '*/Zoo*'
|
|
||||||
parent: false
|
|
||||||
destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }}
|
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket for tauri
|
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
|
||||||
with:
|
|
||||||
path: last_update.json
|
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
|
||||||
|
|
||||||
- name: Upload release files to Github
|
- name: Upload release files to Github
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
2
.github/workflows/build-test-web.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
- run: yarn xstate:typegen
|
- run: yarn xstate:typegen
|
||||||
- run: yarn tsc
|
- run: yarn tsc
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn eslint --max-warnings 0 src e2e
|
run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client
|
||||||
|
|
||||||
|
|
||||||
check-typos:
|
check-typos:
|
||||||
|
3
.github/workflows/cargo-clippy.yml
vendored
@ -28,6 +28,7 @@ jobs:
|
|||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: taiki-e/install-action@just
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@ -41,7 +42,7 @@ jobs:
|
|||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
cargo clippy --all --tests --benches -- -D warnings
|
just lint
|
||||||
# If this fails, run "cargo check" to update Cargo.lock,
|
# If this fails, run "cargo check" to update Cargo.lock,
|
||||||
# then add Cargo.lock to the PR.
|
# then add Cargo.lock to the PR.
|
||||||
- name: Check Cargo.lock doesn't need updating
|
- name: Check Cargo.lock doesn't need updating
|
||||||
|
19
README.md
@ -351,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Tauri e2e tests
|
|
||||||
|
|
||||||
#### Windows (local only until the CI edge version mismatch is fixed)
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn install
|
|
||||||
yarn build:wasm-dev
|
|
||||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
|
||||||
yarn vite build --mode development
|
|
||||||
yarn tauri build --debug -b
|
|
||||||
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
|
|
||||||
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
|
|
||||||
$env:E2E_TAURI_ENABLED="true"
|
|
||||||
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
|
|
||||||
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
|
|
||||||
Stop-Process -Name msedgedriver
|
|
||||||
yarn wdio run wdio.conf.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## KCL
|
## KCL
|
||||||
|
|
||||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||||
|
@ -24,6 +24,3 @@ once fixed in engine will just start working here with no language changes.
|
|||||||
chamfer cases work currently.
|
chamfer cases work currently.
|
||||||
|
|
||||||
Sketching on the chamfered face does not currently work.
|
Sketching on the chamfered face does not currently work.
|
||||||
|
|
||||||
- **Shell**: Shell sometimes does not work when arcs or fillets are involved.
|
|
||||||
We are tracking the engine side bug on this.
|
|
||||||
|
@ -56,6 +56,7 @@ layout: manual
|
|||||||
* [`line`](kcl/line)
|
* [`line`](kcl/line)
|
||||||
* [`lineTo`](kcl/lineTo)
|
* [`lineTo`](kcl/lineTo)
|
||||||
* [`ln`](kcl/ln)
|
* [`ln`](kcl/ln)
|
||||||
|
* [`loft`](kcl/loft)
|
||||||
* [`log`](kcl/log)
|
* [`log`](kcl/log)
|
||||||
* [`log10`](kcl/log10)
|
* [`log10`](kcl/log10)
|
||||||
* [`log2`](kcl/log2)
|
* [`log2`](kcl/log2)
|
||||||
@ -63,6 +64,7 @@ layout: manual
|
|||||||
* [`max`](kcl/max)
|
* [`max`](kcl/max)
|
||||||
* [`min`](kcl/min)
|
* [`min`](kcl/min)
|
||||||
* [`mm`](kcl/mm)
|
* [`mm`](kcl/mm)
|
||||||
|
* [`offsetPlane`](kcl/offsetPlane)
|
||||||
* [`patternCircular2d`](kcl/patternCircular2d)
|
* [`patternCircular2d`](kcl/patternCircular2d)
|
||||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||||
|
516
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
4948
docs/kcl/std.json
@ -271,10 +271,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('bracket').click()
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// If they're open by default, we're not actually testing anything.
|
// If they're open by default, we're not actually testing anything.
|
||||||
@ -302,16 +299,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('router-template-slate').click()
|
await page.getByText('router-template-slate').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
|
||||||
).toBeEnabled({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('All panes opened before should be visible', async () => {
|
await test.step('All panes opened before should be visible', async () => {
|
||||||
|
@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
panesOpen,
|
openKclCodePanel,
|
||||||
|
openFilePanel,
|
||||||
createAndSelectProject,
|
createAndSelectProject,
|
||||||
pasteCodeInEditor,
|
pasteCodeInEditor,
|
||||||
createNewFileAndSelect,
|
createNewFileAndSelect,
|
||||||
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
|
|||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen(['files', 'code'])
|
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
await createAndSelectProject('project-000')
|
||||||
|
await openKclCodePanel()
|
||||||
|
await openFilePanel()
|
||||||
// File the main.kcl with contents
|
// File the main.kcl with contents
|
||||||
const kclCube = await fsp.readFile(
|
const kclCube = await fsp.readFile(
|
||||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||||
@ -201,4 +202,78 @@ test.describe('when using the file tree to', () => {
|
|||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'loading small file, then large, then back to small',
|
||||||
|
{
|
||||||
|
tag: '@electron',
|
||||||
|
},
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { page } = await setupElectron({
|
||||||
|
testInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
panesOpen,
|
||||||
|
createAndSelectProject,
|
||||||
|
pasteCodeInEditor,
|
||||||
|
createNewFile,
|
||||||
|
openDebugPanel,
|
||||||
|
closeDebugPanel,
|
||||||
|
expectCmdLog,
|
||||||
|
} = await getUtils(page, test)
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
page.on('console', console.log)
|
||||||
|
|
||||||
|
await panesOpen(['files', 'code'])
|
||||||
|
await createAndSelectProject('project-000')
|
||||||
|
|
||||||
|
// Create a small file
|
||||||
|
const kclCube = await fsp.readFile(
|
||||||
|
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
// pasted into main.kcl
|
||||||
|
await pasteCodeInEditor(kclCube)
|
||||||
|
|
||||||
|
// Create a large lego file
|
||||||
|
await createNewFile('lego')
|
||||||
|
const legoFile = page.getByRole('listitem').filter({
|
||||||
|
has: page.getByRole('button', { name: 'lego.kcl' }),
|
||||||
|
})
|
||||||
|
await expect(legoFile).toBeVisible({ timeout: 60_000 })
|
||||||
|
await legoFile.click()
|
||||||
|
const kclLego = await fsp.readFile(
|
||||||
|
'src/wasm-lib/tests/executor/inputs/lego.kcl',
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
await pasteCodeInEditor(kclLego)
|
||||||
|
const mainFile = page.getByRole('listitem').filter({
|
||||||
|
has: page.getByRole('button', { name: 'main.kcl' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Open settings and enable the debug panel
|
||||||
|
await page
|
||||||
|
.getByRole('link', {
|
||||||
|
name: 'settings Settings',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.locator('#showDebugPanel').getByText('OffOn').click()
|
||||||
|
await page.getByTestId('settings-close-button').click()
|
||||||
|
|
||||||
|
await test.step('swap between small and large files', async () => {
|
||||||
|
await openDebugPanel()
|
||||||
|
// Previously created a file so we need to start back at main.kcl
|
||||||
|
await mainFile.click()
|
||||||
|
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
|
||||||
|
// Click the large file
|
||||||
|
await legoFile.click()
|
||||||
|
// Once it is building, click back to the smaller file
|
||||||
|
await mainFile.click()
|
||||||
|
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
|
||||||
|
await closeDebugPanel()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
@ -147,9 +147,6 @@ test.describe('Can export from electron app', () => {
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
await electronApp.context().addInitScript(async () => {
|
|
||||||
;(window as any).playwrightSkipFilePicker = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const pointOnModel = { x: 630, y: 280 }
|
const pointOnModel = { x: 630, y: 280 }
|
||||||
|
|
||||||
@ -938,16 +935,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('bracket').click()
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
|
||||||
).toBeEnabled({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// gray at this pixel means the stream has loaded in the most
|
// gray at this pixel means the stream has loaded in the most
|
||||||
// user way we can verify it (pixel color)
|
// user way we can verify it (pixel color)
|
||||||
@ -972,16 +960,7 @@ test(
|
|||||||
|
|
||||||
await page.getByText('router-template-slate').click()
|
await page.getByText('router-template-slate').click()
|
||||||
|
|
||||||
await expect(page.getByTestId('loading')).toBeAttached()
|
await u.waitForPageLoad()
|
||||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
|
||||||
).toBeEnabled({
|
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// gray at this pixel means the stream has loaded in the most
|
// gray at this pixel means the stream has loaded in the most
|
||||||
// user way we can verify it (pixel color)
|
// user way we can verify it (pixel color)
|
||||||
@ -1740,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Rename the folder', async () => {
|
await test.step('Rename the folder', async () => {
|
||||||
await page.waitForTimeout(60000)
|
await page.waitForTimeout(2000)
|
||||||
await folderToRename.click({ button: 'right' })
|
await folderToRename.click({ button: 'right' })
|
||||||
await expect(renameMenuItem).toBeVisible()
|
await expect(renameMenuItem).toBeVisible()
|
||||||
await renameMenuItem.click()
|
await renameMenuItem.click()
|
||||||
|
@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
|
|||||||
const crypticErrorText = `ApiError`
|
const crypticErrorText = `ApiError`
|
||||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||||
})
|
})
|
||||||
|
test('user should not have to press down twice in cmdbar', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// because the model has `line([0,0]..` it is valid code, but the model is invalid
|
||||||
|
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
|
||||||
|
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const sketch2 = startSketchOn("XY")
|
||||||
|
const sketch001 = startSketchAt([-0, -0])
|
||||||
|
|> line([0, 0], %)
|
||||||
|
|> line([-4.84, -5.29], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForPageLoad()
|
||||||
|
|
||||||
|
await test.step('Check arrow down works', async () => {
|
||||||
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('option', { name: 'floppy disk arrow Export' })
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// press arrow down key twice
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
|
||||||
|
// STL is the third option, which makes sense for two arrow downs
|
||||||
|
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||||
|
'STL'
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Check arrow up works', async () => {
|
||||||
|
// theme in test is dark, which is the second option, which means we can test arrow up
|
||||||
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
|
|
||||||
|
await page.getByText('The overall appearance of the').click()
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||||
|
'light'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
test('executes on load', async ({ page }) => {
|
test('executes on load', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
|
|||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||||
import { isArray } from 'lib/utils'
|
import { isArray } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
type TestColor = [number, number, number]
|
type TestColor = [number, number, number]
|
||||||
export const TEST_COLORS = {
|
export const TEST_COLORS = {
|
||||||
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
}
|
}
|
||||||
return maxDiff
|
return maxDiff
|
||||||
},
|
},
|
||||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||||
new Promise(async (resolve) => {
|
new Promise<boolean>((resolve) => {
|
||||||
await page.screenshot({
|
;(async () => {
|
||||||
path: './e2e/playwright/temp1.png',
|
|
||||||
fullPage: true,
|
|
||||||
})
|
|
||||||
await fn()
|
|
||||||
const isImageDiff = async () => {
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: './e2e/playwright/temp2.png',
|
path: './e2e/playwright/temp1.png',
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
})
|
})
|
||||||
const screenshot1 = PNG.sync.read(
|
await fn()
|
||||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
const isImageDiff = async () => {
|
||||||
)
|
await page.screenshot({
|
||||||
const screenshot2 = PNG.sync.read(
|
path: './e2e/playwright/temp2.png',
|
||||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
fullPage: true,
|
||||||
)
|
})
|
||||||
const actualDiffCount = pixelMatch(
|
const screenshot1 = PNG.sync.read(
|
||||||
screenshot1.data,
|
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||||
screenshot2.data,
|
)
|
||||||
null,
|
const screenshot2 = PNG.sync.read(
|
||||||
screenshot1.width,
|
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||||
screenshot2.height
|
)
|
||||||
)
|
const actualDiffCount = pixelMatch(
|
||||||
return actualDiffCount > diffCount
|
screenshot1.data,
|
||||||
}
|
screenshot2.data,
|
||||||
|
null,
|
||||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
screenshot1.width,
|
||||||
let count = 0
|
screenshot2.height
|
||||||
const interval = setInterval(async () => {
|
)
|
||||||
count++
|
return actualDiffCount > diffCount
|
||||||
if (await isImageDiff()) {
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve(true)
|
|
||||||
} else if (count > 100) {
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve(false)
|
|
||||||
}
|
}
|
||||||
}, 50)
|
|
||||||
|
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||||
|
let count = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
;(async () => {
|
||||||
|
count++
|
||||||
|
if (await isImageDiff()) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(true)
|
||||||
|
} else if (count > 100) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})().catch(reportRejection)
|
||||||
|
}, 50)
|
||||||
|
})().catch(reportRejection)
|
||||||
}),
|
}),
|
||||||
emulateNetworkConditions: async (
|
emulateNetworkConditions: async (
|
||||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||||
@ -548,13 +553,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
|
|
||||||
createNewFileAndSelect: async (name: string) => {
|
createNewFileAndSelect: async (name: string) => {
|
||||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||||
|
await openFilePanel(page)
|
||||||
await page.getByTestId('create-file-button').click()
|
await page.getByTestId('create-file-button').click()
|
||||||
await page.getByTestId('file-rename-field').fill(name)
|
await page.getByTestId('file-rename-field').fill(name)
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await page
|
const newFile = page
|
||||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||||
.filter({ hasText: name })
|
.filter({ hasText: name })
|
||||||
.click()
|
|
||||||
|
await expect(newFile).toBeVisible()
|
||||||
|
await newFile.click()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -585,6 +593,15 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Sorry I don't have time to fix this right now, but runs like
|
||||||
|
* the one linked below show me that setting the open panes in this manner is not reliable.
|
||||||
|
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
|
||||||
|
* or you can imperatively open the panes with functions like {openKclCodePanel}
|
||||||
|
* (or we can make a general openPane function that takes a paneId).,
|
||||||
|
* but having a separate initScript does not seem to work reliably.
|
||||||
|
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
|
||||||
|
*/
|
||||||
panesOpen: async (paneIds: PaneId[]) => {
|
panesOpen: async (paneIds: PaneId[]) => {
|
||||||
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
@ -852,10 +869,12 @@ export async function setupElectron({
|
|||||||
testInfo,
|
testInfo,
|
||||||
folderSetupFn,
|
folderSetupFn,
|
||||||
cleanProjectDir = true,
|
cleanProjectDir = true,
|
||||||
|
appSettings,
|
||||||
}: {
|
}: {
|
||||||
testInfo: TestInfo
|
testInfo: TestInfo
|
||||||
folderSetupFn?: (projectDirName: string) => Promise<void>
|
folderSetupFn?: (projectDirName: string) => Promise<void>
|
||||||
cleanProjectDir?: boolean
|
cleanProjectDir?: boolean
|
||||||
|
appSettings?: Partial<SaveSettingsPayload>
|
||||||
}) {
|
}) {
|
||||||
// create or otherwise clear the folder
|
// create or otherwise clear the folder
|
||||||
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
||||||
@ -889,15 +908,19 @@ export async function setupElectron({
|
|||||||
|
|
||||||
if (cleanProjectDir) {
|
if (cleanProjectDir) {
|
||||||
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
||||||
const settingsOverrides = TOML.stringify({
|
const settingsOverrides = TOML.stringify(
|
||||||
...TEST_SETTINGS,
|
appSettings
|
||||||
settings: {
|
? { settings: appSettings }
|
||||||
app: {
|
: {
|
||||||
...TEST_SETTINGS.app,
|
...TEST_SETTINGS,
|
||||||
projectDirectory: projectDirName,
|
settings: {
|
||||||
},
|
app: {
|
||||||
},
|
...TEST_SETTINGS.app,
|
||||||
})
|
projectDirectory: projectDirName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
tearDown,
|
tearDown,
|
||||||
executorInputPath,
|
executorInputPath,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
|
|
||||||
@ -154,29 +154,33 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
test('Project and user settings can be reset', async ({ page }) => {
|
test('Project and user settings can be reset', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await test.step(`Setup`, async () => {
|
||||||
await u.waitForAuthSkipAppStart()
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page
|
await u.waitForAuthSkipAppStart()
|
||||||
.getByRole('button', { name: 'Start Sketch' })
|
})
|
||||||
.waitFor({ state: 'visible' })
|
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||||
const resetButton = page.getByRole('button', {
|
const resetButton = (level: SettingsLevel) =>
|
||||||
name: 'Restore default settings',
|
page.getByRole('button', {
|
||||||
})
|
name: `Reset ${level}-level settings`,
|
||||||
|
})
|
||||||
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
||||||
const settingValues = {
|
const settingValues = {
|
||||||
default: '259',
|
default: '259',
|
||||||
user: '120',
|
user: '120',
|
||||||
project: '50',
|
project: '50',
|
||||||
}
|
}
|
||||||
|
const resetToast = (level: SettingsLevel) =>
|
||||||
|
page.getByText(`${level}-level settings were reset`)
|
||||||
|
|
||||||
// Open the settings modal with lower-right button
|
await test.step(`Open the settings modal`, async () => {
|
||||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
await test.step('Set up theme color', async () => {
|
await test.step('Set up theme color', async () => {
|
||||||
// Verify we're looking at the project-level settings,
|
// Verify we're looking at the project-level settings,
|
||||||
@ -195,37 +199,40 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
await test.step('Reset project settings', async () => {
|
await test.step('Reset project settings', async () => {
|
||||||
// Click the reset settings button.
|
// Click the reset settings button.
|
||||||
await resetButton.click()
|
await resetButton('project').click()
|
||||||
|
|
||||||
await expect(page.getByText('Settings restored to default')).toBeVisible()
|
await expect(resetToast('project')).toBeVisible()
|
||||||
await expect(
|
await expect(resetToast('project')).not.toBeVisible()
|
||||||
page.getByText('Settings restored to default')
|
|
||||||
).not.toBeVisible()
|
|
||||||
|
|
||||||
// Verify it is now set to the inherited user value
|
// Verify it is now set to the inherited user value
|
||||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||||
|
|
||||||
// Check that the user setting also rolled back
|
await test.step(`Check that the user settings did not change`, async () => {
|
||||||
await userSettingsTab.click()
|
await userSettingsTab.click()
|
||||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||||
await projectSettingsTab.click()
|
})
|
||||||
|
|
||||||
// Set project-level value to 50 again to test the user-level reset
|
await test.step(`Set project-level again to test the user-level reset`, async () => {
|
||||||
await themeColorSetting.fill(settingValues.project)
|
await projectSettingsTab.click()
|
||||||
await userSettingsTab.click()
|
await themeColorSetting.fill(settingValues.project)
|
||||||
|
await userSettingsTab.click()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Reset user settings', async () => {
|
await test.step('Reset user settings', async () => {
|
||||||
// Change the setting and click the reset settings button.
|
// Click the reset settings button.
|
||||||
await themeColorSetting.fill(settingValues.user)
|
await resetButton('user').click()
|
||||||
await resetButton.click()
|
|
||||||
|
await expect(resetToast('user')).toBeVisible()
|
||||||
|
await expect(resetToast('user')).not.toBeVisible()
|
||||||
|
|
||||||
// Verify it is now set to the default value
|
// Verify it is now set to the default value
|
||||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||||
|
|
||||||
// Check that the project setting also changed
|
await test.step(`Check that the project settings did not change`, async () => {
|
||||||
await projectSettingsTab.click()
|
await projectSettingsTab.click()
|
||||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
await expect(themeColorSetting).toHaveValue(settingValues.project)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -288,7 +295,7 @@ test.describe('Testing settings', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Refresh the application and see project setting applied', async () => {
|
await test.step('Refresh the application and see project setting applied', async () => {
|
||||||
await page.reload()
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||||
|
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||||
await settingsCloseButton.click()
|
await settingsCloseButton.click()
|
||||||
@ -303,53 +310,109 @@ test.describe('Testing settings', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Load desktop app with no settings file`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
// This is what makes no settings file get created
|
||||||
|
cleanProjectDir: false,
|
||||||
|
testInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const errorHeading = page.getByRole('heading', {
|
||||||
|
name: 'An unextected error occurred',
|
||||||
|
})
|
||||||
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
|
// If the app loads without exploding we're in the clear
|
||||||
|
await expect(errorHeading).not.toBeVisible()
|
||||||
|
await expect(projectDirLink).toBeVisible()
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Load desktop app with a settings file, but no project directory setting`,
|
||||||
|
{ tag: '@electron' },
|
||||||
|
async ({ browser: _ }, testInfo) => {
|
||||||
|
const { electronApp, page } = await setupElectron({
|
||||||
|
testInfo,
|
||||||
|
appSettings: {
|
||||||
|
app: {
|
||||||
|
themeColor: '259',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const errorHeading = page.getByRole('heading', {
|
||||||
|
name: 'An unextected error occurred',
|
||||||
|
})
|
||||||
|
const projectDirLink = page.getByText('Loaded from')
|
||||||
|
|
||||||
|
// If the app loads without exploding we're in the clear
|
||||||
|
await expect(errorHeading).not.toBeVisible()
|
||||||
|
await expect(projectDirLink).toBeVisible()
|
||||||
|
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
`Closing settings modal should go back to the original file being viewed`,
|
`Closing settings modal should go back to the original file being viewed`,
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browser: _ }, testInfo) => {
|
async ({ browser: _ }, testInfo) => {
|
||||||
const { electronApp, page } = await setupElectron({
|
const { electronApp, page } = await setupElectron({
|
||||||
testInfo,
|
testInfo,
|
||||||
folderSetupFn: async () => {},
|
folderSetupFn: async (dir) => {
|
||||||
|
const bracketDir = join(dir, 'project-000')
|
||||||
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
|
await fsp.copyFile(
|
||||||
|
executorInputPath('cube.kcl'),
|
||||||
|
join(bracketDir, 'main.kcl')
|
||||||
|
)
|
||||||
|
await fsp.copyFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
join(bracketDir, '2.kcl')
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
|
||||||
|
const kclCylinder = await fsp.readFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
panesOpen,
|
openKclCodePanel,
|
||||||
createAndSelectProject,
|
openFilePanel,
|
||||||
pasteCodeInEditor,
|
waitForPageLoad,
|
||||||
clickPane,
|
selectFile,
|
||||||
createNewFileAndSelect,
|
|
||||||
editorTextMatches,
|
editorTextMatches,
|
||||||
} = await getUtils(page, test)
|
} = await getUtils(page, test)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen([])
|
await test.step('Precondition: Open to second project file', async () => {
|
||||||
|
|
||||||
await test.step('Precondition: No projects exist', async () => {
|
|
||||||
await expect(page.getByTestId('home-section')).toBeVisible()
|
await expect(page.getByTestId('home-section')).toBeVisible()
|
||||||
const projectLinksPre = page.getByTestId('project-link')
|
await page.getByText('project-000').click()
|
||||||
await expect(projectLinksPre).toHaveCount(0)
|
await waitForPageLoad()
|
||||||
|
await openKclCodePanel()
|
||||||
|
await openFilePanel()
|
||||||
|
await editorTextMatches(kclCube)
|
||||||
|
|
||||||
|
await selectFile('2.kcl')
|
||||||
|
await editorTextMatches(kclCylinder)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
|
||||||
|
|
||||||
await clickPane('code')
|
|
||||||
const kclCube = await fsp.readFile(
|
|
||||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
await pasteCodeInEditor(kclCube)
|
|
||||||
|
|
||||||
await clickPane('files')
|
|
||||||
await createNewFileAndSelect('2.kcl')
|
|
||||||
|
|
||||||
const kclCylinder = await fsp.readFile(
|
|
||||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
await pasteCodeInEditor(kclCylinder)
|
|
||||||
|
|
||||||
const settingsOpenButton = page.getByRole('link', {
|
const settingsOpenButton = page.getByRole('link', {
|
||||||
name: 'settings Settings',
|
name: 'settings Settings',
|
||||||
})
|
})
|
||||||
@ -357,6 +420,9 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
await test.step('Open and close settings', async () => {
|
await test.step('Open and close settings', async () => {
|
||||||
await settingsOpenButton.click()
|
await settingsOpenButton.click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||||
|
).toBeVisible()
|
||||||
await settingsCloseButton.click()
|
await settingsCloseButton.click()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -370,25 +436,37 @@ test.describe('Testing settings', () => {
|
|||||||
|
|
||||||
test('Changing modeling default unit', async ({ page }) => {
|
test('Changing modeling default unit', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await test.step(`Test setup`, async () => {
|
||||||
await u.waitForAuthSkipAppStart()
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page
|
await u.waitForAuthSkipAppStart()
|
||||||
.getByRole('button', { name: 'Start Sketch' })
|
await page
|
||||||
.waitFor({ state: 'visible' })
|
.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
.waitFor({ state: 'visible' })
|
||||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
})
|
||||||
|
|
||||||
// Open the settings modal with lower-right button
|
// Selectors and constants
|
||||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||||
await expect(
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
const defaultUnitSection = page.getByText(
|
||||||
).toBeVisible()
|
'default unitRoll back default unitRoll back to match'
|
||||||
|
)
|
||||||
const resetButton = page.getByRole('button', {
|
const defaultUnitRollbackButton = page.getByRole('button', {
|
||||||
name: 'Restore default settings',
|
name: 'Roll back default unit',
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Open the settings modal`, async () => {
|
||||||
|
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Reset unit setting`, async () => {
|
||||||
|
await userSettingsTab.click()
|
||||||
|
await defaultUnitSection.hover()
|
||||||
|
await defaultUnitRollbackButton.click()
|
||||||
|
await projectSettingsTab.click()
|
||||||
})
|
})
|
||||||
// Default unit should be mm
|
|
||||||
await resetButton.click()
|
|
||||||
|
|
||||||
await test.step('Change modeling default unit within project tab', async () => {
|
await test.step('Change modeling default unit within project tab', async () => {
|
||||||
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
|
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
|
||||||
@ -493,4 +571,70 @@ test.describe('Testing settings', () => {
|
|||||||
await changeUnitOfMeasureInGizmo('m', 'Meters')
|
await changeUnitOfMeasureInGizmo('m', 'Meters')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Changing theme in sketch mode', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 0], %)
|
||||||
|
|> line([0, 5], %)
|
||||||
|
|> line([-5, 0], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
const extrude001 = extrude(5, sketch001)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
// Selectors and constants
|
||||||
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
|
const lineToolButton = page.getByTestId('line')
|
||||||
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||||
|
const sketchOriginLocation = { x: 600, y: 250 }
|
||||||
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||||
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||||
|
|
||||||
|
await test.step(`Get into sketch mode`, async () => {
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
await expect(editSketchButton).toBeVisible()
|
||||||
|
await editSketchButton.click()
|
||||||
|
|
||||||
|
// We use the line tool as a proxy for sketch mode
|
||||||
|
await expect(lineToolButton).toBeVisible()
|
||||||
|
await expect(segmentOverlays).toHaveCount(4)
|
||||||
|
// but we allow more time to pass for animating to the sketch
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Check the sketch line color before`, async () => {
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
|
await page.keyboard.press('ControlOrMeta+K')
|
||||||
|
await page.getByRole('option', { name: 'theme' }).click()
|
||||||
|
await page.getByRole('option', { name: 'light' }).click()
|
||||||
|
await expect(page.getByText('theme to "light"')).toBeVisible()
|
||||||
|
|
||||||
|
// Make sure we haven't left sketch mode
|
||||||
|
await expect(lineToolButton).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Check the sketch line color after`, async () => {
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
|
|
||||||
// Ensure the final toast remains.
|
// Ensure the final toast remains.
|
||||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
||||||
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
|
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
|
||||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||||
|
|
||||||
// Ensure you can copy the code for the final model.
|
// Ensure you can copy the code for the final model.
|
||||||
@ -690,40 +690,53 @@ test(
|
|||||||
'Text-to-CAD functionality',
|
'Text-to-CAD functionality',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
|
const projectName = 'project-000'
|
||||||
|
const prompt = 'lego 2x4'
|
||||||
|
const textToCadFileName = 'lego-2x4.kcl'
|
||||||
|
|
||||||
const { electronApp, page, dir } = await setupElectron({ testInfo })
|
const { electronApp, page, dir } = await setupElectron({ testInfo })
|
||||||
const fileExists = () =>
|
const fileExists = () =>
|
||||||
fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl'))
|
fs.existsSync(join(dir, projectName, textToCadFileName))
|
||||||
|
|
||||||
const { createAndSelectProject, panesOpen } = await getUtils(page, test)
|
const {
|
||||||
|
createAndSelectProject,
|
||||||
|
openFilePanel,
|
||||||
|
openKclCodePanel,
|
||||||
|
waitForPageLoad,
|
||||||
|
} = await getUtils(page, test)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await panesOpen(['code', 'files'])
|
// Locators
|
||||||
|
const projectMenuButton = page.getByRole('button', { name: projectName })
|
||||||
|
const textToCadFileButton = page.getByRole('listitem').filter({
|
||||||
|
has: page.getByRole('button', { name: textToCadFileName }),
|
||||||
|
})
|
||||||
|
const textToCadComment = page.getByText(
|
||||||
|
`// Generated by Text-to-CAD: ${prompt}`
|
||||||
|
)
|
||||||
|
|
||||||
// Create and navigate to the project
|
// Create and navigate to the project
|
||||||
await createAndSelectProject('project-000')
|
await createAndSelectProject('project-000')
|
||||||
|
|
||||||
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
||||||
await expect(
|
await waitForPageLoad()
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
await openFilePanel()
|
||||||
).toBeEnabled({
|
await openKclCodePanel()
|
||||||
timeout: 20_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step(`Test file creation`, async () => {
|
await test.step(`Test file creation`, async () => {
|
||||||
await sendPromptFromCommandBar(page, 'lego 2x4')
|
await sendPromptFromCommandBar(page, prompt)
|
||||||
// File is considered created if it shows up in the Project Files pane
|
// File is considered created if it shows up in the Project Files pane
|
||||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||||
await expect(file).toBeVisible({ timeout: 20_000 })
|
|
||||||
expect(fileExists()).toBeTruthy()
|
expect(fileExists()).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Test file navigation`, async () => {
|
await test.step(`Test file navigation`, async () => {
|
||||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
await file.click()
|
await textToCadFileButton.click()
|
||||||
const kclComment = page.getByText('Lego 2x4 Brick')
|
|
||||||
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
|
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
|
||||||
await expect(kclComment).toBeVisible({ timeout: 20_000 })
|
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
|
||||||
|
await expect(projectMenuButton).toContainText(textToCadFileName)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Test file deletion on rejection`, async () => {
|
await test.step(`Test file deletion on rejection`, async () => {
|
||||||
@ -737,6 +750,8 @@ test(
|
|||||||
)
|
)
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
await expect(submittingToastMessage).toBeVisible()
|
||||||
expect(fileExists()).toBeFalsy()
|
expect(fileExists()).toBeFalsy()
|
||||||
|
// Confirm we've navigated back to the main.kcl file after deletion
|
||||||
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
})
|
})
|
||||||
|
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
|
@ -11,16 +11,23 @@ mac:
|
|||||||
category: public.app-category.developer-tools
|
category: public.app-category.developer-tools
|
||||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: dmg
|
||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
- target: zip
|
- target: zip
|
||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
notarize:
|
notarize:
|
||||||
teamId: 92H8YB3B95
|
teamId: 92H8YB3B95
|
||||||
|
fileAssociations:
|
||||||
|
- ext: kcl
|
||||||
|
name: kcl
|
||||||
|
mimeType: text/vnd.zoo.kcl
|
||||||
|
description: Zoo KCL File
|
||||||
|
role: Editor
|
||||||
|
rank: Owner
|
||||||
|
|
||||||
win:
|
win:
|
||||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||||
@ -38,6 +45,12 @@ win:
|
|||||||
sign: "./sign-win.js"
|
sign: "./sign-win.js"
|
||||||
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
|
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
|
||||||
icon: "assets/icon.ico"
|
icon: "assets/icon.ico"
|
||||||
|
fileAssociations:
|
||||||
|
- ext: kcl
|
||||||
|
name: kcl
|
||||||
|
mimeType: text/vnd.zoo.kcl
|
||||||
|
description: Zoo KCL File
|
||||||
|
role: Editor
|
||||||
|
|
||||||
msi:
|
msi:
|
||||||
oneClick: false
|
oneClick: false
|
||||||
@ -57,8 +70,14 @@ linux:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
|
fileAssociations:
|
||||||
|
- ext: kcl
|
||||||
|
name: kcl
|
||||||
|
mimeType: text/vnd.zoo.kcl
|
||||||
|
description: Zoo KCL File
|
||||||
|
role: Editor
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
- provider: generic
|
- provider: generic
|
||||||
url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder
|
url: https://dl.zoo.dev/releases/modeling-app
|
||||||
channel: latest
|
channel: latest
|
||||||
|
2
interface.d.ts
vendored
@ -30,8 +30,6 @@ export interface IElectronAPI {
|
|||||||
join: typeof path.join
|
join: typeof path.join
|
||||||
sep: typeof path.sep
|
sep: typeof path.sep
|
||||||
rename: (prev: string, next: string) => typeof fs.rename
|
rename: (prev: string, next: string) => typeof fs.rename
|
||||||
setBaseUrl: (value: string) => void
|
|
||||||
loadProjectAtStartup: () => Promise<ProjectState | null>
|
|
||||||
packageJson: {
|
packageJson: {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
13
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.24.12",
|
"version": "0.25.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@tweenjs/tween.js": "^23.1.1",
|
"@tweenjs/tween.js": "^23.1.1",
|
||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^4.1.1",
|
||||||
"bonjour-service": "^1.2.1",
|
"bonjour-service": "^1.2.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.1",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-modal": "^3.16.1",
|
"react-modal": "^3.16.1",
|
||||||
"react-modal-promise": "^1.0.2",
|
"react-modal-promise": "^1.0.2",
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-uri": "^3.0.8",
|
"vscode-uri": "^3.0.8",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"xstate": "^4.38.2"
|
"xstate": "^5.17.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||||
"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\"",
|
"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": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src e2e",
|
"lint": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||||
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||||
@ -137,7 +137,6 @@
|
|||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@playwright/test": "^1.46.1",
|
"@playwright/test": "^1.46.1",
|
||||||
"@tauri-apps/cli": "^2.0.0-rc.9",
|
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^15.0.2",
|
"@testing-library/react": "^15.0.2",
|
||||||
"@types/d3-force": "^3.0.10",
|
"@types/d3-force": "^3.0.10",
|
||||||
@ -169,7 +168,7 @@
|
|||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"eslint-plugin-import": "^2.25.0",
|
"eslint-plugin-import": "^2.30.0",
|
||||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||||
"happy-dom": "^14.3.10",
|
"happy-dom": "^14.3.10",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
|
@ -72,6 +72,7 @@ export class LanguageServerClient {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
// Start the client in the background.
|
// Start the client in the background.
|
||||||
this.client.setNotifyFn(this.processNotifications.bind(this))
|
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.client.start()
|
this.client.start()
|
||||||
|
|
||||||
this.ready = true
|
this.ready = true
|
||||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private processNotifications(notification: LSP.NotificationMessage) {
|
private processNotifications(notification: LSP.NotificationMessage) {
|
||||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
for (const plugin of this.plugins) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
plugin.processNotification(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ export default function lspFormatExt(
|
|||||||
run: (view: EditorView) => {
|
run: (view: EditorView) => {
|
||||||
let value = view.plugin(plugin)
|
let value = view.plugin(plugin)
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
value.requestFormatting()
|
value.requestFormatting()
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
|
|
||||||
this.processLspNotification = options.processLspNotification
|
this.processLspNotification = options.processLspNotification
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.initialize({
|
this.initialize({
|
||||||
documentText: this.getDocText(),
|
documentText: this.getDocText(),
|
||||||
})
|
})
|
||||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize({ documentText }: { documentText: string }) {
|
async initialize({ documentText }: { documentText: string }) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
if (this.client.initializePromise) {
|
if (this.client.initializePromise) {
|
||||||
await this.client.initializePromise
|
await this.client.initializePromise
|
||||||
}
|
}
|
||||||
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.requestSemanticTokens()
|
this.requestSemanticTokens()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.updateFoldingRanges()
|
this.updateFoldingRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.requestSemanticTokens()
|
this.requestSemanticTokens()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.updateFoldingRanges()
|
this.updateFoldingRanges()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@ -526,7 +532,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||||
if (params.uri !== this.getDocUri()) return
|
if (params.uri !== this.getDocUri()) return
|
||||||
|
|
||||||
const diagnostics = params.diagnostics
|
// Commented to avoid the lint. See TODO below.
|
||||||
|
// const diagnostics =
|
||||||
|
params.diagnostics
|
||||||
.map(({ range, message, severity }) => ({
|
.map(({ range, message, severity }) => ({
|
||||||
from: posToOffset(this.view.state.doc, range.start)!,
|
from: posToOffset(this.view.state.doc, range.start)!,
|
||||||
to: posToOffset(this.view.state.doc, range.end)!,
|
to: posToOffset(this.view.state.doc, range.end)!,
|
||||||
|
15
src/App.tsx
@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
|||||||
import Gizmo from 'components/Gizmo'
|
import Gizmo from 'components/Gizmo'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
@ -80,7 +81,7 @@ export function App() {
|
|||||||
useEngineConnectionSubscriptions()
|
useEngineConnectionSubscriptions()
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager.sendSceneCommand(message)
|
engineCommandManager.sendSceneCommand(message).catch(reportRejection)
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
if (state.matches('Sketch')) {
|
if (state.matches('Sketch')) {
|
||||||
@ -95,7 +96,7 @@ export function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (state.matches('idle.showPlanes')) return
|
if (state.matches({ idle: 'showPlanes' })) return
|
||||||
if (context.store?.buttonDownInStream !== undefined) return
|
if (context.store?.buttonDownInStream !== undefined) return
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -122,11 +123,11 @@ export function App() {
|
|||||||
// Override the electron window draggable region behavior as well
|
// Override the electron window draggable region behavior as well
|
||||||
// when the button is down in the stream
|
// when the button is down in the stream
|
||||||
style={
|
style={
|
||||||
{
|
isDesktop() && context.store?.buttonDownInStream
|
||||||
'-webkit-app-region': context.store?.buttonDownInStream
|
? ({
|
||||||
? 'no-drag'
|
'-webkit-app-region': 'no-drag',
|
||||||
: '',
|
} as React.CSSProperties)
|
||||||
} as React.CSSProperties
|
: {}
|
||||||
}
|
}
|
||||||
project={{ project, file }}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
|
@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { coreDump } from 'lang/wasm'
|
import { coreDump } from 'lang/wasm'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -69,19 +70,6 @@ const router = createRouter([
|
|||||||
path: PATHS.INDEX,
|
path: PATHS.INDEX,
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
const onDesktop = isDesktop()
|
const onDesktop = isDesktop()
|
||||||
if (onDesktop) {
|
|
||||||
const projectStartupFile =
|
|
||||||
await window.electron.loadProjectAtStartup()
|
|
||||||
if (projectStartupFile !== null) {
|
|
||||||
// Redirect to the file if we have a file path.
|
|
||||||
if (projectStartupFile.length > 0) {
|
|
||||||
return redirect(
|
|
||||||
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return onDesktop
|
return onDesktop
|
||||||
? redirect(PATHS.HOME)
|
? redirect(PATHS.HOME)
|
||||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||||
@ -186,21 +174,23 @@ function CoreDump() {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
useHotkeyWrapper(['mod + shift + .'], () => {
|
useHotkeyWrapper(['mod + shift + .'], () => {
|
||||||
toast.promise(
|
toast
|
||||||
coreDump(coreDumpManager, true),
|
.promise(
|
||||||
{
|
coreDump(coreDumpManager, true),
|
||||||
loading: 'Starting core dump...',
|
{
|
||||||
success: 'Core dump completed successfully',
|
loading: 'Starting core dump...',
|
||||||
error: 'Error while exporting core dump',
|
success: 'Core dump completed successfully',
|
||||||
},
|
error: 'Error while exporting core dump',
|
||||||
{
|
|
||||||
success: {
|
|
||||||
// Note: this extended duration is especially important for Playwright e2e testing
|
|
||||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
|
||||||
duration: 6000,
|
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
)
|
success: {
|
||||||
|
// Note: this extended duration is especially important for Playwright e2e testing
|
||||||
|
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||||
|
duration: 6000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(reportRejection)
|
||||||
})
|
})
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import {
|
|||||||
ToolbarItemResolved,
|
ToolbarItemResolved,
|
||||||
ToolbarModeName,
|
ToolbarModeName,
|
||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -68,12 +70,12 @@ export function Toolbar({
|
|||||||
*/
|
*/
|
||||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
modelingStateMatches: state.matches,
|
modelingState: state,
|
||||||
modelingSend: send,
|
modelingSend: send,
|
||||||
commandBarSend,
|
commandBarSend,
|
||||||
sketchPathId,
|
sketchPathId,
|
||||||
}),
|
}),
|
||||||
[state.matches, send, commandBarSend, sketchPathId]
|
[state, send, commandBarSend, sketchPathId]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
inert={false}
|
inert={false}
|
||||||
|
wrapperStyle={
|
||||||
|
isDesktop()
|
||||||
|
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||||
|
: {}
|
||||||
|
}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
wrapperClassName="!p-4 !pointer-events-auto"
|
wrapperClassName="!p-4 !pointer-events-auto"
|
||||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||||
@ -337,6 +344,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
|||||||
<li key={link.label} className="contents">
|
<li key={link.label} className="contents">
|
||||||
<a
|
<a
|
||||||
href={link.url}
|
href={link.url}
|
||||||
|
onClick={openExternalBrowserIfDesktop(link.url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
||||||
|
@ -22,18 +22,19 @@ import {
|
|||||||
UnreliableSubscription,
|
UnreliableSubscription,
|
||||||
} from 'lang/std/engineConnection'
|
} from 'lang/std/engineConnection'
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { toSync, uuidv4 } from 'lib/utils'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { isQuaternionVertical } from './helpers'
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||||
const FRAMES_TO_ANIMATE_IN = 30
|
const FRAMES_TO_ANIMATE_IN = 30
|
||||||
|
|
||||||
const tempQuaternion = new Quaternion() // just used for maths
|
const tempQuaternion = new Quaternion() // just used for maths
|
||||||
|
|
||||||
type interactionType = 'pan' | 'rotate' | 'zoom'
|
export type CameraInteractionType = 'pan' | 'rotate' | 'zoom'
|
||||||
|
|
||||||
interface ThreeCamValues {
|
interface ThreeCamValues {
|
||||||
position: Vector3
|
position: Vector3
|
||||||
@ -100,6 +101,7 @@ export class CameraControls {
|
|||||||
camProps.type === 'perspective' &&
|
camProps.type === 'perspective' &&
|
||||||
this.camera instanceof OrthographicCamera
|
this.camera instanceof OrthographicCamera
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
} else if (
|
} else if (
|
||||||
camProps.type === 'orthographic' &&
|
camProps.type === 'orthographic' &&
|
||||||
@ -127,6 +129,7 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(cmd)
|
this.engineCommandManager.sendSceneCommand(cmd)
|
||||||
}, 1000 / 30)
|
}, 1000 / 30)
|
||||||
|
|
||||||
@ -139,6 +142,7 @@ export class CameraControls {
|
|||||||
...convertThreeCamValuesToEngineCam(threeValues),
|
...convertThreeCamValuesToEngineCam(threeValues),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(cmd)
|
this.engineCommandManager.sendSceneCommand(cmd)
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
@ -151,6 +155,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveCmd &&
|
this.lastPerspectiveCmd &&
|
||||||
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
||||||
this.lastPerspectiveCmdTime = Date.now()
|
this.lastPerspectiveCmdTime = Date.now()
|
||||||
}
|
}
|
||||||
@ -218,6 +223,7 @@ export class CameraControls {
|
|||||||
this.useOrthographicCamera()
|
this.useOrthographicCamera()
|
||||||
}
|
}
|
||||||
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
}
|
}
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||||
@ -249,6 +255,7 @@ export class CameraControls {
|
|||||||
const doZoom = () => {
|
const doZoom = () => {
|
||||||
if (this.zoomDataFromLastFrame !== undefined) {
|
if (this.zoomDataFromLastFrame !== undefined) {
|
||||||
this.handleStart()
|
this.handleStart()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -266,6 +273,7 @@ export class CameraControls {
|
|||||||
|
|
||||||
const doMove = () => {
|
const doMove = () => {
|
||||||
if (this.moveDataFromLastFrame !== undefined) {
|
if (this.moveDataFromLastFrame !== undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -459,6 +467,7 @@ export class CameraControls {
|
|||||||
|
|
||||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
@ -541,7 +550,7 @@ export class CameraControls {
|
|||||||
const oldFov = this.camera.fov
|
const oldFov = this.camera.fov
|
||||||
|
|
||||||
const viewHeightFactor = (fov: number) => {
|
const viewHeightFactor = (fov: number) => {
|
||||||
/* *
|
/* *
|
||||||
/|
|
/|
|
||||||
/ |
|
/ |
|
||||||
/ |
|
/ |
|
||||||
@ -929,6 +938,7 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isReducedMotion()) {
|
if (isReducedMotion()) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
onComplete()
|
onComplete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -937,7 +947,7 @@ export class CameraControls {
|
|||||||
.to({ t: tweenEnd }, duration)
|
.to({ t: tweenEnd }, duration)
|
||||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||||
.onUpdate(({ t }) => cameraAtTime(t))
|
.onUpdate(({ t }) => cameraAtTime(t))
|
||||||
.onComplete(onComplete)
|
.onComplete(toSync(onComplete, reportRejection))
|
||||||
.start()
|
.start()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -962,6 +972,7 @@ export class CameraControls {
|
|||||||
// Decrease the FOV
|
// Decrease the FOV
|
||||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.dollyZoom(currentFov)
|
this.dollyZoom(currentFov)
|
||||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
} else if (frameWaitOnFinish > 0) {
|
} else if (frameWaitOnFinish > 0) {
|
||||||
@ -991,6 +1002,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov = 4
|
this.lastPerspectiveFov = 4
|
||||||
let currentFov = 4
|
let currentFov = 4
|
||||||
const initialCameraUp = this.camera.up.clone()
|
const initialCameraUp = this.camera.up.clone()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
const tempVec = new Vector3()
|
const tempVec = new Vector3()
|
||||||
|
|
||||||
@ -999,6 +1011,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
||||||
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
||||||
this.camera.up.copy(currentUp)
|
this.camera.up.copy(currentUp)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.dollyZoom(currentFov)
|
this.dollyZoom(currentFov)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,6 +1040,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov = 4
|
this.lastPerspectiveFov = 4
|
||||||
let currentFov = 4
|
let currentFov = 4
|
||||||
const initialCameraUp = this.camera.up.clone()
|
const initialCameraUp = this.camera.up.clone()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
const tempVec = new Vector3()
|
const tempVec = new Vector3()
|
||||||
|
|
||||||
@ -1175,7 +1189,7 @@ function convertThreeCamValuesToEngineCam({
|
|||||||
const lookAt = buildLookAt(64 / zoom, target, position)
|
const lookAt = buildLookAt(64 / zoom, target, position)
|
||||||
return {
|
return {
|
||||||
center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z),
|
center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z),
|
||||||
up: new Vector3(0, 0, 1),
|
up: new Vector3(upVector.x, upVector.y, upVector.z),
|
||||||
vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z),
|
vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1205,8 +1219,8 @@ function _getInteractionType(
|
|||||||
enablePan: boolean,
|
enablePan: boolean,
|
||||||
enableRotate: boolean,
|
enableRotate: boolean,
|
||||||
enableZoom: boolean
|
enableZoom: boolean
|
||||||
): interactionType | 'none' {
|
): CameraInteractionType | 'none' {
|
||||||
let state: interactionType | 'none' = 'none'
|
let state: CameraInteractionType | 'none' = 'none'
|
||||||
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
|
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
|
||||||
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
|
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
|
||||||
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||||
|
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||||
import { ReactCameraProperties } from './CameraControls'
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
import { throttle } from 'lib/utils'
|
import { throttle, toSync } from 'lib/utils'
|
||||||
import {
|
import {
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
kclManager,
|
kclManager,
|
||||||
@ -44,7 +44,7 @@ import {
|
|||||||
removeSingleConstraintInfo,
|
removeSingleConstraintInfo,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -124,9 +124,9 @@ export const ClientSideScene = ({
|
|||||||
} else if (context.mouseState.type === 'isDragging') {
|
} else if (context.mouseState.type === 'isDragging') {
|
||||||
cursor = 'grabbing'
|
cursor = 'grabbing'
|
||||||
} else if (
|
} else if (
|
||||||
state.matches('Sketch.Line tool') ||
|
state.matches({ Sketch: 'Line tool' }) ||
|
||||||
state.matches('Sketch.Tangential arc to') ||
|
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||||
state.matches('Sketch.Rectangle tool')
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
) {
|
) {
|
||||||
cursor = 'crosshair'
|
cursor = 'crosshair'
|
||||||
} else {
|
} else {
|
||||||
@ -214,9 +214,9 @@ const Overlay = ({
|
|||||||
overlay.visible &&
|
overlay.visible &&
|
||||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.Line tool') ||
|
state.matches({ Sketch: 'Line tool' }) ||
|
||||||
state.matches('Sketch.Tangential arc to') ||
|
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||||
state.matches('Sketch.Rectangle tool')
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -582,7 +582,7 @@ const ConstraintSymbol = ({
|
|||||||
}}
|
}}
|
||||||
// disabled={isConstrained || !convertToVarEnabled}
|
// disabled={isConstrained || !convertToVarEnabled}
|
||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
send({
|
send({
|
||||||
type: 'Convert to variable',
|
type: 'Convert to variable',
|
||||||
@ -616,13 +616,14 @@ const ConstraintSymbol = ({
|
|||||||
)
|
)
|
||||||
if (!transform) return
|
if (!transform) return
|
||||||
const { modifiedAst } = transform
|
const { modifiedAst } = transform
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.updateAst(modifiedAst, true)
|
kclManager.updateAst(modifiedAst, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
}
|
}
|
||||||
toast.success('Constraint removed')
|
toast.success('Constraint removed')
|
||||||
}
|
}
|
||||||
}}
|
}, reportRejection)}
|
||||||
>
|
>
|
||||||
<CustomIcon name={name} />
|
<CustomIcon name={name} />
|
||||||
</button>
|
</button>
|
||||||
@ -688,7 +689,7 @@ const ConstraintSymbol = ({
|
|||||||
|
|
||||||
const throttled = throttle((a: ReactCameraProperties) => {
|
const throttled = throttle((a: ReactCameraProperties) => {
|
||||||
if (a.type === 'perspective' && a.fov) {
|
if (a.type === 'perspective' && a.fov) {
|
||||||
sceneInfra.camControls.dollyZoom(a.fov)
|
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
|
||||||
}
|
}
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
|
|||||||
if (camSettings.type === 'perspective') {
|
if (camSettings.type === 'perspective') {
|
||||||
sceneInfra.camControls.useOrthographicCamera()
|
sceneInfra.camControls.useOrthographicCamera()
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneInfra.camControls.usePerspectiveCamera(true)
|
sceneInfra.camControls.usePerspectiveCamera(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -725,7 +727,7 @@ export const CamDebugSettings = () => {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition()
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Camera Position
|
Reset Camera Position
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
OnMouseEnterLeaveArgs,
|
OnMouseEnterLeaveArgs,
|
||||||
RAYCASTABLE_PLANE,
|
RAYCASTABLE_PLANE,
|
||||||
SEGMENT_LENGTH_LABEL,
|
SEGMENT_LENGTH_LABEL,
|
||||||
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
|
||||||
SEGMENT_LENGTH_LABEL_TEXT,
|
SEGMENT_LENGTH_LABEL_TEXT,
|
||||||
SKETCH_GROUP_SEGMENTS,
|
SKETCH_GROUP_SEGMENTS,
|
||||||
SKETCH_LAYER,
|
SKETCH_LAYER,
|
||||||
@ -102,8 +101,8 @@ import {
|
|||||||
getRectangleCallExpressions,
|
getRectangleCallExpressions,
|
||||||
updateRectangleSketch,
|
updateRectangleSketch,
|
||||||
} from 'lib/rectangleTool'
|
} from 'lib/rectangleTool'
|
||||||
import { getThemeColorForThreeJs } from 'lib/theme'
|
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
|
|
||||||
@ -324,6 +323,7 @@ export class SceneEntities {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
@ -634,6 +634,7 @@ export class SceneEntities {
|
|||||||
draftExpressionsIndices,
|
draftExpressionsIndices,
|
||||||
})
|
})
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
@ -701,7 +702,7 @@ export class SceneEntities {
|
|||||||
if (profileStart) {
|
if (profileStart) {
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||||
} else {
|
} else {
|
||||||
this.setUpDraftSegment(
|
await this.setUpDraftSegment(
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
@ -771,6 +772,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onMove: async (args) => {
|
onMove: async (args) => {
|
||||||
// Update the width and height of the draft rectangle
|
// Update the width and height of the draft rectangle
|
||||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||||
@ -818,6 +820,7 @@ export class SceneEntities {
|
|||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||||
const cornerPoint = args.intersectionPoint?.twoD
|
const cornerPoint = args.intersectionPoint?.twoD
|
||||||
@ -892,9 +895,11 @@ export class SceneEntities {
|
|||||||
}) => {
|
}) => {
|
||||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onDragEnd: async () => {
|
onDragEnd: async () => {
|
||||||
if (addingNewSegmentStatus !== 'nothing') {
|
if (addingNewSegmentStatus !== 'nothing') {
|
||||||
await this.tearDownSketch({ removeAxis: false })
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.setupSketch({
|
this.setupSketch({
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
maybeModdedAst: kclManager.ast,
|
maybeModdedAst: kclManager.ast,
|
||||||
@ -911,6 +916,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onDrag: async ({
|
onDrag: async ({
|
||||||
selected,
|
selected,
|
||||||
intersectionPoint,
|
intersectionPoint,
|
||||||
@ -958,6 +964,7 @@ export class SceneEntities {
|
|||||||
|
|
||||||
await kclManager.executeAstMock(mod.modifiedAst)
|
await kclManager.executeAstMock(mod.modifiedAst)
|
||||||
await this.tearDownSketch({ removeAxis: false })
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.setupSketch({
|
this.setupSketch({
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
maybeModdedAst: kclManager.ast,
|
maybeModdedAst: kclManager.ast,
|
||||||
@ -1161,7 +1168,7 @@ export class SceneEntities {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
sceneInfra.overlayCallbacks(callBacks)
|
sceneInfra.overlayCallbacks(callBacks)
|
||||||
})()
|
})().catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1414,20 +1421,14 @@ export class SceneEntities {
|
|||||||
) as CSS2DObject
|
) as CSS2DObject
|
||||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||||
label.innerText = `${roundOff(length)}${sceneInfra._baseUnit}`
|
label.innerText = `${roundOff(length)}`
|
||||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||||
const offsetFromMidpoint = new Vector2(to[0] - from[0], to[1] - from[1])
|
const slope = (to[1] - from[1]) / (to[0] - from[0])
|
||||||
.normalize()
|
let slopeAngle = ((Math.atan(slope) * 180) / Math.PI) * -1
|
||||||
.rotateAround(new Vector2(0, 0), Math.PI / 2)
|
label.style.setProperty('--degree', `${slopeAngle}deg`)
|
||||||
.multiplyScalar(SEGMENT_LENGTH_LABEL_OFFSET_PX * scale)
|
label.style.setProperty('--x', `0px`)
|
||||||
label.style.setProperty('--x', `${offsetFromMidpoint.x}px`)
|
label.style.setProperty('--y', `0px`)
|
||||||
label.style.setProperty('--y', `${offsetFromMidpoint.y}px`)
|
labelWrapper.position.set((from[0] + to[0]) / 2, (from[1] + to[1]) / 2, 0)
|
||||||
labelWrapper.position.set(
|
|
||||||
(from[0] + to[0]) / 2 + offsetFromMidpoint.x,
|
|
||||||
(from[1] + to[1]) / 2 + offsetFromMidpoint.y,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
labelGroup.visible = isHandlesVisible
|
labelGroup.visible = isHandlesVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1465,6 +1466,25 @@ export class SceneEntities {
|
|||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Update the base color of each of the THREEjs meshes
|
||||||
|
* that represent each of the sketch segments, to get the
|
||||||
|
* latest value from `sceneInfra._theme`
|
||||||
|
*/
|
||||||
|
updateSegmentBaseColor(newColor: Themes.Light | Themes.Dark) {
|
||||||
|
const newColorThreeJs = getThemeColorForThreeJs(newColor)
|
||||||
|
Object.values(this.activeSegments).forEach((group) => {
|
||||||
|
group.userData.baseColor = newColorThreeJs
|
||||||
|
group.traverse((child) => {
|
||||||
|
if (
|
||||||
|
child instanceof Mesh &&
|
||||||
|
child.material instanceof MeshBasicMaterial
|
||||||
|
) {
|
||||||
|
child.material.color.set(newColorThreeJs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
removeSketchGrid() {
|
removeSketchGrid() {
|
||||||
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.desktopApp {
|
||||||
/* Make the header act as a handle to drag the electron app window,
|
/* Make the header act as a handle to drag the electron app window,
|
||||||
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
|
* per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region
|
||||||
* all interactive elements opt-out of this behavior by default in src/index.css
|
* all interactive elements opt-out of this behavior by default in src/index.css
|
||||||
|
@ -6,6 +6,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
import { RefreshButton } from 'components/RefreshButton'
|
import { RefreshButton } from 'components/RefreshButton'
|
||||||
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -32,7 +33,9 @@ export const AppHeader = ({
|
|||||||
className={
|
className={
|
||||||
'w-full grid ' +
|
'w-full grid ' +
|
||||||
styles.header +
|
styles.header +
|
||||||
' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
|
` ${
|
||||||
|
isDesktop() ? styles.desktopApp + ' ' : ''
|
||||||
|
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
|
||||||
className
|
className
|
||||||
}
|
}
|
||||||
style={style}
|
style={style}
|
||||||
|
@ -151,6 +151,7 @@ export function useCalc({
|
|||||||
})
|
})
|
||||||
if (trap(error)) return
|
if (trap(error)) return
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
executeAst({
|
executeAst({
|
||||||
ast,
|
ast,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
import { throttle, isReducedMotion } from 'lib/utils'
|
import { throttle, isReducedMotion } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const updateDollyZoom = throttle(
|
const updateDollyZoom = throttle(
|
||||||
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||||
@ -16,8 +17,8 @@ export const CamToggle = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.addEventListener(
|
engineCommandManager.addEventListener(
|
||||||
EngineCommandManagerEvents.SceneReady,
|
EngineCommandManagerEvents.SceneReady,
|
||||||
async () => {
|
() => {
|
||||||
sceneInfra.camControls.dollyZoom(fov)
|
sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
@ -26,11 +27,11 @@ export const CamToggle = () => {
|
|||||||
if (isPerspective) {
|
if (isPerspective) {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.camControls.useOrthographicCamera()
|
? sceneInfra.camControls.useOrthographicCamera()
|
||||||
: sceneInfra.camControls.animateToOrthographic()
|
: sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.camControls.usePerspectiveCamera()
|
? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
|
||||||
: sceneInfra.camControls.animateToPerspective()
|
: sceneInfra.camControls.animateToPerspective().catch(reportRejection)
|
||||||
}
|
}
|
||||||
setIsPerspective(!isPerspective)
|
setIsPerspective(!isPerspective)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,17 @@ function CommandArgOptionInput({
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
inputRef.current?.select()
|
inputRef.current?.select()
|
||||||
}, [inputRef])
|
}, [inputRef])
|
||||||
|
useEffect(() => {
|
||||||
|
// work around to make sure the user doesn't have to press the down arrow key to focus the first option
|
||||||
|
// instead this makes it move from the first hit
|
||||||
|
const downArrowEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'ArrowDown',
|
||||||
|
keyCode: 40,
|
||||||
|
which: 40,
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
inputRef?.current?.dispatchEvent(downArrowEvent)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Filter the options based on the query,
|
// Filter the options based on the query,
|
||||||
// resetting the query when the options change
|
// resetting the query when the options change
|
||||||
|
@ -1,53 +1,43 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { createActorContext } from '@xstate/react'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { createContext, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { EventFrom, StateFrom } from 'xstate'
|
|
||||||
|
|
||||||
type CommandsContextType = {
|
export const CommandsContext = createActorContext(
|
||||||
commandBarState: StateFrom<typeof commandBarMachine>
|
commandBarMachine.provide({
|
||||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandsContext = createContext<CommandsContextType>({
|
|
||||||
commandBarState: commandBarMachine.initialState,
|
|
||||||
commandBarSend: () => {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
|
||||||
devTools: true,
|
|
||||||
guards: {
|
guards: {
|
||||||
'Command has no arguments': (context, _event) => {
|
'Command has no arguments': ({ context }) => {
|
||||||
return (
|
return (
|
||||||
!context.selectedCommand?.args ||
|
!context.selectedCommand?.args ||
|
||||||
Object.keys(context.selectedCommand?.args).length === 0
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'All arguments are skippable': (context, _event) => {
|
'All arguments are skippable': ({ context }) => {
|
||||||
return Object.values(context.selectedCommand!.args!).every(
|
return Object.values(context.selectedCommand!.args!).every(
|
||||||
(argConfig) => argConfig.skip
|
(argConfig) => argConfig.skip
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
export const CommandBarProvider = ({
|
||||||
editorManager.setCommandBarSend(commandBarSend)
|
children,
|
||||||
})
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<CommandsContext.Provider
|
<CommandsContext.Provider>
|
||||||
value={{
|
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||||
commandBarState,
|
|
||||||
commandBarSend,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CommandsContext.Provider>
|
</CommandsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
||||||
|
const commandBarActor = CommandsContext.useActorRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.setCommandBarSend(commandBarActor.send)
|
||||||
|
})
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
data: argumentsToSubmit,
|
output: argumentsToSubmit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
getSelectionTypeDisplayText,
|
getSelectionTypeDisplayText,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
||||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
|
|||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const initSelectionsByType = useCallback(() => {
|
const selectionsByType = useMemo(() => {
|
||||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||||
? 'none'
|
? 'none'
|
||||||
: getSelectionType(selection)
|
: getSelectionType(selection)
|
||||||
}, [selection, code])
|
}, [selection, code])
|
||||||
const selectionsByType = initSelectionsByType()
|
const canSubmitSelection = useMemo<boolean>(
|
||||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||||
canSubmitSelectionArg(selectionsByType, arg)
|
[selectionsByType]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
|
|||||||
// Fast-forward through this arg if it's marked as skippable
|
// Fast-forward through this arg if it's marked as skippable
|
||||||
// and we have a valid selection already
|
// and we have a valid selection already
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('selection input effect', {
|
|
||||||
selectionsByType,
|
|
||||||
canSubmitSelection,
|
|
||||||
arg,
|
|
||||||
})
|
|
||||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
|
||||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||||
handleSubmit({
|
handleSubmit()
|
||||||
preventDefault: () => {},
|
|
||||||
} as React.FormEvent<HTMLFormElement>)
|
|
||||||
}
|
}
|
||||||
}, [selectionsByType, arg])
|
}, [canSubmitSelection])
|
||||||
|
|
||||||
function handleChange() {
|
function handleChange() {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e?.preventDefault()
|
||||||
|
|
||||||
if (!canSubmitSelection) {
|
if (!canSubmitSelection) {
|
||||||
setHasSubmitted(true)
|
setHasSubmitted(true)
|
||||||
|
@ -11,6 +11,7 @@ export function CommandBarOpenButton() {
|
|||||||
<button
|
<button
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
|
data-testid="command-bar-open-button"
|
||||||
>
|
>
|
||||||
<span>Commands</span>
|
<span>Commands</span>
|
||||||
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
|
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommandLog } from 'lang/std/engineConnection'
|
import { CommandLog } from 'lang/std/engineConnection'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
export function useEngineCommands(): [CommandLog[], () => void] {
|
export function useEngineCommands(): [CommandLog[], () => void] {
|
||||||
@ -77,9 +78,11 @@ export const EngineCommands = () => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
data-testid="custom-cmd-send-button"
|
data-testid="custom-cmd-send-button"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
engineCommandManager
|
||||||
}
|
.sendSceneCommand(JSON.parse(customCmd))
|
||||||
|
.catch(reportRejection)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Send custom command
|
Send custom command
|
||||||
</button>
|
</button>
|
||||||
|
@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
|
|||||||
import React, { createContext } from 'react'
|
import React, { createContext } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
|
Actor,
|
||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
EventFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
Prop,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
assign,
|
fromPromise,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
|
|||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
context: ContextFrom<T>
|
context: ContextFrom<T>
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
send: Prop<Actor<T>, 'send'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileContext = createContext(
|
export const FileContext = createContext(
|
||||||
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
|
||||||
const [state, send] = useMachine(fileMachine, {
|
const [state, send] = useMachine(
|
||||||
context: {
|
fileMachine.provide({
|
||||||
project,
|
actions: {
|
||||||
selectedDirectory: project,
|
renameToastSuccess: ({ event }) => {
|
||||||
},
|
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||||
actions: {
|
toast.success(event.output.message)
|
||||||
navigateToFile: (context, event) => {
|
},
|
||||||
if (event.data && 'name' in event.data) {
|
createToastSuccess: ({ event }) => {
|
||||||
commandBarSend({ type: 'Close' })
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
navigate(
|
toast.success(event.output.message)
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
},
|
||||||
context.selectedDirectory +
|
toastSuccess: ({ event }) => {
|
||||||
window.electron.path.sep +
|
if (
|
||||||
event.data.name
|
event.type !== 'xstate.done.actor.rename-file' &&
|
||||||
)}`
|
event.type !== 'xstate.done.actor.delete-file'
|
||||||
)
|
)
|
||||||
} else if (
|
return
|
||||||
event.data &&
|
toast.success(event.output.message)
|
||||||
'path' in event.data &&
|
},
|
||||||
event.data.path.endsWith(FILE_EXT)
|
toastError: ({ event }) => {
|
||||||
) {
|
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||||
// Don't navigate to newly created directories
|
toast.error(event.output.message)
|
||||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
},
|
||||||
}
|
navigateToFile: ({ context, event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
|
if (event.output && 'name' in event.output) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
navigate(
|
||||||
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
context.selectedDirectory +
|
||||||
|
window.electron.path.sep +
|
||||||
|
event.output.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
event.output &&
|
||||||
|
'path' in event.output &&
|
||||||
|
event.output.path.endsWith(FILE_EXT)
|
||||||
|
) {
|
||||||
|
// Don't navigate to newly created directories
|
||||||
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
addFileToRenamingQueue: assign({
|
actors: {
|
||||||
itemsBeingRenamed: (context, event) => [
|
readFiles: fromPromise(async ({ input }) => {
|
||||||
...context.itemsBeingRenamed,
|
const newFiles =
|
||||||
event.data.path,
|
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
|
||||||
],
|
[]
|
||||||
}),
|
|
||||||
removeFileFromRenamingQueue: assign({
|
|
||||||
itemsBeingRenamed: (
|
|
||||||
context,
|
|
||||||
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
|
|
||||||
) =>
|
|
||||||
context.itemsBeingRenamed.filter(
|
|
||||||
(path) => path !== event.data.oldPath
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
renameToastSuccess: (_, event) => toast.success(event.data.message),
|
|
||||||
createToastSuccess: (_, event) => toast.success(event.data.message),
|
|
||||||
toastSuccess: (_, event) =>
|
|
||||||
event.data && toast.success((event.data || '') + ''),
|
|
||||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
|
||||||
const newFiles = isDesktop()
|
|
||||||
? (await getProjectInfo(context.project.path)).children
|
|
||||||
: []
|
|
||||||
return {
|
|
||||||
...context.project,
|
|
||||||
children: newFiles,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createAndOpenFile: async (context, event) => {
|
|
||||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
|
||||||
let createdPath: string
|
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
|
||||||
let { name, path } = getNextDirName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.mkdir(createdPath)
|
|
||||||
} else {
|
|
||||||
const { name, path } = getNextFileName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `Successfully created "${createdName}"`,
|
|
||||||
path: createdPath,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createFile: async (context, event) => {
|
|
||||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
|
||||||
let createdPath: string
|
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
|
||||||
let { name, path } = getNextDirName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.mkdir(createdPath)
|
|
||||||
} else {
|
|
||||||
const { name, path } = getNextFileName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: createdPath,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renameFile: async (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
|
||||||
) => {
|
|
||||||
const { oldName, newName, isDir } = event.data
|
|
||||||
const name = newName
|
|
||||||
? newName.endsWith(FILE_EXT) || isDir
|
|
||||||
? newName
|
|
||||||
: newName + FILE_EXT
|
|
||||||
: DEFAULT_FILE_NAME
|
|
||||||
const oldPath = window.electron.path.join(
|
|
||||||
context.selectedDirectory.path,
|
|
||||||
oldName
|
|
||||||
)
|
|
||||||
const newPath = window.electron.path.join(
|
|
||||||
context.selectedDirectory.path,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
|
|
||||||
// no-op
|
|
||||||
if (oldPath === newPath) {
|
|
||||||
return {
|
return {
|
||||||
message: `Old is the same as new.`,
|
...input,
|
||||||
|
children: newFiles,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createAndOpenFile: fromPromise(async ({ input }) => {
|
||||||
|
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||||
|
let createdPath: string
|
||||||
|
|
||||||
|
if (input.makeDir) {
|
||||||
|
let { name, path } = getNextDirName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.mkdir(createdPath)
|
||||||
|
} else {
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully created "${createdName}"`,
|
||||||
|
path: createdPath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createFile: fromPromise(async ({ input }) => {
|
||||||
|
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||||
|
let createdPath: string
|
||||||
|
|
||||||
|
if (input.makeDir) {
|
||||||
|
let { name, path } = getNextDirName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.mkdir(createdPath)
|
||||||
|
} else {
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: createdPath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
renameFile: fromPromise(async ({ input }) => {
|
||||||
|
const { oldName, newName, isDir } = input
|
||||||
|
const name = newName
|
||||||
|
? newName.endsWith(FILE_EXT) || isDir
|
||||||
|
? newName
|
||||||
|
: newName + FILE_EXT
|
||||||
|
: DEFAULT_FILE_NAME
|
||||||
|
const oldPath = window.electron.path.join(
|
||||||
|
input.selectedDirectory.path,
|
||||||
|
oldName
|
||||||
|
)
|
||||||
|
const newPath = window.electron.path.join(
|
||||||
|
input.selectedDirectory.path,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
|
// no-op
|
||||||
|
if (oldPath === newPath) {
|
||||||
|
return {
|
||||||
|
message: `Old is the same as new.`,
|
||||||
|
newPath,
|
||||||
|
oldPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are any siblings with the same name, report error.
|
||||||
|
const entries = await window.electron.readdir(
|
||||||
|
window.electron.path.dirname(newPath)
|
||||||
|
)
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry === newName) {
|
||||||
|
return Promise.reject(new Error('Filename already exists.'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.rename(oldPath, newPath)
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return Promise.reject(new Error('file is not defined'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldPath === file.path && project?.path) {
|
||||||
|
// If we just renamed the current file, navigate to the new path
|
||||||
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||||
|
} else if (file?.path.includes(oldPath)) {
|
||||||
|
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||||
|
navigate(
|
||||||
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
file.path.replace(oldPath, newPath)
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||||
newPath,
|
newPath,
|
||||||
oldPath,
|
oldPath,
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
|
deleteFile: fromPromise(async ({ input }) => {
|
||||||
|
const isDir = !!input.children
|
||||||
|
|
||||||
// if there are any siblings with the same name, report error.
|
if (isDir) {
|
||||||
const entries = await window.electron.readdir(
|
await window.electron
|
||||||
window.electron.path.dirname(newPath)
|
.rm(input.path, {
|
||||||
)
|
recursive: true,
|
||||||
for (let entry of entries) {
|
})
|
||||||
if (entry === newName) {
|
.catch((e) => console.error('Error deleting directory', e))
|
||||||
return Promise.reject(new Error('Filename already exists.'))
|
} else {
|
||||||
|
await window.electron
|
||||||
|
.rm(input.path)
|
||||||
|
.catch((e) => console.error('Error deleting file', e))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.electron.rename(oldPath, newPath)
|
// If there are no more files at all in the project, create a main.kcl
|
||||||
|
// for when we navigate to the root.
|
||||||
|
if (!project?.path) {
|
||||||
|
return Promise.reject(new Error('Project path not set.'))
|
||||||
|
}
|
||||||
|
|
||||||
if (!file) {
|
const entries = await window.electron.readdir(project.path)
|
||||||
return Promise.reject(new Error('file is not defined'))
|
const hasKclEntries =
|
||||||
}
|
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
|
||||||
|
if (!hasKclEntries) {
|
||||||
|
await window.electron.writeFile(
|
||||||
|
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
// Refresh the route selected above because it's possible we're on
|
||||||
|
// the same path on the navigate, which doesn't cause anything to
|
||||||
|
// refresh, leaving a stale execution state.
|
||||||
|
navigate(0)
|
||||||
|
return {
|
||||||
|
message: 'No more files in project, created main.kcl',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (oldPath === file.path && project?.path) {
|
// If we just deleted the current file or one of its parent directories,
|
||||||
// If we just renamed the current file, navigate to the new path
|
// navigate to the project root
|
||||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
if (
|
||||||
} else if (file?.path.includes(oldPath)) {
|
(input.path === file?.path || file?.path.includes(input.path)) &&
|
||||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
project?.path
|
||||||
navigate(
|
) {
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||||
file.path.replace(oldPath, newPath)
|
}
|
||||||
)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||||
newPath,
|
input.name
|
||||||
oldPath,
|
}"`,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
deleteFile: async (
|
}),
|
||||||
context: ContextFrom<typeof fileMachine>,
|
{
|
||||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
input: {
|
||||||
) => {
|
project,
|
||||||
const isDir = !!event.data.children
|
selectedDirectory: project,
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
await window.electron
|
|
||||||
.rm(event.data.path, {
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
.catch((e) => console.error('Error deleting directory', e))
|
|
||||||
} else {
|
|
||||||
await window.electron
|
|
||||||
.rm(event.data.path)
|
|
||||||
.catch((e) => console.error('Error deleting file', e))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no more files at all in the project, create a main.kcl
|
|
||||||
// for when we navigate to the root.
|
|
||||||
if (!project?.path) {
|
|
||||||
return Promise.reject(new Error('Project path not set.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await window.electron.readdir(project.path)
|
|
||||||
const hasKclEntries =
|
|
||||||
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
|
|
||||||
if (!hasKclEntries) {
|
|
||||||
await window.electron.writeFile(
|
|
||||||
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
// Refresh the route selected above because it's possible we're on
|
|
||||||
// the same path on the navigate, which doesn't cause anything to
|
|
||||||
// refresh, leaving a stale execution state.
|
|
||||||
navigate(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we just deleted the current file or one of its parent directories,
|
|
||||||
// navigate to the project root
|
|
||||||
if (
|
|
||||||
(event.data.path === file?.path ||
|
|
||||||
file?.path.includes(event.data.path)) &&
|
|
||||||
project?.path
|
|
||||||
) {
|
|
||||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
|
||||||
event.data.name
|
|
||||||
}"`
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
guards: {
|
)
|
||||||
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
|
||||||
if (event.type !== 'done.invoke.read-files') return false
|
|
||||||
return !!event?.data?.children && event.data.children.length > 0
|
|
||||||
},
|
|
||||||
'Is not silent': (_, event) => !event.data?.silent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider
|
<FileContext.Provider
|
||||||
|
@ -176,9 +176,11 @@ const FileTreeItem = ({
|
|||||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||||
codeManager.code
|
codeManager.code
|
||||||
)
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
// Prevent seeing the model built one piece at a time when changing files
|
// Prevent seeing the model built one piece at a time when changing files
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
} else {
|
} else {
|
||||||
// Let the lsp servers know we closed a file.
|
// Let the lsp servers know we closed a file.
|
||||||
@ -243,13 +245,13 @@ const FileTreeItem = ({
|
|||||||
onClickCapture={(e) =>
|
onClickCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onFocusCapture={(e) =>
|
onFocusCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||||
@ -296,13 +298,13 @@ const FileTreeItem = ({
|
|||||||
onClickCapture={(e) => {
|
onClickCapture={(e) => {
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onFocusCapture={(e) =>
|
onFocusCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -388,14 +390,14 @@ interface FileTreeProps {
|
|||||||
export const FileTreeMenu = () => {
|
export const FileTreeMenu = () => {
|
||||||
const { send } = useFileContext()
|
const { send } = useFileContext()
|
||||||
|
|
||||||
async function createFile() {
|
function createFile() {
|
||||||
send({
|
send({
|
||||||
type: 'Create file',
|
type: 'Create file',
|
||||||
data: { name: '', makeDir: false },
|
data: { name: '', makeDir: false },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFolder() {
|
function createFolder() {
|
||||||
send({
|
send({
|
||||||
type: 'Create file',
|
type: 'Create file',
|
||||||
data: { name: '', makeDir: true },
|
data: { name: '', makeDir: true },
|
||||||
@ -482,7 +484,7 @@ export const FileTreeInner = ({
|
|||||||
onClickCapture={(e) => {
|
onClickCapture={(e) => {
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileContext.project,
|
directory: fileContext.project,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
} from './ContextMenu'
|
} from './ContextMenu'
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -67,7 +68,9 @@ export default function Gizmo() {
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
key={axisName}
|
key={axisName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
|
sceneInfra.camControls
|
||||||
|
.updateCameraToAxis(axisName as AxisNames)
|
||||||
|
.catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{axisSemantic} view
|
{axisSemantic} view
|
||||||
@ -75,7 +78,7 @@ export default function Gizmo() {
|
|||||||
)),
|
)),
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition()
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset view
|
Reset view
|
||||||
@ -299,7 +302,7 @@ const initializeMouseEvents = (
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (raycasterIntersect.current) {
|
if (raycasterIntersect.current) {
|
||||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||||
sceneInfra.camControls.updateCameraToAxis(axisName)
|
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
|
|||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const HelpMenuDivider = () => (
|
const HelpMenuDivider = () => (
|
||||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||||
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
if (isInProject) {
|
if (isInProject) {
|
||||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject({ onProjectOpen, navigate })
|
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
|
||||||
|
reportRejection
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -12,6 +12,7 @@ import { CoreDumpManager } from 'lib/coredump'
|
|||||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||||
import { ModelStateIndicator } from './ModelStateIndicator'
|
import { ModelStateIndicator } from './ModelStateIndicator'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export function LowerRightControls({
|
export function LowerRightControls({
|
||||||
children,
|
children,
|
||||||
@ -25,7 +26,7 @@ export function LowerRightControls({
|
|||||||
const linkOverrideClassName =
|
const linkOverrideClassName =
|
||||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||||
|
|
||||||
async function reportbug(event: {
|
function reportbug(event: {
|
||||||
preventDefault: () => void
|
preventDefault: () => void
|
||||||
stopPropagation: () => void
|
stopPropagation: () => void
|
||||||
}) {
|
}) {
|
||||||
@ -34,7 +35,9 @@ export function LowerRightControls({
|
|||||||
|
|
||||||
if (!coreDumpManager) {
|
if (!coreDumpManager) {
|
||||||
// open default reporting option
|
// open default reporting option
|
||||||
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
|
openWindow(
|
||||||
|
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||||
|
).catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
toast
|
toast
|
||||||
.promise(
|
.promise(
|
||||||
@ -56,7 +59,7 @@ export function LowerRightControls({
|
|||||||
if (err) {
|
if (err) {
|
||||||
openWindow(
|
openWindow(
|
||||||
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||||
)
|
).catch(reportRejection)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
// Update the folding ranges, since the AST has changed.
|
// Update the folding ranges, since the AST has changed.
|
||||||
// This is a hack since codemirror does not support async foldService.
|
// This is a hack since codemirror does not support async foldService.
|
||||||
// When they do we can delete this.
|
// When they do we can delete this.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
plugin.updateFoldingRanges()
|
plugin.updateFoldingRanges()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
plugin.requestSemanticTokens()
|
plugin.requestSemanticTokens()
|
||||||
break
|
break
|
||||||
case 'kcl/memoryUpdated':
|
case 'kcl/memoryUpdated':
|
||||||
|
@ -29,6 +29,12 @@ export const ModelStateIndicator = () => {
|
|||||||
name="checkmark"
|
name="checkmark"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (lastCommandType === 'export-done') {
|
||||||
|
className +=
|
||||||
|
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||||
|
icon = (
|
||||||
|
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
|||||||
{convertToVarEnabled && (
|
{convertToVarEnabled && (
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleConvertToVarClick()}
|
onClick={() => {
|
||||||
|
handleConvertToVarClick().catch(reportRejection)
|
||||||
|
}}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
<span>Convert to Variable</span>
|
<span>Convert to Variable</span>
|
||||||
|
@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
icon: 'printer3d',
|
icon: 'printer3d',
|
||||||
iconClassName: '!p-0',
|
iconClassName: '!p-0',
|
||||||
keybinding: 'Ctrl + Shift + M',
|
keybinding: 'Ctrl + Shift + M',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
action: async () => {
|
action: async () => {
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
|
@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
|
|||||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||||
[NetworkHealthState.Ok]: 'Connected',
|
[NetworkHealthState.Ok]: 'Connected',
|
||||||
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
|
|||||||
</div>
|
</div>
|
||||||
{issues[name as ConnectingTypeGroup] && (
|
{issues[name as ConnectingTypeGroup] && (
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
JSON.stringify(error, null, 2) || ''
|
JSON.stringify(error, null, 2) || ''
|
||||||
)
|
)
|
||||||
setHasCopied(true)
|
setHasCopied(true)
|
||||||
setTimeout(() => setHasCopied(false), 5000)
|
setTimeout(() => setHasCopied(false), 5000)
|
||||||
}}
|
}, reportRejection)}
|
||||||
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
||||||
>
|
>
|
||||||
{hasCopied ? 'Copied' : 'Copy Error'}
|
{hasCopied ? 'Copied' : 'Copy Error'}
|
||||||
|
@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
|
|||||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
||||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
function ProjectCard({
|
function ProjectCard({
|
||||||
project,
|
project,
|
||||||
@ -165,10 +167,10 @@ function ProjectCard({
|
|||||||
{isConfirmingDelete && (
|
{isConfirmingDelete && (
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
title="Delete Project"
|
title="Delete Project"
|
||||||
onConfirm={async () => {
|
onConfirm={toSync(async () => {
|
||||||
await handleDeleteProject(project)
|
await handleDeleteProject(project)
|
||||||
setIsConfirmingDelete(false)
|
setIsConfirmingDelete(false)
|
||||||
}}
|
}, reportRejection)}
|
||||||
onDismiss={() => setIsConfirmingDelete(false)}
|
onDismiss={() => setIsConfirmingDelete(false)}
|
||||||
>
|
>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
|
@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const { auth } = useSettingsAuthContext()
|
||||||
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
|||||||
// Window may not be available in some environments
|
// Window may not be available in some environments
|
||||||
window?.location.reload()
|
window?.location.reload()
|
||||||
})
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={toSync(refresh, reportRejection)}
|
||||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||||
>
|
>
|
||||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||||
|
@ -12,7 +12,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||||
import { getInitialDefaultDir } from 'lib/desktop'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { APP_VERSION } from 'routes/Settings'
|
import { APP_VERSION } from 'routes/Settings'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
@ -20,6 +19,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
|
|||||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||||
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
interface AllSettingsFieldsProps {
|
interface AllSettingsFieldsProps {
|
||||||
searchParamTab: SettingsLevel
|
searchParamTab: SettingsLevel
|
||||||
@ -54,7 +55,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
async function restartOnboarding() {
|
function restartOnboarding() {
|
||||||
send({
|
send({
|
||||||
type: `set.app.onboardingStatus`,
|
type: `set.app.onboardingStatus`,
|
||||||
data: { level: 'user', value: '' },
|
data: { level: 'user', value: '' },
|
||||||
@ -82,6 +83,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
navigateToOnboardingStart()
|
navigateToOnboardingStart()
|
||||||
}, [isFileSettings, navigate, state])
|
}, [isFileSettings, navigate, state])
|
||||||
|
|
||||||
@ -190,7 +192,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
{isDesktop() && (
|
{isDesktop() && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
const paths = await getSettingsFolderPaths(
|
const paths = await getSettingsFolderPaths(
|
||||||
projectPath ? decodeURIComponent(projectPath) : undefined
|
projectPath ? decodeURIComponent(projectPath) : undefined
|
||||||
)
|
)
|
||||||
@ -199,7 +201,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
return new Error('finalPath undefined')
|
return new Error('finalPath undefined')
|
||||||
}
|
}
|
||||||
window.electron.showInFolder(finalPath)
|
window.electron.showInFolder(finalPath)
|
||||||
}}
|
}, reportRejection)}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
@ -211,13 +213,14 @@ export const AllSettingsFields = forwardRef(
|
|||||||
)}
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const defaultDirectory = await getInitialDefaultDir()
|
|
||||||
send({
|
send({
|
||||||
type: 'Reset settings',
|
type: 'Reset settings',
|
||||||
defaultDirectory,
|
level: searchParamTab,
|
||||||
})
|
})
|
||||||
toast.success('Settings restored to default')
|
toast.success(
|
||||||
|
`Your ${searchParamTab}-level settings were reset`
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'refresh',
|
icon: 'refresh',
|
||||||
@ -226,7 +229,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
bgClassName: 'bg-destroy-70',
|
bgClassName: 'bg-destroy-70',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Restore default settings
|
Reset {searchParamTab}-level settings
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Event } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
|
|
||||||
interface SettingsFieldInputProps {
|
interface SettingsFieldInputProps {
|
||||||
// We don't need the fancy types here,
|
// We don't need the fancy types here,
|
||||||
@ -59,7 +59,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: newValue,
|
value: newValue,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -103,7 +103,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{options &&
|
{options &&
|
||||||
@ -137,7 +137,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -14,16 +14,15 @@ import {
|
|||||||
Themes,
|
Themes,
|
||||||
} from 'lib/theme'
|
} from 'lib/theme'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import {
|
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
|
||||||
StateFrom,
|
|
||||||
} from 'xstate'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
import {
|
||||||
|
kclManager,
|
||||||
|
sceneInfra,
|
||||||
|
engineCommandManager,
|
||||||
|
sceneEntitiesManager,
|
||||||
|
} from 'lib/singletons'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
@ -39,7 +38,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
|
|||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
context: ContextFrom<T>
|
context: ContextFrom<T>
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
send: Prop<Actor<T>, 'send'>
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsAuthContextType = {
|
type SettingsAuthContextType = {
|
||||||
@ -50,7 +49,7 @@ type SettingsAuthContextType = {
|
|||||||
// a little hacky for sure, open to changing it
|
// a little hacky for sure, open to changing it
|
||||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
// this implies that we should only even have one instance of this provider mounted at any one time
|
||||||
// but I think that's a safe assumption
|
// but I think that's a safe assumption
|
||||||
let settingsStateRef: (typeof settingsMachine)['context'] | undefined
|
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
||||||
export const getSettingsState = () => settingsStateRef
|
export const getSettingsState = () => settingsStateRef
|
||||||
|
|
||||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||||
@ -101,21 +100,20 @@ export const SettingsAuthProviderBase = ({
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||||
settingsMachine,
|
settingsMachine.provide({
|
||||||
{
|
|
||||||
context: loadedSettings,
|
|
||||||
actions: {
|
actions: {
|
||||||
//TODO: batch all these and if that's difficult to do from tsx,
|
//TODO: batch all these and if that's difficult to do from tsx,
|
||||||
// make it easy to do
|
// make it easy to do
|
||||||
|
|
||||||
setClientSideSceneUnits: (context, event) => {
|
setClientSideSceneUnits: ({ context, event }) => {
|
||||||
const newBaseUnit =
|
const newBaseUnit =
|
||||||
event.type === 'set.modeling.defaultUnit'
|
event.type === 'set.modeling.defaultUnit'
|
||||||
? (event.data.value as BaseUnit)
|
? (event.data.value as BaseUnit)
|
||||||
: context.modeling.defaultUnit.current
|
: context.modeling.defaultUnit.current
|
||||||
sceneInfra.baseUnit = newBaseUnit
|
sceneInfra.baseUnit = newBaseUnit
|
||||||
},
|
},
|
||||||
setEngineTheme: (context) => {
|
setEngineTheme: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -126,6 +124,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -135,16 +134,18 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setEngineScaleGridVisibility: (context) => {
|
setEngineScaleGridVisibility: ({ context }) => {
|
||||||
engineCommandManager.setScaleGridVisibility(
|
engineCommandManager.setScaleGridVisibility(
|
||||||
context.modeling.showScaleGrid.current
|
context.modeling.showScaleGrid.current
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
setClientTheme: (context) => {
|
setClientTheme: ({ context }) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
},
|
},
|
||||||
setEngineEdges: (context) => {
|
setEngineEdges: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -154,7 +155,8 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toastSuccess: (_, event) => {
|
toastSuccess: ({ event }) => {
|
||||||
|
if (!('data' in event)) return
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
keyof typeof settings,
|
keyof typeof settings,
|
||||||
string
|
string
|
||||||
@ -176,7 +178,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
id: `${event.type}.success`,
|
id: `${event.type}.success`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Execute AST': (context, event) => {
|
'Execute AST': ({ context, event }) => {
|
||||||
try {
|
try {
|
||||||
const allSettingsIncludesUnitChange =
|
const allSettingsIncludesUnitChange =
|
||||||
event.type === 'Set all settings' &&
|
event.type === 'Set all settings' &&
|
||||||
@ -193,6 +195,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
// Unit changes requires a re-exec of code
|
// Unit changes requires a re-exec of code
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
} else {
|
} else {
|
||||||
// For any future logging we'd like to do
|
// For any future logging we'd like to do
|
||||||
@ -204,12 +207,13 @@ export const SettingsAuthProviderBase = ({
|
|||||||
console.error('Error executing AST after settings change', e)
|
console.error('Error executing AST after settings change', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
persistSettings: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
saveSettings(context, loadedProject?.project?.path)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
services: {
|
}),
|
||||||
'Persist settings': (context) =>
|
{ input: loadedSettings }
|
||||||
saveSettings(context, loadedProject?.project?.path),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
settingsStateRef = settingsState.context
|
||||||
|
|
||||||
@ -292,19 +296,22 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||||
|
|
||||||
// Auth machine setup
|
// Auth machine setup
|
||||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
const [authState, authSend, authActor] = useMachine(
|
||||||
actions: {
|
authMachine.provide({
|
||||||
goToSignInPage: () => {
|
actions: {
|
||||||
navigate(PATHS.SIGN_IN)
|
goToSignInPage: () => {
|
||||||
logout()
|
navigate(PATHS.SIGN_IN)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
logout()
|
||||||
|
},
|
||||||
|
goToIndexPage: () => {
|
||||||
|
if (location.pathname.includes(PATHS.SIGN_IN)) {
|
||||||
|
navigate(PATHS.INDEX)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
goToIndexPage: () => {
|
})
|
||||||
if (location.pathname.includes(PATHS.SIGN_IN)) {
|
)
|
||||||
navigate(PATHS.INDEX)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
machineId: 'auth',
|
machineId: 'auth',
|
||||||
@ -336,13 +343,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
|
|
||||||
export default SettingsAuthProvider
|
export default SettingsAuthProvider
|
||||||
|
|
||||||
export function logout() {
|
export async function logout() {
|
||||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
return (
|
if (isDesktop()) return Promise.resolve(null)
|
||||||
!isDesktop() &&
|
return fetch(withBaseUrl('/logout'), {
|
||||||
fetch(withBaseUrl('/logout'), {
|
method: 'POST',
|
||||||
method: 'POST',
|
credentials: 'include',
|
||||||
credentials: 'include',
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,10 @@ export const Stream = () => {
|
|||||||
* executed. If we can find a way to do this from a more
|
* executed. If we can find a way to do this from a more
|
||||||
* central place, we can move this code there.
|
* central place, we can move this code there.
|
||||||
*/
|
*/
|
||||||
async function executeCodeAndPlayStream() {
|
function executeCodeAndPlayStream() {
|
||||||
kclManager.executeCode(true).then(() => {
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
videoRef.current?.play().catch((e) => {
|
kclManager.executeCode(true).then(async () => {
|
||||||
|
await videoRef.current?.play().catch((e) => {
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
})
|
})
|
||||||
setStreamState(StreamState.Playing)
|
setStreamState(StreamState.Playing)
|
||||||
@ -218,12 +219,12 @@ export const Stream = () => {
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kclManager.isExecuting) {
|
if (!kclManager.isExecuting) {
|
||||||
setTimeout(() =>
|
setTimeout(() => {
|
||||||
// execute in the next event loop
|
// execute in the next event loop
|
||||||
videoRef.current?.play().catch((e) => {
|
videoRef.current?.play().catch((e) => {
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}, [kclManager.isExecuting])
|
}, [kclManager.isExecuting])
|
||||||
|
|
||||||
@ -287,9 +288,10 @@ export const Stream = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
if (state.matches('idle.showPlanes')) return
|
if (state.matches({ idle: 'showPlanes' })) return
|
||||||
|
|
||||||
if (!context.store?.didDragInStream && btnName(e).left) {
|
if (!context.store?.didDragInStream && btnName(e).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendSelectEventToEngine(
|
sendSelectEventToEngine(
|
||||||
e,
|
e,
|
||||||
videoRef.current,
|
videoRef.current,
|
||||||
|
@ -5,7 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
Color,
|
Color,
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
LineSegments,
|
LineSegments,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
|
MOUSE,
|
||||||
OrthographicCamera,
|
OrthographicCamera,
|
||||||
Scene,
|
Scene,
|
||||||
Vector3,
|
Vector3,
|
||||||
@ -26,8 +27,18 @@ import { sendTelemetry } from 'lib/textToCad'
|
|||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { EventData, EventFrom } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
CameraControls,
|
||||||
|
CameraInteractionType,
|
||||||
|
} from 'clientSideScene/CameraControls'
|
||||||
|
import {
|
||||||
|
cameraMouseDragGuards,
|
||||||
|
CameraSystem,
|
||||||
|
MouseGuard,
|
||||||
|
} from 'lib/cameraControls'
|
||||||
|
|
||||||
const CANVAS_SIZE = 128
|
const CANVAS_SIZE = 128
|
||||||
const PROMPT_TRUNCATE_LENGTH = 128
|
const PROMPT_TRUNCATE_LENGTH = 128
|
||||||
@ -45,7 +56,7 @@ export function ToastTextToCadError({
|
|||||||
prompt: string
|
prompt: string
|
||||||
commandBarSend: (
|
commandBarSend: (
|
||||||
event: EventFrom<typeof commandBarMachine>,
|
event: EventFrom<typeof commandBarMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => void
|
) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -112,13 +123,19 @@ export function ToastTextToCadSuccess({
|
|||||||
token?: string
|
token?: string
|
||||||
fileMachineSend: (
|
fileMachineSend: (
|
||||||
event: EventFrom<typeof fileMachine>,
|
event: EventFrom<typeof fileMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => void
|
) => void
|
||||||
settings: {
|
settings: {
|
||||||
theme: Themes
|
theme: Themes
|
||||||
highlightEdges: boolean
|
highlightEdges: boolean
|
||||||
|
mouseControls: CameraSystem
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
const interactionGuards = useMemo(
|
||||||
|
() => cameraMouseDragGuards[settings.mouseControls],
|
||||||
|
[settings.mouseControls]
|
||||||
|
)
|
||||||
|
const controlsRef = useRef<OrbitControls | null>(null)
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
const animationRequestRef = useRef<number>()
|
const animationRequestRef = useRef<number>()
|
||||||
@ -167,8 +184,59 @@ export function ToastTextToCadSuccess({
|
|||||||
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
|
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
const camera = createCamera()
|
const camera = createCamera()
|
||||||
|
// Because this listener is registered before the OrbitControls are created,
|
||||||
|
// it runs first and can block the OrbitControls from working.
|
||||||
|
renderer.domElement.addEventListener('pointerdown', (e) => {
|
||||||
|
if (!controlsRef.current) return
|
||||||
|
const newInteractionType = getCameraInteractionType({
|
||||||
|
interactionGuards,
|
||||||
|
event: e,
|
||||||
|
})
|
||||||
|
console.log('newInteractionType', newInteractionType)
|
||||||
|
|
||||||
|
if (newInteractionType === 'none') {
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the OrbitControls to enable only the current interaction type.
|
||||||
|
* This is a hack to override the interaction types of the OrbitControls
|
||||||
|
* to match ours. In the future, we should roll our own class based on OrbitControls,
|
||||||
|
* which can handle interaction guards that are more complex than just mouse buttons.
|
||||||
|
*/
|
||||||
|
if (newInteractionType === 'pan') {
|
||||||
|
controlsRef.current.enablePan = true
|
||||||
|
controlsRef.current.enableZoom = false
|
||||||
|
controlsRef.current.enableRotate = false
|
||||||
|
controlsRef.current.mouseButtons = {
|
||||||
|
LEFT: MOUSE.PAN,
|
||||||
|
MIDDLE: MOUSE.PAN,
|
||||||
|
RIGHT: MOUSE.PAN,
|
||||||
|
}
|
||||||
|
} else if (newInteractionType === 'zoom') {
|
||||||
|
controlsRef.current.enablePan = false
|
||||||
|
controlsRef.current.enableZoom = true
|
||||||
|
controlsRef.current.enableRotate = false
|
||||||
|
controlsRef.current.mouseButtons = {
|
||||||
|
LEFT: MOUSE.DOLLY,
|
||||||
|
MIDDLE: MOUSE.DOLLY,
|
||||||
|
RIGHT: MOUSE.DOLLY,
|
||||||
|
}
|
||||||
|
} else if (newInteractionType === 'rotate') {
|
||||||
|
controlsRef.current.enablePan = false
|
||||||
|
controlsRef.current.enableZoom = false
|
||||||
|
controlsRef.current.enableRotate = true
|
||||||
|
controlsRef.current.mouseButtons = {
|
||||||
|
LEFT: MOUSE.ROTATE,
|
||||||
|
MIDDLE: MOUSE.ROTATE,
|
||||||
|
RIGHT: MOUSE.ROTATE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update()
|
||||||
|
})
|
||||||
const controls = new OrbitControls(camera, renderer.domElement)
|
const controls = new OrbitControls(camera, renderer.domElement)
|
||||||
controls.enableDamping = true
|
controlsRef.current = controls
|
||||||
const loader = new GLTFLoader()
|
const loader = new GLTFLoader()
|
||||||
const dracoLoader = new DRACOLoader()
|
const dracoLoader = new DRACOLoader()
|
||||||
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/')
|
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/')
|
||||||
@ -269,7 +337,7 @@ export function ToastTextToCadSuccess({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 min-w-80" ref={wrapperRef}>
|
<div className="flex gap-4 min-w-80 user-select-none" ref={wrapperRef}>
|
||||||
<div
|
<div
|
||||||
className="flex-none overflow-hidden"
|
className="flex-none overflow-hidden"
|
||||||
style={{ width: CANVAS_SIZE + 'px', height: CANVAS_SIZE + 'px' }}
|
style={{ width: CANVAS_SIZE + 'px', height: CANVAS_SIZE + 'px' }}
|
||||||
@ -297,7 +365,7 @@ export function ToastTextToCadSuccess({
|
|||||||
name={hasCopied ? 'Close' : 'Reject'}
|
name={hasCopied ? 'Close' : 'Reject'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!hasCopied) {
|
if (!hasCopied) {
|
||||||
sendTelemetry(modelId, 'rejected', token)
|
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
|
||||||
}
|
}
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
// Delete the file from the project
|
// Delete the file from the project
|
||||||
@ -323,6 +391,7 @@ export function ToastTextToCadSuccess({
|
|||||||
}}
|
}}
|
||||||
name="Accept"
|
name="Accept"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendTelemetry(modelId, 'accepted', token)
|
sendTelemetry(modelId, 'accepted', token)
|
||||||
navigate(
|
navigate(
|
||||||
`${PATHS.FILE}/${encodeURIComponent(
|
`${PATHS.FILE}/${encodeURIComponent(
|
||||||
@ -342,7 +411,9 @@ export function ToastTextToCadSuccess({
|
|||||||
}}
|
}}
|
||||||
name="Copy to clipboard"
|
name="Copy to clipboard"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendTelemetry(modelId, 'accepted', token)
|
sendTelemetry(modelId, 'accepted', token)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
navigator.clipboard.writeText(data.code || '// no code found')
|
navigator.clipboard.writeText(data.code || '// no code found')
|
||||||
setShowCopiedUi(true)
|
setShowCopiedUi(true)
|
||||||
setHasCopied(true)
|
setHasCopied(true)
|
||||||
@ -407,3 +478,22 @@ function traverseSceneToStyleObjects({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCameraInteractionType({
|
||||||
|
interactionGuards,
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
interactionGuards: MouseGuard
|
||||||
|
event: MouseEvent
|
||||||
|
}): CameraInteractionType | 'none' {
|
||||||
|
if (interactionGuards.pan.callback(event)) {
|
||||||
|
return 'pan'
|
||||||
|
}
|
||||||
|
if (interactionGuards.zoom.dragCallback(event)) {
|
||||||
|
return 'zoom'
|
||||||
|
}
|
||||||
|
if (interactionGuards.rotate.callback(event)) {
|
||||||
|
return 'rotate'
|
||||||
|
}
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ interface TooltipProps extends React.PropsWithChildren {
|
|||||||
position?: TooltipPosition
|
position?: TooltipPosition
|
||||||
wrapperClassName?: string
|
wrapperClassName?: string
|
||||||
contentClassName?: string
|
contentClassName?: string
|
||||||
|
wrapperStyle?: React.CSSProperties
|
||||||
delay?: number
|
delay?: number
|
||||||
hoverOnly?: boolean
|
hoverOnly?: boolean
|
||||||
inert?: boolean
|
inert?: boolean
|
||||||
@ -22,6 +23,7 @@ export default function Tooltip({
|
|||||||
position = 'top',
|
position = 'top',
|
||||||
wrapperClassName: className,
|
wrapperClassName: className,
|
||||||
contentClassName,
|
contentClassName,
|
||||||
|
wrapperStyle = {},
|
||||||
delay = 200,
|
delay = 200,
|
||||||
hoverOnly = false,
|
hoverOnly = false,
|
||||||
inert = true,
|
inert = true,
|
||||||
@ -36,7 +38,10 @@ export default function Tooltip({
|
|||||||
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
|
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
|
||||||
styles[position]
|
styles[position]
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
|
style={Object.assign(
|
||||||
|
{ '--_delay': delay + 'ms' } as React.CSSProperties,
|
||||||
|
wrapperStyle
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
|
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
Element: 'button',
|
Element: 'button',
|
||||||
'data-testid': 'user-sidebar-sign-out',
|
'data-testid': 'user-sidebar-sign-out',
|
||||||
children: 'Sign out',
|
children: 'Sign out',
|
||||||
onClick: () => send('Log out'),
|
onClick: () => send({ type: 'Log out' }),
|
||||||
className: '', // Just making TS's filter type coercion happy 😠
|
className: '', // Just making TS's filter type coercion happy 😠
|
||||||
},
|
},
|
||||||
].filter(
|
].filter(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
forEachDiagnostic,
|
forEachDiagnostic,
|
||||||
setDiagnosticsEffect,
|
setDiagnosticsEffect,
|
||||||
} from '@codemirror/lint'
|
} from '@codemirror/lint'
|
||||||
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||||
@ -38,7 +39,7 @@ export default class EditorManager {
|
|||||||
private _lastEvent: { event: string; time: number } | null = null
|
private _lastEvent: { event: string; time: number } | null = null
|
||||||
|
|
||||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
private _modelingEvent: ModelingMachineEvent | null = null
|
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||||
|
|
||||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
||||||
() => {}
|
() => {}
|
||||||
@ -80,8 +81,8 @@ export default class EditorManager {
|
|||||||
this._modelingSend = send
|
this._modelingSend = send
|
||||||
}
|
}
|
||||||
|
|
||||||
set modelingEvent(event: ModelingMachineEvent) {
|
set modelingState(state: StateFrom<typeof modelingMachine>) {
|
||||||
this._modelingEvent = event
|
this._modelingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
||||||
@ -248,13 +249,11 @@ export default class EditorManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
|
if (!this._modelingState) {
|
||||||
|
|
||||||
if (!this._modelingEvent) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ignoreEvents.includes(this._modelingEvent.type)) {
|
if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,8 +285,9 @@ export default class EditorManager {
|
|||||||
|
|
||||||
this._lastEvent = { event: stringEvent, time: Date.now() }
|
this._lastEvent = { event: stringEvent, time: Date.now() }
|
||||||
this._modelingSend(eventInfo.modelingEvent)
|
this._modelingSend(eventInfo.modelingEvent)
|
||||||
eventInfo.engineEvents.forEach((event) =>
|
eventInfo.engineEvents.forEach((event) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand(event)
|
engineCommandManager.sendSceneCommand(event)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompleti
|
|||||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
||||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const copilotPluginAnnotation = Annotation.define<boolean>()
|
const copilotPluginAnnotation = Annotation.define<boolean>()
|
||||||
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
|
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
|
||||||
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
|
|
||||||
if (!this.client.ready) return
|
if (!this.client.ready) return
|
||||||
try {
|
try {
|
||||||
this.requestCompletions()
|
this.requestCompletions().catch(reportRejection)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
||||||
})
|
})
|
||||||
|
|
||||||
this.accept(ghostText.uuid)
|
this.accept(ghostText.uuid).catch(reportRejection)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
this.reject()
|
this.reject().catch(reportRejection)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ export class KclPlugin implements PluginValue {
|
|||||||
|
|
||||||
const newCode = viewUpdate.state.doc.toString()
|
const newCode = viewUpdate.state.doc.toString()
|
||||||
codeManager.code = newCode
|
codeManager.code = newCode
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
this.scheduleUpdateDoc()
|
this.scheduleUpdateDoc()
|
||||||
@ -117,6 +118,7 @@ export class KclPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.client.ready) return
|
if (!this.client.ready) return
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode()
|
kclManager.executeCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
const fromServer: FromServer | Error = FromServer.create()
|
const fromServer: FromServer | Error = FromServer.create()
|
||||||
@ -60,7 +60,8 @@ export async function kclLspRun(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onmessage = function (event) {
|
// WebWorker message handler.
|
||||||
|
onmessage = function (event: MessageEvent) {
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
const { worker, eventType, eventData }: LspWorkerEvent = event.data
|
const { worker, eventType, eventData }: LspWorkerEvent = event.data
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ onmessage = function (event) {
|
|||||||
| KclWorkerOptions
|
| KclWorkerOptions
|
||||||
| CopilotWorkerOptions
|
| CopilotWorkerOptions
|
||||||
initialise(wasmUrl)
|
initialise(wasmUrl)
|
||||||
.then((instantiatedModule) => {
|
.then(async (instantiatedModule) => {
|
||||||
console.log('Worker: WASM module loaded', worker, instantiatedModule)
|
console.log('Worker: WASM module loaded', worker, instantiatedModule)
|
||||||
const config = new ServerConfig(
|
const config = new ServerConfig(
|
||||||
intoServer,
|
intoServer,
|
||||||
@ -81,7 +82,7 @@ onmessage = function (event) {
|
|||||||
switch (worker) {
|
switch (worker) {
|
||||||
case LspWorker.Kcl:
|
case LspWorker.Kcl:
|
||||||
const kclData = eventData as KclWorkerOptions
|
const kclData = eventData as KclWorkerOptions
|
||||||
kclLspRun(
|
await kclLspRun(
|
||||||
config,
|
config,
|
||||||
null,
|
null,
|
||||||
kclData.token,
|
kclData.token,
|
||||||
@ -91,7 +92,11 @@ onmessage = function (event) {
|
|||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
|
await copilotLspRun(
|
||||||
|
config,
|
||||||
|
copilotData.token,
|
||||||
|
copilotData.apiBaseUrl
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -104,7 +109,7 @@ onmessage = function (event) {
|
|||||||
intoServer.enqueue(data)
|
intoServer.enqueue(data)
|
||||||
const json: jsrpc.JSONRPCRequest = Codec.decode(data)
|
const json: jsrpc.JSONRPCRequest = Codec.decode(data)
|
||||||
if (null != json.id) {
|
if (null != json.id) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
|
||||||
fromServer.responses.get(json.id)!.then((response) => {
|
fromServer.responses.get(json.id)!.then((response) => {
|
||||||
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
|
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
@ -115,19 +120,17 @@ onmessage = function (event) {
|
|||||||
console.error('Worker: Unknown message type', worker, eventType)
|
console.error('Worker: Unknown message type', worker, eventType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
;(async () => {
|
||||||
new Promise<void>(async (resolve) => {
|
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
for await (const requests of fromServer.requests) {
|
for await (const requests of fromServer.requests) {
|
||||||
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
|
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
}
|
}
|
||||||
})
|
})().catch(reportRejection)
|
||||||
|
;(async () => {
|
||||||
new Promise<void>(async (resolve) => {
|
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
for await (const notification of fromServer.notifications) {
|
for await (const notification of fromServer.notifications) {
|
||||||
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
|
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
}
|
}
|
||||||
})
|
})().catch(reportRejection)
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
export const useCommandsContext = () => {
|
export const useCommandsContext = () => {
|
||||||
return useContext(CommandsContext)
|
const commandBarActor = CommandsContext.useActorRef()
|
||||||
|
const commandBarState = CommandsContext.useSelector((state) => state)
|
||||||
|
return {
|
||||||
|
commandBarSend: commandBarActor.send,
|
||||||
|
commandBarState,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
getSolid2dCodeRef,
|
getSolid2dCodeRef,
|
||||||
getWallCodeRef,
|
getWallCodeRef,
|
||||||
} from 'lang/std/artifactGraph'
|
} from 'lang/std/artifactGraph'
|
||||||
import { err } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
|
||||||
@ -86,9 +86,11 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: async (engineEvent) => {
|
callback: (engineEvent) => {
|
||||||
const event = await getEventForSelectWithPoint(engineEvent)
|
;(async () => {
|
||||||
event && send(event)
|
const event = await getEventForSelectWithPoint(engineEvent)
|
||||||
|
event && send(event)
|
||||||
|
})().catch(reportRejection)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
@ -101,118 +103,120 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
const unSub = engineCommandManager.subscribeTo({
|
const unSub = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: state.matches('Sketch no face')
|
callback: state.matches('Sketch no face')
|
||||||
? async ({ data }) => {
|
? ({ data }) => {
|
||||||
let planeOrFaceId = data.entity_id
|
;(async () => {
|
||||||
if (!planeOrFaceId) return
|
let planeOrFaceId = data.entity_id
|
||||||
if (
|
if (!planeOrFaceId) return
|
||||||
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
if (
|
||||||
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
||||||
) {
|
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
||||||
let planeId = planeOrFaceId
|
) {
|
||||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
let planeId = planeOrFaceId
|
||||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||||
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||||
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
||||||
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
||||||
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
||||||
}
|
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
||||||
// TODO can we get this information from rust land when it creates the default planes?
|
}
|
||||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
// TODO can we get this information from rust land when it creates the default planes?
|
||||||
let zAxis: [number, number, number] = [0, 0, 1]
|
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||||
let yAxis: [number, number, number] = [0, 1, 0]
|
let zAxis: [number, number, number] = [0, 0, 1]
|
||||||
|
let yAxis: [number, number, number] = [0, 1, 0]
|
||||||
|
|
||||||
// get unit vector from camera position to target
|
// get unit vector from camera position to target
|
||||||
const camVector = sceneInfra.camControls.camera.position
|
const camVector = sceneInfra.camControls.camera.position
|
||||||
.clone()
|
.clone()
|
||||||
.sub(sceneInfra.camControls.target)
|
.sub(sceneInfra.camControls.target)
|
||||||
|
|
||||||
if (engineCommandManager.defaultPlanes?.xy === planeId) {
|
if (engineCommandManager.defaultPlanes?.xy === planeId) {
|
||||||
zAxis = [0, 0, 1]
|
zAxis = [0, 0, 1]
|
||||||
yAxis = [0, 1, 0]
|
yAxis = [0, 1, 0]
|
||||||
if (camVector.z < 0) {
|
if (camVector.z < 0) {
|
||||||
zAxis = [0, 0, -1]
|
zAxis = [0, 0, -1]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negXy || ''
|
planeId = engineCommandManager.defaultPlanes?.negXy || ''
|
||||||
}
|
}
|
||||||
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
|
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
|
||||||
zAxis = [1, 0, 0]
|
zAxis = [1, 0, 0]
|
||||||
yAxis = [0, 0, 1]
|
yAxis = [0, 0, 1]
|
||||||
if (camVector.x < 0) {
|
if (camVector.x < 0) {
|
||||||
zAxis = [-1, 0, 0]
|
zAxis = [-1, 0, 0]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negYz || ''
|
planeId = engineCommandManager.defaultPlanes?.negYz || ''
|
||||||
}
|
}
|
||||||
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
|
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
|
||||||
zAxis = [0, 1, 0]
|
zAxis = [0, 1, 0]
|
||||||
yAxis = [0, 0, 1]
|
yAxis = [0, 0, 1]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negXz || ''
|
planeId = engineCommandManager.defaultPlanes?.negXz || ''
|
||||||
if (camVector.y < 0) {
|
if (camVector.y < 0) {
|
||||||
zAxis = [0, -1, 0]
|
zAxis = [0, -1, 0]
|
||||||
planeId = engineCommandManager.defaultPlanes?.xz || ''
|
planeId = engineCommandManager.defaultPlanes?.xz || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Select default plane',
|
||||||
|
data: {
|
||||||
|
type: 'defaultPlane',
|
||||||
|
planeId: planeId,
|
||||||
|
plane: defaultPlaneStrMap[planeId],
|
||||||
|
zAxis,
|
||||||
|
yAxis,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
const faceId = planeOrFaceId
|
||||||
|
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||||
|
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||||
|
faceId,
|
||||||
|
engineCommandManager.artifactGraph
|
||||||
|
)
|
||||||
|
|
||||||
|
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
||||||
|
|
||||||
|
const codeRef =
|
||||||
|
artifact.type === 'cap'
|
||||||
|
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||||
|
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||||
|
|
||||||
|
const faceInfo = await getFaceDetails(faceId)
|
||||||
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||||
|
return
|
||||||
|
const { z_axis, y_axis, origin } = faceInfo
|
||||||
|
const sketchPathToNode = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
err(codeRef) ? [0, 0] : codeRef.range
|
||||||
|
)
|
||||||
|
|
||||||
|
const extrudePathToNode = !err(extrusion)
|
||||||
|
? getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
extrusion.codeRef.range
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
sceneInfra.modelingSend({
|
||||||
type: 'Select default plane',
|
type: 'Select default plane',
|
||||||
data: {
|
data: {
|
||||||
type: 'defaultPlane',
|
type: 'extrudeFace',
|
||||||
planeId: planeId,
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||||
plane: defaultPlaneStrMap[planeId],
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||||
zAxis,
|
position: [origin.x, origin.y, origin.z].map(
|
||||||
yAxis,
|
(num) => num / sceneInfra._baseUnitMultiplier
|
||||||
|
) as [number, number, number],
|
||||||
|
sketchPathToNode,
|
||||||
|
extrudePathToNode,
|
||||||
|
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
||||||
|
faceId: faceId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
})().catch(reportRejection)
|
||||||
const faceId = planeOrFaceId
|
|
||||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
|
||||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
|
||||||
faceId,
|
|
||||||
engineCommandManager.artifactGraph
|
|
||||||
)
|
|
||||||
|
|
||||||
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
|
||||||
|
|
||||||
const codeRef =
|
|
||||||
artifact.type === 'cap'
|
|
||||||
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
|
||||||
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
|
||||||
|
|
||||||
const faceInfo = await getFaceDetails(faceId)
|
|
||||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
|
||||||
return
|
|
||||||
const { z_axis, y_axis, origin } = faceInfo
|
|
||||||
const sketchPathToNode = getNodePathFromSourceRange(
|
|
||||||
kclManager.ast,
|
|
||||||
err(codeRef) ? [0, 0] : codeRef.range
|
|
||||||
)
|
|
||||||
|
|
||||||
const extrudePathToNode = !err(extrusion)
|
|
||||||
? getNodePathFromSourceRange(
|
|
||||||
kclManager.ast,
|
|
||||||
extrusion.codeRef.range
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Select default plane',
|
|
||||||
data: {
|
|
||||||
type: 'extrudeFace',
|
|
||||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
|
||||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
|
||||||
position: [origin.x, origin.y, origin.z].map(
|
|
||||||
(num) => num / sceneInfra._baseUnitMultiplier
|
|
||||||
) as [number, number, number],
|
|
||||||
sketchPathToNode,
|
|
||||||
extrudePathToNode,
|
|
||||||
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
|
||||||
faceId: faceId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
: () => {},
|
: () => {},
|
||||||
})
|
})
|
||||||
|
@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ctx.settings.send('Set all settings', {
|
ctx.settings.send({
|
||||||
|
type: 'Set all settings',
|
||||||
settings: routeData,
|
settings: routeData,
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
|
import { AnyStateMachine, Actor, StateFrom } from 'xstate'
|
||||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { useAppState } from 'AppState'
|
import { useAppState } from 'AppState'
|
||||||
|
import { getActorNextEvents } from 'lib/utils'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
|
|||||||
machineId: T['id']
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor: InterpreterFrom<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
allCommandsRequireNetwork?: boolean
|
allCommandsRequireNetwork?: boolean
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
|
|||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
const newCommands = state.nextEvents
|
const newCommands = getActorNextEvents(state)
|
||||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
.flatMap((type) =>
|
.flatMap((type) =>
|
||||||
|
@ -3,13 +3,14 @@ import {
|
|||||||
createSetVarNameModal,
|
createSetVarNameModal,
|
||||||
} from 'components/SetVarNameModal'
|
} from 'components/SetVarNameModal'
|
||||||
import { editorManager, kclManager } from 'lib/singletons'
|
import { editorManager, kclManager } from 'lib/singletons'
|
||||||
import { trap } from 'lib/trap'
|
import { reportRejection, trap } from 'lib/trap'
|
||||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm'
|
import { PathToNode, SourceRange } from 'lang/wasm'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
|
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
|
||||||
|
|
||||||
@ -23,8 +24,7 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}, [enable])
|
}, [enable])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parsed = parse(recast(ast))
|
const parsed = ast
|
||||||
if (trap(parsed)) return
|
|
||||||
|
|
||||||
const meta = isNodeSafeToReplace(
|
const meta = isNodeSafeToReplace(
|
||||||
parsed,
|
parsed,
|
||||||
@ -63,7 +63,7 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editorManager.convertToVariableCallback = handleClick
|
editorManager.convertToVariableCallback = toSync(handleClick, reportRejection)
|
||||||
|
|
||||||
return { enable, handleClick }
|
return { enable, handleClick }
|
||||||
}
|
}
|
||||||
|
@ -267,7 +267,8 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.segment-length-label-text {
|
.segment-length-label-text {
|
||||||
transform: translate(var(--x, 0), var(--y, 0));
|
transform: translate(var(--x, 0), var(--y, 0)) rotate(var(--degree, 0));
|
||||||
|
@apply font-mono body-bg px-2 pt-0.5 rounded-sm border border-chalkboard-110 dark:border-chalkboard-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@ -275,7 +276,7 @@ code {
|
|||||||
@apply font-mono text-xs inline-block px-0.5 py-[2px] rounded;
|
@apply font-mono text-xs inline-block px-0.5 py-[2px] rounded;
|
||||||
|
|
||||||
/* This is the only place in our code where layout is impacted by theme.
|
/* This is the only place in our code where layout is impacted by theme.
|
||||||
* We may not want that later, if hotkeys are possibly visible
|
* We may not want that later, if hotkeys are possibly visible
|
||||||
* while switching theme, but more padding feels better in dark mode.
|
* while switching theme, but more padding feels better in dark mode.
|
||||||
*/
|
*/
|
||||||
@apply dark:px-1;
|
@apply dark:px-1;
|
||||||
@ -287,32 +288,11 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */
|
/*
|
||||||
@keyframes circle-in-hesitate {
|
This is where your own custom Tailwind utility classes can go,
|
||||||
0% {
|
which lets you use them with @apply in your CSS, and get
|
||||||
clip-path: circle(
|
autocomplete in classNames in your JSX.
|
||||||
var(--circle-size-start, 0%) at var(--circle-x, 50%)
|
*/
|
||||||
var(--circle-y, 50%)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
clip-path: circle(
|
|
||||||
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
clip-path: circle(
|
|
||||||
var(--circle-size-end, 125%) at var(--circle-x, 50%)
|
|
||||||
var(--circle-y, 50%)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.in-circle-hesitate {
|
|
||||||
animation: var(--circle-duration, 2.5s)
|
|
||||||
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
|
|
||||||
both;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-mirror-override .cm-scroller,
|
#code-mirror-override .cm-scroller,
|
||||||
|
@ -129,8 +129,8 @@ export class KclManager {
|
|||||||
if (!isExecuting && this.executeIsStale) {
|
if (!isExecuting && this.executeIsStale) {
|
||||||
const args = this.executeIsStale
|
const args = this.executeIsStale
|
||||||
this.executeIsStale = null
|
this.executeIsStale = null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.executeAst(args)
|
this.executeAst(args)
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
this._isExecutingCallback(isExecuting)
|
this._isExecutingCallback(isExecuting)
|
||||||
}
|
}
|
||||||
@ -154,6 +154,7 @@ export class KclManager {
|
|||||||
constructor(engineCommandManager: EngineCommandManager) {
|
constructor(engineCommandManager: EngineCommandManager) {
|
||||||
this.engineCommandManager = engineCommandManager
|
this.engineCommandManager = engineCommandManager
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.ensureWasmInit().then(() => {
|
this.ensureWasmInit().then(() => {
|
||||||
this.ast = this.safeParse(codeManager.code) || this.ast
|
this.ast = this.safeParse(codeManager.code) || this.ast
|
||||||
})
|
})
|
||||||
@ -400,9 +401,11 @@ export class KclManager {
|
|||||||
// Update the code state and the editor.
|
// Update the code state and the editor.
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
// Write back to the file system.
|
// Write back to the file system.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
// execute the code.
|
// execute the code.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.executeCode()
|
this.executeCode()
|
||||||
}
|
}
|
||||||
// There's overlapping responsibility between updateAst and executeAst.
|
// There's overlapping responsibility between updateAst and executeAst.
|
||||||
@ -541,6 +544,7 @@ function defaultSelectionFilter(
|
|||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
programMemory.hasSketchOrExtrudeGroup() &&
|
programMemory.hasSketchOrExtrudeGroup() &&
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
|
@ -64,6 +64,7 @@ export async function executeAst({
|
|||||||
try {
|
try {
|
||||||
if (!useFakeExecutor) {
|
if (!useFakeExecutor) {
|
||||||
engineCommandManager.endSession()
|
engineCommandManager.endSession()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
}
|
}
|
||||||
const programMemory = await (useFakeExecutor
|
const programMemory = await (useFakeExecutor
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Selection } from 'lib/selections'
|
import { Selection } from 'lib/selections'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -938,115 +938,119 @@ export async function deleteFromSelection(
|
|||||||
const expressionIndex = pathToNode[1][0] as number
|
const expressionIndex = pathToNode[1][0] as number
|
||||||
astClone.body.splice(expressionIndex, 1)
|
astClone.body.splice(expressionIndex, 1)
|
||||||
if (extrudeNameToDelete) {
|
if (extrudeNameToDelete) {
|
||||||
await new Promise(async (resolve) => {
|
await new Promise((resolve) => {
|
||||||
let currentVariableName = ''
|
;(async () => {
|
||||||
const pathsDependingOnExtrude: Array<{
|
let currentVariableName = ''
|
||||||
path: PathToNode
|
const pathsDependingOnExtrude: Array<{
|
||||||
sketchName: string
|
path: PathToNode
|
||||||
}> = []
|
sketchName: string
|
||||||
traverse(astClone, {
|
}> = []
|
||||||
leave: (node) => {
|
traverse(astClone, {
|
||||||
if (node.type === 'VariableDeclaration') {
|
leave: (node) => {
|
||||||
currentVariableName = ''
|
if (node.type === 'VariableDeclaration') {
|
||||||
}
|
currentVariableName = ''
|
||||||
},
|
}
|
||||||
enter: async (node, path) => {
|
},
|
||||||
if (node.type === 'VariableDeclaration') {
|
enter: (node, path) => {
|
||||||
currentVariableName = node.declarations[0].id.name
|
;(async () => {
|
||||||
}
|
if (node.type === 'VariableDeclaration') {
|
||||||
if (
|
currentVariableName = node.declarations[0].id.name
|
||||||
// match startSketchOn(${extrudeNameToDelete})
|
}
|
||||||
node.type === 'CallExpression' &&
|
if (
|
||||||
node.callee.name === 'startSketchOn' &&
|
// match startSketchOn(${extrudeNameToDelete})
|
||||||
node.arguments[0].type === 'Identifier' &&
|
node.type === 'CallExpression' &&
|
||||||
node.arguments[0].name === extrudeNameToDelete
|
node.callee.name === 'startSketchOn' &&
|
||||||
) {
|
node.arguments[0].type === 'Identifier' &&
|
||||||
pathsDependingOnExtrude.push({
|
node.arguments[0].name === extrudeNameToDelete
|
||||||
path,
|
) {
|
||||||
sketchName: currentVariableName,
|
pathsDependingOnExtrude.push({
|
||||||
})
|
path,
|
||||||
}
|
sketchName: currentVariableName,
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
const roundLiteral = (x: number) => createLiteral(roundOff(x))
|
})().catch(reportRejection)
|
||||||
const modificationDetails: {
|
},
|
||||||
parent: PipeExpression['body']
|
|
||||||
faceDetails: Models['FaceIsPlanar_type']
|
|
||||||
lastKey: number
|
|
||||||
}[] = []
|
|
||||||
for (const { path, sketchName } of pathsDependingOnExtrude) {
|
|
||||||
const parent = getNodeFromPath<PipeExpression['body']>(
|
|
||||||
astClone,
|
|
||||||
path.slice(0, -1)
|
|
||||||
)
|
|
||||||
if (err(parent)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sketchToPreserve = sketchGroupFromKclValue(
|
|
||||||
programMemory.get(sketchName),
|
|
||||||
sketchName
|
|
||||||
)
|
|
||||||
if (err(sketchToPreserve)) return sketchToPreserve
|
|
||||||
console.log('sketchName', sketchName)
|
|
||||||
// Can't kick off multiple requests at once as getFaceDetails
|
|
||||||
// is three engine calls in one and they conflict
|
|
||||||
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
faceDetails.origin &&
|
|
||||||
faceDetails.x_axis &&
|
|
||||||
faceDetails.y_axis &&
|
|
||||||
faceDetails.z_axis
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const lastKey = Number(path.slice(-1)[0][0])
|
|
||||||
modificationDetails.push({
|
|
||||||
parent: parent.node,
|
|
||||||
faceDetails,
|
|
||||||
lastKey,
|
|
||||||
})
|
})
|
||||||
}
|
const roundLiteral = (x: number) => createLiteral(roundOff(x))
|
||||||
for (const { parent, faceDetails, lastKey } of modificationDetails) {
|
const modificationDetails: {
|
||||||
if (
|
parent: PipeExpression['body']
|
||||||
!(
|
faceDetails: Models['FaceIsPlanar_type']
|
||||||
faceDetails.origin &&
|
lastKey: number
|
||||||
faceDetails.x_axis &&
|
}[] = []
|
||||||
faceDetails.y_axis &&
|
for (const { path, sketchName } of pathsDependingOnExtrude) {
|
||||||
faceDetails.z_axis
|
const parent = getNodeFromPath<PipeExpression['body']>(
|
||||||
|
astClone,
|
||||||
|
path.slice(0, -1)
|
||||||
)
|
)
|
||||||
) {
|
if (err(parent)) {
|
||||||
continue
|
return
|
||||||
|
}
|
||||||
|
const sketchToPreserve = sketchGroupFromKclValue(
|
||||||
|
programMemory.get(sketchName),
|
||||||
|
sketchName
|
||||||
|
)
|
||||||
|
if (err(sketchToPreserve)) return sketchToPreserve
|
||||||
|
console.log('sketchName', sketchName)
|
||||||
|
// Can't kick off multiple requests at once as getFaceDetails
|
||||||
|
// is three engine calls in one and they conflict
|
||||||
|
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
faceDetails.origin &&
|
||||||
|
faceDetails.x_axis &&
|
||||||
|
faceDetails.y_axis &&
|
||||||
|
faceDetails.z_axis
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lastKey = Number(path.slice(-1)[0][0])
|
||||||
|
modificationDetails.push({
|
||||||
|
parent: parent.node,
|
||||||
|
faceDetails,
|
||||||
|
lastKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
|
for (const { parent, faceDetails, lastKey } of modificationDetails) {
|
||||||
createObjectExpression({
|
if (
|
||||||
plane: createObjectExpression({
|
!(
|
||||||
origin: createObjectExpression({
|
faceDetails.origin &&
|
||||||
x: roundLiteral(faceDetails.origin.x),
|
faceDetails.x_axis &&
|
||||||
y: roundLiteral(faceDetails.origin.y),
|
faceDetails.y_axis &&
|
||||||
z: roundLiteral(faceDetails.origin.z),
|
faceDetails.z_axis
|
||||||
}),
|
)
|
||||||
x_axis: createObjectExpression({
|
) {
|
||||||
x: roundLiteral(faceDetails.x_axis.x),
|
continue
|
||||||
y: roundLiteral(faceDetails.x_axis.y),
|
}
|
||||||
z: roundLiteral(faceDetails.x_axis.z),
|
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
|
||||||
}),
|
createObjectExpression({
|
||||||
y_axis: createObjectExpression({
|
plane: createObjectExpression({
|
||||||
x: roundLiteral(faceDetails.y_axis.x),
|
origin: createObjectExpression({
|
||||||
y: roundLiteral(faceDetails.y_axis.y),
|
x: roundLiteral(faceDetails.origin.x),
|
||||||
z: roundLiteral(faceDetails.y_axis.z),
|
y: roundLiteral(faceDetails.origin.y),
|
||||||
}),
|
z: roundLiteral(faceDetails.origin.z),
|
||||||
z_axis: createObjectExpression({
|
}),
|
||||||
x: roundLiteral(faceDetails.z_axis.x),
|
x_axis: createObjectExpression({
|
||||||
y: roundLiteral(faceDetails.z_axis.y),
|
x: roundLiteral(faceDetails.x_axis.x),
|
||||||
z: roundLiteral(faceDetails.z_axis.z),
|
y: roundLiteral(faceDetails.x_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.x_axis.z),
|
||||||
|
}),
|
||||||
|
y_axis: createObjectExpression({
|
||||||
|
x: roundLiteral(faceDetails.y_axis.x),
|
||||||
|
y: roundLiteral(faceDetails.y_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.y_axis.z),
|
||||||
|
}),
|
||||||
|
z_axis: createObjectExpression({
|
||||||
|
x: roundLiteral(faceDetails.z_axis.x),
|
||||||
|
y: roundLiteral(faceDetails.z_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.z_axis.z),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
])
|
||||||
])
|
}
|
||||||
}
|
resolve(true)
|
||||||
resolve(true)
|
})().catch(reportRejection)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// await prom
|
// await prom
|
||||||
|
@ -36,7 +36,7 @@ beforeAll(async () => {
|
|||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
modifyGrid: async () => {},
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -49,6 +49,22 @@ export function applyFilletToSelection(
|
|||||||
): void | Error {
|
): void | Error {
|
||||||
// 1. get AST
|
// 1. get AST
|
||||||
let ast = kclManager.ast
|
let ast = kclManager.ast
|
||||||
|
|
||||||
|
// 2. modify ast clone with fillet and tag
|
||||||
|
const result = modifyAstWithFilletAndTag(ast, selection, radius)
|
||||||
|
if (err(result)) return result
|
||||||
|
const { modifiedAst, pathToFilletNode } = result
|
||||||
|
|
||||||
|
// 3. update ast
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
updateAstAndFocus(modifiedAst, pathToFilletNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyAstWithFilletAndTag(
|
||||||
|
ast: Program,
|
||||||
|
selection: Selections,
|
||||||
|
radius: KclCommandValue
|
||||||
|
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
||||||
const astResult = insertRadiusIntoAst(ast, radius)
|
const astResult = insertRadiusIntoAst(ast, radius)
|
||||||
if (err(astResult)) return astResult
|
if (err(astResult)) return astResult
|
||||||
|
|
||||||
@ -77,8 +93,7 @@ export function applyFilletToSelection(
|
|||||||
if (trap(addFilletResult)) return addFilletResult
|
if (trap(addFilletResult)) return addFilletResult
|
||||||
const { modifiedAst, pathToFilletNode } = addFilletResult
|
const { modifiedAst, pathToFilletNode } = addFilletResult
|
||||||
|
|
||||||
// 4. update ast
|
return { modifiedAst, pathToFilletNode }
|
||||||
updateAstAndFocus(modifiedAst, pathToFilletNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertRadiusIntoAst(
|
function insertRadiusIntoAst(
|
||||||
|
@ -124,6 +124,7 @@ beforeAll(async () => {
|
|||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
modifyGrid: async () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: async () => {
|
||||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||||
CodeKey,
|
CodeKey,
|
||||||
|
@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
|
|||||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
|
export type ArtifactId = string
|
||||||
|
|
||||||
interface CommonCommandProperties {
|
interface CommonCommandProperties {
|
||||||
range: SourceRange
|
range: SourceRange
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
@ -10,7 +12,7 @@ interface CommonCommandProperties {
|
|||||||
|
|
||||||
export interface PlaneArtifact {
|
export interface PlaneArtifact {
|
||||||
type: 'plane'
|
type: 'plane'
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
export interface PlaneArtifactRich {
|
export interface PlaneArtifactRich {
|
||||||
@ -21,16 +23,16 @@ export interface PlaneArtifactRich {
|
|||||||
|
|
||||||
export interface PathArtifact {
|
export interface PathArtifact {
|
||||||
type: 'path'
|
type: 'path'
|
||||||
planeId: string
|
planeId: ArtifactId
|
||||||
segIds: Array<string>
|
segIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
solid2dId?: string
|
solid2dId?: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
interface solid2D {
|
interface solid2D {
|
||||||
type: 'solid2D'
|
type: 'solid2D'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
}
|
}
|
||||||
export interface PathArtifactRich {
|
export interface PathArtifactRich {
|
||||||
type: 'path'
|
type: 'path'
|
||||||
@ -42,10 +44,10 @@ export interface PathArtifactRich {
|
|||||||
|
|
||||||
interface SegmentArtifact {
|
interface SegmentArtifact {
|
||||||
type: 'segment'
|
type: 'segment'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
edgeCutId?: string
|
edgeCutId?: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
interface SegmentArtifactRich {
|
interface SegmentArtifactRich {
|
||||||
@ -59,9 +61,9 @@ interface SegmentArtifactRich {
|
|||||||
|
|
||||||
interface ExtrusionArtifact {
|
interface ExtrusionArtifact {
|
||||||
type: 'extrusion'
|
type: 'extrusion'
|
||||||
pathId: string
|
pathId: ArtifactId
|
||||||
surfaceIds: Array<string>
|
surfaceIds: Array<ArtifactId>
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
interface ExtrusionArtifactRich {
|
interface ExtrusionArtifactRich {
|
||||||
@ -74,23 +76,23 @@ interface ExtrusionArtifactRich {
|
|||||||
|
|
||||||
interface WallArtifact {
|
interface WallArtifact {
|
||||||
type: 'wall'
|
type: 'wall'
|
||||||
segId: string
|
segId: ArtifactId
|
||||||
edgeCutEdgeIds: Array<string>
|
edgeCutEdgeIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
}
|
}
|
||||||
interface CapArtifact {
|
interface CapArtifact {
|
||||||
type: 'cap'
|
type: 'cap'
|
||||||
subType: 'start' | 'end'
|
subType: 'start' | 'end'
|
||||||
edgeCutEdgeIds: Array<string>
|
edgeCutEdgeIds: Array<ArtifactId>
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
pathIds: Array<string>
|
pathIds: Array<ArtifactId>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtrudeEdge {
|
interface ExtrudeEdge {
|
||||||
type: 'extrudeEdge'
|
type: 'extrudeEdge'
|
||||||
segId: string
|
segId: ArtifactId
|
||||||
extrusionId: string
|
extrusionId: ArtifactId
|
||||||
subType: 'opposite' | 'adjacent'
|
subType: 'opposite' | 'adjacent'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,16 +100,16 @@ interface ExtrudeEdge {
|
|||||||
interface EdgeCut {
|
interface EdgeCut {
|
||||||
type: 'edgeCut'
|
type: 'edgeCut'
|
||||||
subType: 'fillet' | 'chamfer'
|
subType: 'fillet' | 'chamfer'
|
||||||
consumedEdgeId: string
|
consumedEdgeId: ArtifactId
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<ArtifactId>
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
codeRef: CommonCommandProperties
|
codeRef: CommonCommandProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EdgeCutEdge {
|
interface EdgeCutEdge {
|
||||||
type: 'edgeCutEdge'
|
type: 'edgeCutEdge'
|
||||||
edgeCutId: string
|
edgeCutId: ArtifactId
|
||||||
surfaceId: string
|
surfaceId: ArtifactId
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Artifact =
|
export type Artifact =
|
||||||
@ -122,7 +124,7 @@ export type Artifact =
|
|||||||
| EdgeCutEdge
|
| EdgeCutEdge
|
||||||
| solid2D
|
| solid2D
|
||||||
|
|
||||||
export type ArtifactGraph = Map<string, Artifact>
|
export type ArtifactGraph = Map<ArtifactId, Artifact>
|
||||||
|
|
||||||
export type EngineCommand = Models['WebSocketRequest_type']
|
export type EngineCommand = Models['WebSocketRequest_type']
|
||||||
|
|
||||||
@ -149,7 +151,7 @@ export function createArtifactGraph({
|
|||||||
responseMap: ResponseMap
|
responseMap: ResponseMap
|
||||||
ast: Program
|
ast: Program
|
||||||
}) {
|
}) {
|
||||||
const myMap = new Map<string, Artifact>()
|
const myMap = new Map<ArtifactId, Artifact>()
|
||||||
|
|
||||||
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
||||||
let currentPlaneId = ''
|
let currentPlaneId = ''
|
||||||
@ -166,7 +168,7 @@ export function createArtifactGraph({
|
|||||||
const artifactsToUpdate = getArtifactsToUpdate({
|
const artifactsToUpdate = getArtifactsToUpdate({
|
||||||
orderedCommand,
|
orderedCommand,
|
||||||
responseMap,
|
responseMap,
|
||||||
getArtifact: (id: string) => myMap.get(id),
|
getArtifact: (id: ArtifactId) => myMap.get(id),
|
||||||
currentPlaneId,
|
currentPlaneId,
|
||||||
ast,
|
ast,
|
||||||
})
|
})
|
||||||
@ -224,11 +226,11 @@ export function getArtifactsToUpdate({
|
|||||||
orderedCommand: OrderedCommand
|
orderedCommand: OrderedCommand
|
||||||
responseMap: ResponseMap
|
responseMap: ResponseMap
|
||||||
/** Passing in a getter because we don't wan this function to update the map directly */
|
/** Passing in a getter because we don't wan this function to update the map directly */
|
||||||
getArtifact: (id: string) => Artifact | undefined
|
getArtifact: (id: ArtifactId) => Artifact | undefined
|
||||||
currentPlaneId: string
|
currentPlaneId: ArtifactId
|
||||||
ast: Program
|
ast: Program
|
||||||
}): Array<{
|
}): Array<{
|
||||||
id: string
|
id: ArtifactId
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
}> {
|
}> {
|
||||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||||
@ -514,7 +516,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
|
|||||||
(!predicate ||
|
(!predicate ||
|
||||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||||
)
|
)
|
||||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
||||||
@ -528,7 +530,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
|||||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||||
},
|
},
|
||||||
map: ArtifactGraph
|
map: ArtifactGraph
|
||||||
): Map<string, Extract<Artifact, { type: T[number] }>> {
|
): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
|
||||||
return new Map(
|
return new Map(
|
||||||
[...map].filter(
|
[...map].filter(
|
||||||
([key, value]) =>
|
([key, value]) =>
|
||||||
@ -537,7 +539,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
|||||||
(!predicate ||
|
(!predicate ||
|
||||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||||
)
|
)
|
||||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
||||||
@ -545,7 +547,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
|||||||
key,
|
key,
|
||||||
types,
|
types,
|
||||||
}: {
|
}: {
|
||||||
key: string
|
key: ArtifactId
|
||||||
types: T
|
types: T
|
||||||
},
|
},
|
||||||
map: ArtifactGraph
|
map: ArtifactGraph
|
||||||
@ -718,7 +720,7 @@ export function getExtrudeEdgeCodeRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getExtrusionFromSuspectedExtrudeSurface(
|
export function getExtrusionFromSuspectedExtrudeSurface(
|
||||||
id: string,
|
id: ArtifactId,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): ExtrusionArtifact | Error {
|
): ExtrusionArtifact | Error {
|
||||||
const artifact = getArtifactOfTypes(
|
const artifact = getArtifactOfTypes(
|
||||||
@ -733,7 +735,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getExtrusionFromSuspectedPath(
|
export function getExtrusionFromSuspectedPath(
|
||||||
id: string,
|
id: ArtifactId,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): ExtrusionArtifact | Error {
|
): ExtrusionArtifact | Error {
|
||||||
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
||||||
|
@ -18,6 +18,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
import { KclManager } from 'lang/KclSingleton'
|
import { KclManager } from 'lang/KclSingleton'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
// TODO(paultag): This ought to be tweakable.
|
// TODO(paultag): This ought to be tweakable.
|
||||||
const pingIntervalMs = 5_000
|
const pingIntervalMs = 5_000
|
||||||
@ -388,11 +389,12 @@ class EngineConnection extends EventTarget {
|
|||||||
default:
|
default:
|
||||||
if (this.isConnecting()) break
|
if (this.isConnecting()) break
|
||||||
// Means we never could do an initial connection. Reconnect everything.
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
if (!this.pingPongSpan.ping) this.connect()
|
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.connect()
|
this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1252,6 +1254,10 @@ export type CommandLog =
|
|||||||
type: 'execution-done'
|
type: 'execution-done'
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'export-done'
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
|
||||||
export enum EngineCommandManagerEvents {
|
export enum EngineCommandManagerEvents {
|
||||||
// engineConnection is available but scene setup may not have run
|
// engineConnection is available but scene setup may not have run
|
||||||
@ -1460,6 +1466,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.onEngineConnectionOpened = async () => {
|
this.onEngineConnectionOpened = async () => {
|
||||||
// Set the stream background color
|
// Set the stream background color
|
||||||
// This takes RGBA values from 0-1
|
// This takes RGBA values from 0-1
|
||||||
@ -1476,6 +1483,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
|
|
||||||
// Sets the default line colors
|
// Sets the default line colors
|
||||||
const opposingTheme = getOppositeTheme(this.settings.theme)
|
const opposingTheme = getOppositeTheme(this.settings.theme)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -1486,6 +1494,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Set the edge lines visibility
|
// Set the edge lines visibility
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
@ -1496,6 +1505,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this._camControlsCameraChange()
|
this._camControlsCameraChange()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
// CameraControls subscribes to default_camera_get_settings response events
|
// CameraControls subscribes to default_camera_get_settings response events
|
||||||
// firing this at connection ensure the camera's are synced initially
|
// firing this at connection ensure the camera's are synced initially
|
||||||
@ -1508,6 +1518,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
// We want modify the grid first because we don't want it to flash.
|
// We want modify the grid first because we don't want it to flash.
|
||||||
// Ideally these would already be default hidden in engine (TODO do
|
// Ideally these would already be default hidden in engine (TODO do
|
||||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
// that) https://github.com/KittyCAD/engine/issues/2282
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
||||||
await this.initPlanes()
|
await this.initPlanes()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
@ -1711,6 +1722,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
this.onEngineConnectionNewTrack as EventListener
|
this.onEngineConnectionNewTrack as EventListener
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect()
|
||||||
}
|
}
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
@ -1918,7 +1930,13 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
} else if (cmd.type === 'export') {
|
} else if (cmd.type === 'export') {
|
||||||
const promise = new Promise<null>((resolve, reject) => {
|
const promise = new Promise<null>((resolve, reject) => {
|
||||||
this.pendingExport = {
|
this.pendingExport = {
|
||||||
resolve,
|
resolve: (passThrough) => {
|
||||||
|
this.addCommandLog({
|
||||||
|
type: 'export-done',
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
resolve(passThrough)
|
||||||
|
},
|
||||||
reject: (reason: string) => {
|
reject: (reason: string) => {
|
||||||
this.exportIntent = null
|
this.exportIntent = null
|
||||||
reject(reason)
|
reject(reason)
|
||||||
@ -2115,6 +2133,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
* @param visible - whether to show or hide the scale grid
|
* @param visible - whether to show or hide the scale grid
|
||||||
*/
|
*/
|
||||||
setScaleGridVisibility(visible: boolean) {
|
setScaleGridVisibility(visible: boolean) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.modifyGrid(!visible)
|
this.modifyGrid(!visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,6 +360,7 @@ export const executor = async (
|
|||||||
): Promise<ProgramMemory> => {
|
): Promise<ProgramMemory> => {
|
||||||
if (err(programMemory)) return Promise.reject(programMemory)
|
if (err(programMemory)) return Promise.reject(programMemory)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
const _programMemory = await _executor(
|
const _programMemory = await _executor(
|
||||||
node,
|
node,
|
||||||
@ -569,6 +570,7 @@ export async function coreDump(
|
|||||||
a new GitHub issue for the user.
|
a new GitHub issue for the user.
|
||||||
*/
|
*/
|
||||||
if (openGithubIssue && dump.github_issue_url) {
|
if (openGithubIssue && dump.github_issue_url) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
openWindow(dump.github_issue_url)
|
openWindow(dump.github_issue_url)
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -6,7 +6,7 @@ const META =
|
|||||||
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
|
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
|
||||||
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
|
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
|
||||||
|
|
||||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
const noModifiersPressed = (e: React.MouseEvent | MouseEvent) =>
|
||||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
export type CameraSystem =
|
export type CameraSystem =
|
||||||
@ -53,14 +53,14 @@ export function mouseControlsToCameraSystem(
|
|||||||
|
|
||||||
interface MouseGuardHandler {
|
interface MouseGuardHandler {
|
||||||
description: string
|
description: string
|
||||||
callback: (e: React.MouseEvent) => boolean
|
callback: (e: React.MouseEvent | MouseEvent) => boolean
|
||||||
lenientDragStartButton?: number
|
lenientDragStartButton?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MouseGuardZoomHandler {
|
interface MouseGuardZoomHandler {
|
||||||
description: string
|
description: string
|
||||||
dragCallback: (e: React.MouseEvent) => boolean
|
dragCallback: (e: React.MouseEvent | MouseEvent) => boolean
|
||||||
scrollCallback: (e: React.MouseEvent) => boolean
|
scrollCallback: (e: React.MouseEvent | MouseEvent) => boolean
|
||||||
lenientDragStartButton?: number
|
lenientDragStartButton?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ export interface MouseGuard {
|
|||||||
rotate: MouseGuardHandler
|
rotate: MouseGuardHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export const btnName = (e: React.MouseEvent) => ({
|
export const btnName = (e: React.MouseEvent | MouseEvent) => ({
|
||||||
middle: !!(e.buttons & 4) || e.button === 1,
|
middle: !!(e.buttons & 4) || e.button === 1,
|
||||||
right: !!(e.buttons & 2) || e.button === 2,
|
right: !!(e.buttons & 2) || e.button === 2,
|
||||||
left: !!(e.buttons & 1) || e.button === 0,
|
left: !!(e.buttons & 1) || e.button === 0,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { PathValue } from 'lib/types'
|
import { PathValue } from 'lib/types'
|
||||||
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate'
|
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
|
||||||
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
||||||
import { buildCommandArgument } from 'lib/createMachineCommand'
|
import { buildCommandArgument } from 'lib/createMachineCommand'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
|
|||||||
) as SettingsPaths[]
|
) as SettingsPaths[]
|
||||||
|
|
||||||
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
||||||
actor: InterpreterFrom<T>,
|
actor: Actor<T>,
|
||||||
isProjectAvailable: boolean,
|
isProjectAvailable: boolean,
|
||||||
hideOnLevel?: SettingsLevel
|
hideOnLevel?: SettingsLevel
|
||||||
): CommandArgument<SettingsLevel, T> => ({
|
): CommandArgument<SettingsLevel, T> => ({
|
||||||
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
|
|||||||
type: SettingsPaths
|
type: SettingsPaths
|
||||||
send: Function
|
send: Function
|
||||||
context: ContextFrom<typeof settingsMachine>
|
context: ContextFrom<typeof settingsMachine>
|
||||||
actor: InterpreterFrom<typeof settingsMachine>
|
actor: Actor<typeof settingsMachine>
|
||||||
isProjectAvailable: boolean
|
isProjectAvailable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ export function createSettingsCommand({
|
|||||||
if (data !== undefined && data !== null) {
|
if (data !== undefined && data !== null) {
|
||||||
send({ type: `set.${type}`, data })
|
send({ type: `set.${type}`, data })
|
||||||
} else {
|
} else {
|
||||||
send(type)
|
send({ type })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { AllMachines } from 'hooks/useStateMachineCommands'
|
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||||
import {
|
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
EventFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
} from 'xstate'
|
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
@ -186,7 +181,7 @@ export type CommandArgument<
|
|||||||
machineContext?: ContextFrom<T>
|
machineContext?: ContextFrom<T>
|
||||||
) => boolean)
|
) => boolean)
|
||||||
skip?: boolean
|
skip?: boolean
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
/** For showing a summary display of the current value, such as in
|
/** For showing a summary display of the current value, such as in
|
||||||
* the command bar's header
|
* the command bar's header
|
||||||
*/
|
*/
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
EventFrom,
|
EventFrom,
|
||||||
InterpreterFrom,
|
Actor,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
|
|||||||
groupId: T['id']
|
groupId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor: InterpreterFrom<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
@ -90,9 +90,9 @@ export function createMachineCommand<
|
|||||||
needsReview: commandConfig.needsReview || false,
|
needsReview: commandConfig.needsReview || false,
|
||||||
onSubmit: (data?: S[typeof type]) => {
|
onSubmit: (data?: S[typeof type]) => {
|
||||||
if (data !== undefined && data !== null) {
|
if (data !== undefined && data !== null) {
|
||||||
send(type, { data })
|
send({ type, data })
|
||||||
} else {
|
} else {
|
||||||
send(type)
|
send({ type })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ function buildCommandArguments<
|
|||||||
>(
|
>(
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
args: CommandConfig<T, CommandName, S>['args'],
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
): NonNullable<Command<T, CommandName, S>['args']> {
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export function buildCommandArgument<
|
|||||||
>(
|
>(
|
||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
context: ContextFrom<T>,
|
context: ContextFrom<T>,
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
parseProjectSettings,
|
parseProjectSettings,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
DEFAULT_HOST,
|
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
PROJECT_FOLDER,
|
PROJECT_FOLDER,
|
||||||
PROJECT_SETTINGS_FILE_NAME,
|
PROJECT_SETTINGS_FILE_NAME,
|
||||||
@ -462,29 +461,60 @@ export const readProjectSettingsFile = async (
|
|||||||
*/
|
*/
|
||||||
export const readAppSettingsFile = async () => {
|
export const readAppSettingsFile = async () => {
|
||||||
let settingsPath = await getAppSettingsFilePath()
|
let settingsPath = await getAppSettingsFilePath()
|
||||||
|
const initialProjectDirConfig: DeepPartial<
|
||||||
|
Configuration['settings']['project']
|
||||||
|
> = { directory: await getInitialDefaultDir() }
|
||||||
|
|
||||||
// The file exists, read it and parse it.
|
// The file exists, read it and parse it.
|
||||||
if (window.electron.exists(settingsPath)) {
|
if (window.electron.exists(settingsPath)) {
|
||||||
const configToml = await window.electron.readFile(settingsPath)
|
const configToml = await window.electron.readFile(settingsPath)
|
||||||
const configObj = parseAppSettings(configToml)
|
const parsedAppConfig = parseAppSettings(configToml)
|
||||||
if (err(configObj)) {
|
if (err(parsedAppConfig)) {
|
||||||
return Promise.reject(configObj)
|
return Promise.reject(parsedAppConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return configObj
|
const hasProjectDirectorySetting =
|
||||||
|
parsedAppConfig.settings?.project?.directory ||
|
||||||
|
parsedAppConfig.settings?.app?.project_directory
|
||||||
|
|
||||||
|
if (hasProjectDirectorySetting) {
|
||||||
|
return parsedAppConfig
|
||||||
|
} else {
|
||||||
|
// inject the default project directory setting
|
||||||
|
const mergedConfig: DeepPartial<Configuration> = {
|
||||||
|
...parsedAppConfig,
|
||||||
|
settings: {
|
||||||
|
...parsedAppConfig.settings,
|
||||||
|
project: Object.assign(
|
||||||
|
{},
|
||||||
|
parsedAppConfig.settings?.project,
|
||||||
|
initialProjectDirConfig
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return mergedConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The file doesn't exist, create a new one.
|
// The file doesn't exist, create a new one.
|
||||||
// This defaultAppConfig is truly an empty object every time.
|
|
||||||
const defaultAppConfig = defaultAppSettings()
|
const defaultAppConfig = defaultAppSettings()
|
||||||
if (err(defaultAppConfig)) {
|
if (err(defaultAppConfig)) {
|
||||||
return Promise.reject(defaultAppConfig)
|
return Promise.reject(defaultAppConfig)
|
||||||
}
|
}
|
||||||
const initialDirConfig: DeepPartial<Configuration> = {
|
|
||||||
settings: { project: { directory: await getInitialDefaultDir() } },
|
// inject the default project directory setting
|
||||||
|
const mergedDefaultConfig: DeepPartial<Configuration> = {
|
||||||
|
...defaultAppConfig,
|
||||||
|
settings: {
|
||||||
|
...defaultAppConfig.settings,
|
||||||
|
project: Object.assign(
|
||||||
|
{},
|
||||||
|
defaultAppConfig.settings?.project,
|
||||||
|
initialProjectDirConfig
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
const config = Object.assign(defaultAppConfig, initialDirConfig)
|
return mergedDefaultConfig
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const writeAppSettingsFile = async (tomlStr: string) => {
|
export const writeAppSettingsFile = async (tomlStr: string) => {
|
||||||
@ -525,28 +555,6 @@ export const getUser = async (
|
|||||||
token: string,
|
token: string,
|
||||||
hostname: string
|
hostname: string
|
||||||
): Promise<Models['User_type']> => {
|
): Promise<Models['User_type']> => {
|
||||||
// Use the host passed in if it's set.
|
|
||||||
// Otherwise, use the default host.
|
|
||||||
const host = !hostname ? DEFAULT_HOST : hostname
|
|
||||||
|
|
||||||
// Change the baseURL to the one we want.
|
|
||||||
let baseurl = host
|
|
||||||
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
|
|
||||||
baseurl = `https://${host}`
|
|
||||||
if (host.indexOf('localhost') === 0) {
|
|
||||||
baseurl = `http://${host}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use kittycad library to fetch the user info from /user/me
|
|
||||||
if (baseurl !== DEFAULT_HOST) {
|
|
||||||
// The TypeScript generated library uses environment variables for this
|
|
||||||
// because it was intended for NodeJS.
|
|
||||||
// Needs to stay like this because window.electron.kittycad needs it
|
|
||||||
// internally.
|
|
||||||
window.electron.setBaseUrl(baseurl)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await window.electron.kittycad('users.get_user_self', {
|
const user = await window.electron.kittycad('users.get_user_self', {
|
||||||
client: { token },
|
client: { token },
|
||||||
|
@ -14,7 +14,7 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
extensions.push(extension)
|
extensions.push(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(window as any).playwrightSkipFilePicker) {
|
if (window.electron.process.env.IS_PLAYWRIGHT) {
|
||||||
// skip file picker, save to default location
|
// skip file picker, save to default location
|
||||||
await window.electron.writeFile(
|
await window.electron.writeFile(
|
||||||
file.name,
|
file.name,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
import { components } from './machine-api'
|
import { components } from './machine-api'
|
||||||
|
import { reportRejection } from './trap'
|
||||||
|
import { toSync } from './utils'
|
||||||
|
|
||||||
export type MachinesListing = Array<
|
export type MachinesListing = Array<
|
||||||
components['schemas']['MachineInfoResponse']
|
components['schemas']['MachineInfoResponse']
|
||||||
@ -17,7 +19,7 @@ export class MachineManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateMachines()
|
this.updateMachines().catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@ -31,11 +33,14 @@ export class MachineManager {
|
|||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
const timeoutLoop = () => {
|
const timeoutLoop = () => {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
timeoutId = setTimeout(async () => {
|
timeoutId = setTimeout(
|
||||||
await this.updateMachineApiIp()
|
toSync(async () => {
|
||||||
await this.updateMachines()
|
await this.updateMachineApiIp()
|
||||||
timeoutLoop()
|
await this.updateMachines()
|
||||||
}, 10000)
|
timeoutLoop()
|
||||||
|
}, reportRejection),
|
||||||
|
10000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
timeoutLoop()
|
timeoutLoop()
|
||||||
}
|
}
|
||||||
|