Compare commits
139 Commits
kurt-does-
...
kurt-circl
| Author | SHA1 | Date | |
|---|---|---|---|
| c26709c06a | |||
| fa580d4035 | |||
| 93d3df4877 | |||
| 9fafc90c57 | |||
| 04e82bf728 | |||
| dab96577a7 | |||
| 0abe4c4082 | |||
| 266c601fd4 | |||
| 25443eba31 | |||
| 0a72d7a39a | |||
| 5f8d4f8294 | |||
| 28a63194c7 | |||
| a6aff874bb | |||
| 25080e9895 | |||
| 99ffc82ffa | |||
| 4219a2c31d | |||
| bb265ca833 | |||
| 63dea715bc | |||
| 15871e6245 | |||
| 7ab9b3ee46 | |||
| e794c5b0e9 | |||
| 7c2cfba0ac | |||
| a1dd884013 | |||
| 994f71bce5 | |||
| b55652f9b7 | |||
| 9d6a09766c | |||
| 840e75e5a1 | |||
| 466511ac46 | |||
| 85abcde086 | |||
| 48d347b4de | |||
| b3c4aec8db | |||
| a59de4efa3 | |||
| 57a460f57a | |||
| 59284ffa50 | |||
| 5592185371 | |||
| 998b194712 | |||
| 5ee43bda22 | |||
| 6753a9e9f8 | |||
| a1b6bbac7e | |||
| e61516f3c3 | |||
| abb8b95b88 | |||
| 94b0510521 | |||
| 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 | |||
| cbddb3553d | |||
| dd754c78ab | |||
| 150f56b47a | |||
| 72e522dd51 | |||
| 0eef6ab7d3 | |||
| 91d3ba3fce | |||
| 7165aa1b41 | |||
| 3cbda10eab | |||
| 0f3432b5a0 | |||
| f11dc07f0b | |||
| e49beb6609 | |||
| b8f27b77a8 | |||
| fa7e31223d | |||
| f04c4588df | |||
| c95812efa6 | |||
| 96385cd5ee | |||
| 64707edaad | |||
| 706af591c5 | |||
| 27baf135e7 | |||
| a4cf68c661 | |||
| 403e074249 | |||
| 50259aa052 | |||
| 1739f3dafe | |||
| 7ceb518446 | |||
| 36a6b8c0ea | |||
| bbdca7421e | |||
| 03c6f6d60e | |||
| 18c7e7934a | |||
| bf650fd129 | |||
| 81ccb65f15 | |||
| a1f8ac4548 | |||
| 0d5a8aea93 | |||
| 335b5100ae | |||
| 1162ff3b03 | |||
| 5e8227ead8 | |||
| 3e4316b614 | |||
| c6577a5ae6 | |||
| 379960561d | |||
| 8d912fa62a | |||
| bfd92a626b | |||
| 44e07ca6e5 | |||
| 22c854815a | |||
| ed339a6b9a | |||
| 1d19fc6b7e | |||
| 5b5355376f | |||
| 5c90f72c91 | |||
| 026a8d19cb | |||
| 3409aa5cfd | |||
| 6dd0981709 | |||
| b231a26115 | |||
| 3f47486fb5 | |||
| 57e97d16d0 | |||
| c56c446e15 | |||
| 1988888699 | |||
| acfa95f728 | |||
| b7c71c01cf |
@ -2,7 +2,9 @@ NODE_ENV=development
|
||||
DEV=true
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||
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_SKIP_AUTH=false
|
||||
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"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
@ -24,7 +26,6 @@
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
"jest/valid-expect": "off"
|
||||
|
||||
357
.github/workflows/build-test-publish-apps.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: build-test-publish-apps
|
||||
name: build-publish-apps
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -21,7 +21,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-json-files:
|
||||
prepare-files:
|
||||
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
@ -33,6 +33,19 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm"
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
@ -42,36 +55,48 @@ jobs:
|
||||
# TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: prepared-files
|
||||
path: |
|
||||
package.json
|
||||
src/wasm-lib/pkg/wasm_lib*
|
||||
|
||||
- id: export_version
|
||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
build-test-app-macos:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: macos-14
|
||||
build-apps:
|
||||
needs: [prepare-files]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-14, windows-2022, ubuntu-22.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
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 }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
name: prepared-files
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
- name: Copy prepared files
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
ls -R prepared-files
|
||||
cp prepared-files/package.json package.json
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
mkdir src/wasm-lib/pkg
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -81,79 +106,10 @@ jobs:
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
||||
|
||||
# TODO: sign the app (and updater bundle potentially)
|
||||
- name: Add signing certificate
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make"
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*/*"
|
||||
|
||||
|
||||
build-test-app-windows:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm manually
|
||||
shell: bash
|
||||
env:
|
||||
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
|
||||
run: |
|
||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||
echo "building with ${{ env.MODE }}"
|
||||
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
- run: yarn tronb:vite
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
@ -168,7 +124,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Setup certicate with SSM KSP (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
@ -178,83 +134,22 @@ jobs:
|
||||
smksp_cert_sync.exe
|
||||
shell: cmd
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
- name: Build the app
|
||||
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
|
||||
- name: Sign using Signtool
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
env:
|
||||
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
|
||||
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
|
||||
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
|
||||
run: |
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.X64_FILE }}"
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}"
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
|
||||
# TODO: Run e2e tests
|
||||
|
||||
|
||||
build-test-app-ubuntu:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
name: out-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
# TODO: sign the app (and updater bundle potentially)
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
# TODO: add the updater tests back
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -262,88 +157,76 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
|
||||
needs: [prepare-files, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||
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' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact/*/*oo*
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
|
||||
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
|
||||
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windows_x86_64_sig,
|
||||
"url": $windows_x86_64_url
|
||||
},
|
||||
"windows-aarch64": {
|
||||
"signature": $windows_aarch64_sig,
|
||||
"url": $windows_aarch64_url
|
||||
}
|
||||
}
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-windows-2022
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-ubuntu-22.04
|
||||
path: out
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
|
||||
--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 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.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,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
"dmg-arm64": {
|
||||
"url": $mac_arm64_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_x86_64_url
|
||||
"dmg-x64": {
|
||||
"url": $mac_x64_url
|
||||
},
|
||||
"msi-aarch64": {
|
||||
"url": $windows_aarch64_url
|
||||
"exe-arm64": {
|
||||
"url": $windows_arm64_url
|
||||
},
|
||||
"exe-x64": {
|
||||
"url": $windows_x64_url
|
||||
},
|
||||
"appimage-arm64": {
|
||||
"url": $linux_arm64_url
|
||||
},
|
||||
"appimage-x64": {
|
||||
"url": $linux_x64_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.5'
|
||||
with:
|
||||
@ -352,24 +235,44 @@ jobs:
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||
with:
|
||||
project_id: kittycadapi
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
path: out
|
||||
glob: 'Zoo*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
with:
|
||||
path: last_update.json
|
||||
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
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: out
|
||||
glob: 'latest*'
|
||||
parent: false
|
||||
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
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
@ -378,7 +281,9 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
files: 'out/Zoo*'
|
||||
|
||||
# TODO: Add GitHub publisher
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
|
||||
3
.github/workflows/cargo-clippy.yml
vendored
@ -28,6 +28,7 @@ jobs:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@just
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -41,7 +42,7 @@ jobs:
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
just lint
|
||||
# If this fails, run "cargo check" to update Cargo.lock,
|
||||
# then add Cargo.lock to the PR.
|
||||
- name: Check Cargo.lock doesn't need updating
|
||||
|
||||
20
.github/workflows/playwright.yml
vendored
@ -139,7 +139,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -174,14 +174,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run playwright/chrome flow (with retries)
|
||||
id: retry
|
||||
@ -244,14 +244,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -262,7 +262,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-14]
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
@ -351,7 +351,7 @@ jobs:
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run electron tests (with retries)
|
||||
id: retry
|
||||
@ -381,7 +381,7 @@ jobs:
|
||||
echo "retried=true" >>$GITHUB_OUTPUT
|
||||
echo "run playwright with last failed tests and retry $retry"
|
||||
if [[ "$IS_UBUNTU" == "true" ]]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
|
||||
else
|
||||
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
fi
|
||||
@ -423,14 +423,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: test-results-electron-${{ github.sha }}
|
||||
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-electron-${{ github.sha }}
|
||||
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
1
.gitignore
vendored
@ -59,6 +59,7 @@ src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
Mac_App_Distribution.provisionprofile
|
||||
|
||||
*.tsbuildinfo
|
||||
src/wasm-lib/pkg
|
||||
|
||||
venv
|
||||
.vite/
|
||||
|
||||
8
Makefile
@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
|
||||
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
|
||||
yarn start
|
||||
|
||||
# I'm sorry this is so specific to my setup you may as well ignore this.
|
||||
# This is so you don't have to deal with electron windows popping up constantly.
|
||||
# It should work for you other Linux users.
|
||||
lee-electron-test:
|
||||
Xephyr -br -ac -noreset -screen 1200x500 :2 &
|
||||
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
|
||||
killall Xephyr
|
||||
|
||||
$(XSTATE_TYPEGENS): $(TS_SRC)
|
||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||
|
||||
|
||||
20
README.md
@ -110,7 +110,6 @@ Which commands from setup are one off vs need to be run every time?
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
```bash
|
||||
yarn install
|
||||
yarn wasm-prep
|
||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||
```
|
||||
@ -352,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
|
||||
</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
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
|
||||
|
||||
KEY_CHAIN=build.keychain
|
||||
CERTIFICATE_P12=certificate.p12
|
||||
|
||||
# Recreate the certificate from the secure environment variable
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
|
||||
|
||||
#create a keychain
|
||||
security create-keychain -p actions $KEY_CHAIN
|
||||
|
||||
# Make the keychain the default so identities are found
|
||||
security default-keychain -s $KEY_CHAIN
|
||||
|
||||
# Unlock the keychain
|
||||
security unlock-keychain -p actions $KEY_CHAIN
|
||||
|
||||
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
|
||||
|
||||
# remove certs
|
||||
rm -fr *.p12
|
||||
@ -270,6 +270,26 @@ const extrusion = extrude(5, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -479,6 +499,26 @@ const extrusion = extrude(5, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -274,6 +274,26 @@ const extrusion = extrude(5, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -483,6 +503,26 @@ const extrusion = extrude(5, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -189,6 +189,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -398,6 +418,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -609,6 +649,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -818,6 +878,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -188,6 +188,26 @@ const extrusion = extrude(10, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -397,6 +417,26 @@ const extrusion = extrude(10, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -608,6 +648,26 @@ const extrusion = extrude(10, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -817,6 +877,26 @@ const extrusion = extrude(10, sketch001)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -190,6 +190,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -399,6 +419,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -610,6 +650,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -819,6 +879,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -282,6 +282,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -491,6 +511,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -702,6 +742,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -911,6 +971,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -187,6 +187,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -396,6 +416,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -607,6 +647,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -816,6 +876,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -187,6 +187,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -396,6 +416,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -607,6 +647,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -816,6 +876,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -200,6 +200,26 @@ const exampleSketch = startSketchOn('XZ')
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -409,6 +429,26 @@ const exampleSketch = startSketchOn('XZ')
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -620,6 +660,26 @@ const exampleSketch = startSketchOn('XZ')
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -829,6 +889,26 @@ const exampleSketch = startSketchOn('XZ')
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -193,6 +193,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -402,6 +422,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -613,6 +653,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -822,6 +882,26 @@ const example = extrude(10, exampleSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -415,6 +415,26 @@ const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
@ -819,6 +839,26 @@ const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
to: [number, number],
|
||||
type: "TangentialArc",
|
||||
} |
|
||||
{
|
||||
// arc's direction
|
||||
ccw: bool,
|
||||
// the arc's center
|
||||
center: [number, number],
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
// the arc's radius
|
||||
radius: number,
|
||||
// The tag of the path.
|
||||
tag: {
|
||||
digest: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number],
|
||||
end: number,
|
||||
start: number,
|
||||
value: string,
|
||||
},
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
type: "Circle",
|
||||
} |
|
||||
{
|
||||
// The from point.
|
||||
from: [number, number],
|
||||
|
||||
@ -56,6 +56,7 @@ layout: manual
|
||||
* [`line`](kcl/line)
|
||||
* [`lineTo`](kcl/lineTo)
|
||||
* [`ln`](kcl/ln)
|
||||
* [`loft`](kcl/loft)
|
||||
* [`log`](kcl/log)
|
||||
* [`log10`](kcl/log10)
|
||||
* [`log2`](kcl/log2)
|
||||
@ -63,6 +64,7 @@ layout: manual
|
||||
* [`max`](kcl/max)
|
||||
* [`min`](kcl/min)
|
||||
* [`mm`](kcl/mm)
|
||||
* [`offsetPlane`](kcl/offsetPlane)
|
||||
* [`patternCircular2d`](kcl/patternCircular2d)
|
||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||
@ -88,6 +90,7 @@ layout: manual
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
* [`tangentialArcTo`](kcl/tangentialArcTo)
|
||||
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
|
||||
* [`tau`](kcl/tau)
|
||||
* [`toDegrees`](kcl/toDegrees)
|
||||
* [`toRadians`](kcl/toRadians)
|
||||
|
||||
516
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
11652
docs/kcl/std.json
@ -37,8 +37,7 @@ const example = extrude(10, exampleSketch)
|
||||
offset: number,
|
||||
// Radius of the arc. Not to be confused with Raiders of the Lost Ark.
|
||||
radius: number,
|
||||
} |
|
||||
[number, number]
|
||||
}
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
|
||||
```js
|
||||
|
||||
863
docs/kcl/tangentialArcToRelative.md
Normal file
@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
}
|
||||
|
||||
// deselect line tool
|
||||
await page.getByTestId('line').click()
|
||||
const btnLine = page.getByTestId('line')
|
||||
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
|
||||
if (btnLineAriaPressed === 'true') {
|
||||
await btnLine.click()
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect
|
||||
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
// click between first two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
if (openPanes.includes('code')) {
|
||||
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
|
||||
).toBeLessThan(3)
|
||||
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
|
||||
}
|
||||
|
||||
// hold down shift
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click between the latest two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// selected two lines therefore there should be two cursors
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Length: open menu' }).click()
|
||||
|
||||
@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Extruded Triangle
|
||||
const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([-5, 10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(5, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -261,10 +271,7 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
// If they're open by default, we're not actually testing anything.
|
||||
@ -292,16 +299,7 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
await test.step('All panes opened before should be visible', async () => {
|
||||
|
||||
@ -43,12 +43,6 @@ test(
|
||||
// open the project
|
||||
await page.getByText(`bracket`).click()
|
||||
|
||||
// wait for the project to load
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
@ -56,6 +50,12 @@ test(
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
// Wait for the model to finish loading
|
||||
const modelStateIndicator = page.getByTestId(
|
||||
'model-state-indicator-execution-done'
|
||||
)
|
||||
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
|
||||
|
||||
const gltfOption = page.getByText('glTF')
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
@ -104,7 +104,7 @@ test(
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(477481)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
|
||||
@ -558,7 +558,7 @@ test.describe('Editor tests', () => {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(`const extrusion = startSketchOn('XY')
|
||||
|> circle([0, 0], dia/2, %)
|
||||
|> circle({ center: [0, 0], radius: dia/2 }, %)
|
||||
|> hole(squareHole(length, width, height), %)
|
||||
|> extrude(height, %)`)
|
||||
|
||||
|
||||
279
e2e/playwright/file-tree.spec.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('when using the file tree to', () => {
|
||||
const fromFile = 'main.kcl'
|
||||
const toFile = 'hello.kcl'
|
||||
|
||||
test(
|
||||
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
renameFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files', 'code'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
await renameFile(fromFile, toFile)
|
||||
await page.reload()
|
||||
|
||||
await test.step('Postcondition: editor has same content as before the rename', async () => {
|
||||
await editorTextMatches(kclCube)
|
||||
})
|
||||
|
||||
await test.step('Postcondition: opening and closing settings works', async () => {
|
||||
const settingsOpenButton = page.getByRole('link', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
const settingsCloseButton = page.getByTestId('settings-close-button')
|
||||
await settingsOpenButton.click()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`create many new untitled files they increment their names`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const { panesOpen, createAndSelectProject, createNewFile } =
|
||||
await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
await createNewFile('')
|
||||
|
||||
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: /Untitled[-]?[0-5]?/ })
|
||||
).toHaveCount(5)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'create a new file with the same name as an existing file cancels the operation',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
openKclCodePanel,
|
||||
openFilePanel,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
createNewFileAndSelect,
|
||||
renameFile,
|
||||
selectFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
await openKclCodePanel()
|
||||
await openFilePanel()
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
const kcl1 = 'main.kcl'
|
||||
const kcl2 = '2.kcl'
|
||||
|
||||
await createNewFileAndSelect(kcl2)
|
||||
const kclCylinder = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCylinder)
|
||||
|
||||
await renameFile(kcl2, kcl1)
|
||||
|
||||
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
|
||||
await selectFile(kcl1)
|
||||
await editorTextMatches(kclCube)
|
||||
})
|
||||
|
||||
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
|
||||
await selectFile(kcl2)
|
||||
await editorTextMatches(kclCylinder)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'deleting all files recreates a default main.kcl with no code',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
deleteFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files', 'code'])
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
// File the main.kcl with contents
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
const kcl1 = 'main.kcl'
|
||||
|
||||
await deleteFile(kcl1)
|
||||
|
||||
await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => {
|
||||
await editorTextMatches('')
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
page.on('console', console.log)
|
||||
await electronApp.context().addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
})
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
@ -173,10 +170,10 @@ test.describe('Can export from electron app', () => {
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
const exportLocations: Array<Paths> = []
|
||||
@ -207,7 +204,7 @@ test.describe('Can export from electron app', () => {
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(477481)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
@ -495,10 +492,6 @@ test(
|
||||
|
||||
await file.click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await expect(u.codeLocator).toContainText(
|
||||
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
|
||||
)
|
||||
@ -856,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
|
||||
await expect(async () => {
|
||||
await page.mouse.move(0, 0, { steps: 5 })
|
||||
@ -867,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
await page.mouse.click(pointOnModel.x, pointOnModel.y)
|
||||
// check user can interact with model by checking it turns yellow
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
|
||||
.toBeLessThan(10)
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
|
||||
.toBeLessThan(15)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
|
||||
await page.getByTestId('app-logo').click()
|
||||
@ -942,24 +935,15 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
@ -976,24 +960,15 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
@ -1744,7 +1719,7 @@ test.describe('Renaming in the file tree', () => {
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await page.waitForTimeout(60000)
|
||||
await page.waitForTimeout(2000)
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
|
||||
@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
const crypticErrorText = `ApiError`
|
||||
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 }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -358,6 +419,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await page.addInitScript(
|
||||
async ({ code }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
},
|
||||
{
|
||||
code: bracket,
|
||||
@ -393,20 +455,22 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await test.step('The second export is blocked', async () => {
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage.first()).toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).toBeVisible(),
|
||||
])
|
||||
})
|
||||
|
||||
await test.step('The first export still succeeds', async () => {
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible({
|
||||
timeout: 15_000,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -419,10 +483,12 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible(),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible(),
|
||||
])
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
|
||||
@ -149,14 +149,16 @@ test.describe('Sketch tests', () => {
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(700, 200)
|
||||
await expect(async () => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect.poll(u.normalisedEditorCode)
|
||||
.toBe(`const sketch001 = startSketchOn('XZ')
|
||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||
.toBe(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|
||||
`)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
})
|
||||
test('Can exit selection of face', async ({ page }) => {
|
||||
// Load the app with the code panes
|
||||
@ -344,6 +346,92 @@ test.describe('Sketch tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Can edit a circle center and radius by dragging its handles', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center: [4.61, -5.01], radius: 8 }, %)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
vantage: { x: 0, y: -1250, z: 580 },
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const startPX = [667, 325]
|
||||
|
||||
const dragPX = 40
|
||||
|
||||
await page
|
||||
.getByText('circle({ center: [4.61, -5.01], radius: 8 }, %)')
|
||||
.click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
let prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
|
||||
|
||||
await test.step('drag circle center handle', async () => {
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: startPX[0], y: startPX[1] },
|
||||
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX },
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
})
|
||||
|
||||
await test.step('drag circle radius handle', async () => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||
await page.waitForTimeout(100)
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
|
||||
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
|
||||
})
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
})
|
||||
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center: [7.26, -2.37], radius: 11.79 }, %)
|
||||
`)
|
||||
})
|
||||
test('Can edit a sketch that has been extruded in the same pipe', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@ -532,6 +532,64 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
test(
|
||||
'Draft circle should look right',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const startXPx = 600
|
||||
|
||||
// Equip the rectangle tool
|
||||
// await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
await page.getByTestId('circle-center').click()
|
||||
|
||||
// Draw the rectangle
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
|
||||
|
||||
// Ensure the draft rectangle looks the same as it usually does
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center: [14.44, -2.44], radius: 1 }, %)`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test.describe(
|
||||
'Client side scene scale should match engine scale',
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
@ -365,10 +365,10 @@ const box = startSketchOn('XY')
|
||||
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
|
||||
|
||||
startSketchOn(keychain, 'end')
|
||||
|> circle([
|
||||
|> circle({ center: [
|
||||
width / 2,
|
||||
height - (keychainHoleSize + 1.5)
|
||||
], keychainHoleSize, %)
|
||||
], radius: keychainHoleSize }, %)
|
||||
|> extrude(-thickness, %)`
|
||||
|
||||
export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `const thing = 1`
|
||||
|
||||
@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
type TestColor = [number, number, number]
|
||||
export const TEST_COLORS = {
|
||||
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
}
|
||||
return maxDiff
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||
new Promise(async (resolve) => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
;(async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// 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)
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
}, 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 (
|
||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||
@ -511,10 +516,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
|
||||
editorTextMatches: async (code: string) => {
|
||||
const editor = page.locator(editorSelector)
|
||||
const editorText = await editor.textContent()
|
||||
return expect(util.toNormalizedCode(editorText || '')).toBe(
|
||||
util.toNormalizedCode(code)
|
||||
)
|
||||
return expect(editor).toHaveText(code, { useInnerText: true })
|
||||
},
|
||||
|
||||
pasteCodeInEditor: async (code: string) => {
|
||||
@ -532,18 +534,74 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
createNewFile: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}`, async () => {
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
},
|
||||
|
||||
selectFile: async (name: string) => {
|
||||
return test?.step(`Select ${name}`, async () => {
|
||||
await page
|
||||
.getByTestId('file-pane-scroll-container')
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
await openFilePanel(page)
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
const newFile = page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
|
||||
await expect(newFile).toBeVisible()
|
||||
await newFile.click()
|
||||
})
|
||||
},
|
||||
|
||||
renameFile: async (fromName: string, toName: string) => {
|
||||
return test?.step(`Rename ${fromName} to ${toName}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: fromName })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-rename').click()
|
||||
await page.getByTestId('file-rename-field').fill(toName)
|
||||
await page.keyboard.press('Enter')
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: toName })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
deleteFile: async (name: string) => {
|
||||
return test?.step(`Delete ${name}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-delete').click()
|
||||
await page.getByTestId('delete-confirmation').click()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @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[]) => {
|
||||
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
||||
await page.addInitScript(
|
||||
@ -772,7 +830,6 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
|
||||
console.log('TEST_SETTINGS.projects', settings)
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
@ -812,10 +869,12 @@ export async function setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn,
|
||||
cleanProjectDir = true,
|
||||
appSettings,
|
||||
}: {
|
||||
testInfo: TestInfo
|
||||
folderSetupFn?: (projectDirName: string) => Promise<void>
|
||||
cleanProjectDir?: boolean
|
||||
appSettings?: Partial<SaveSettingsPayload>
|
||||
}) {
|
||||
// create or otherwise clear the folder
|
||||
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
||||
@ -849,15 +908,19 @@ export async function setupElectron({
|
||||
|
||||
if (cleanProjectDir) {
|
||||
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
||||
const settingsOverrides = TOML.stringify({
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
})
|
||||
const settingsOverrides = TOML.stringify(
|
||||
appSettings
|
||||
? { settings: appSettings }
|
||||
: {
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
||||
}
|
||||
|
||||
|
||||
@ -774,6 +774,80 @@ const part001 = startSketchOn('XZ')
|
||||
locator: '[data-overlay-toolbar-index="12"]',
|
||||
})
|
||||
})
|
||||
test('for segment [circle]', async ({ page }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('XZ')
|
||||
|> circle({ center: [1 + 0, 0], radius: 8 }, %)
|
||||
`
|
||||
)
|
||||
localStorage.setItem('disableAxis', 'true')
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page
|
||||
.getByText('circle({ center: [1 + 0, 0], radius: 8 }, %)')
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page)
|
||||
const clickConstrained = _clickConstrained(page)
|
||||
|
||||
const hoverPos = { x: 789, y: 114 } as const
|
||||
let ang = await u.getAngle('[data-overlay-index="0"]')
|
||||
console.log('angl', ang)
|
||||
console.log('circle center x')
|
||||
await clickConstrained({
|
||||
hoverPos,
|
||||
constraintType: 'xAbsolute',
|
||||
expectBeforeUnconstrained:
|
||||
'circle({ center: [1 + 0, 0], radius: 8 }, %)',
|
||||
expectAfterUnconstrained: 'circle({ center: [1, 0], radius: 8 }, %)',
|
||||
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
|
||||
ang: ang + 105,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="0"]',
|
||||
})
|
||||
console.log('circle center y')
|
||||
await clickUnconstrained({
|
||||
hoverPos,
|
||||
constraintType: 'yAbsolute',
|
||||
expectBeforeUnconstrained:
|
||||
'circle({ center: [xAbs001, 0], radius: 8 }, %)',
|
||||
expectAfterUnconstrained:
|
||||
'circle({ center: [xAbs001, yAbs001], radius: 8 }, %)',
|
||||
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
|
||||
ang: ang + 105,
|
||||
steps: 10,
|
||||
locator: '[data-overlay-toolbar-index="0"]',
|
||||
})
|
||||
console.log('circle radius')
|
||||
await clickUnconstrained({
|
||||
hoverPos,
|
||||
constraintType: 'radius',
|
||||
expectBeforeUnconstrained:
|
||||
'circle({ center: [xAbs001, 0], radius: 8 }, %)',
|
||||
expectAfterUnconstrained:
|
||||
'circle({ center: [xAbs001, 0], radius: radius001 }, %)',
|
||||
expectFinal: 'circle({ center: [xAbs001, 0], radius: 8 }, %)',
|
||||
ang: ang + 105,
|
||||
steps: 10,
|
||||
locator: '[data-overlay-toolbar-index="0"]',
|
||||
})
|
||||
})
|
||||
})
|
||||
test.describe('Testing deleting a segment', () => {
|
||||
const _deleteSegmentSequence =
|
||||
|
||||
@ -773,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
let noHoverColor: [number, number, number] = [82, 82, 82]
|
||||
let hoverColor: [number, number, number] = [116, 116, 116]
|
||||
let selectColor: [number, number, number] = [144, 148, 97]
|
||||
let noHoverColor: [number, number, number] = [92, 92, 92]
|
||||
let hoverColor: [number, number, number] = [127, 127, 127]
|
||||
let selectColor: [number, number, number] = [155, 155, 105]
|
||||
|
||||
const extrudeWall = { x: 670, y: 275 }
|
||||
const extrudeText = `line([170.36, -121.61], %, $seg01)`
|
||||
@ -787,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
||||
.toBeLessThan(5)
|
||||
.toBeLessThan(15)
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
||||
@ -798,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.mouse.click(extrudeWall.x, extrudeWall.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
// because of shading, color is not exact everywhere on the face
|
||||
noHoverColor = [104, 104, 104]
|
||||
hoverColor = [134, 134, 134]
|
||||
selectColor = [158, 162, 110]
|
||||
noHoverColor = [115, 115, 115]
|
||||
hoverColor = [145, 145, 145]
|
||||
selectColor = [168, 168, 120]
|
||||
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
|
||||
await page.mouse.move(cap.x, cap.y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||
removeAfterFirstParenthesis(capText)
|
||||
)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
||||
await page.mouse.click(cap.x, cap.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
})
|
||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||
page,
|
||||
|
||||
@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
|
||||
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 settingsCloseButton.click()
|
||||
@ -303,53 +303,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(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
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 {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
clickPane,
|
||||
createNewFileAndSelect,
|
||||
openKclCodePanel,
|
||||
openFilePanel,
|
||||
waitForPageLoad,
|
||||
selectFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen([])
|
||||
|
||||
await test.step('Precondition: No projects exist', async () => {
|
||||
await test.step('Precondition: Open to second project file', async () => {
|
||||
await expect(page.getByTestId('home-section')).toBeVisible()
|
||||
const projectLinksPre = page.getByTestId('project-link')
|
||||
await expect(projectLinksPre).toHaveCount(0)
|
||||
await page.getByText('project-000').click()
|
||||
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', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
@ -357,6 +413,9 @@ test.describe('Testing settings', () => {
|
||||
|
||||
await test.step('Open and close settings', async () => {
|
||||
await settingsOpenButton.click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
tearDown,
|
||||
setupElectron,
|
||||
createProjectAndRenameIt,
|
||||
} from './test-utils'
|
||||
import { getUtils, setup, tearDown, setupElectron } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -538,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
|
||||
// Ensure the final toast remains.
|
||||
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()
|
||||
|
||||
// Ensure you can copy the code for the final model.
|
||||
@ -694,41 +690,53 @@ test(
|
||||
'Text-to-CAD functionality',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
const projectName = 'project-000'
|
||||
const prompt = 'lego 2x4'
|
||||
const textToCadFileName = 'lego-2x4.kcl'
|
||||
|
||||
const { electronApp, page, dir } = await setupElectron({ testInfo })
|
||||
const fileExists = () =>
|
||||
fs.existsSync(join(dir, projectName, textToCadFileName))
|
||||
|
||||
const {
|
||||
createAndSelectProject,
|
||||
openFilePanel,
|
||||
openKclCodePanel,
|
||||
waitForPageLoad,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// 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
|
||||
await createProjectAndRenameIt({ name: 'test-000', page })
|
||||
await page.getByTestId('project-link').click()
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// Open the files pane
|
||||
const filesPaneButton = page.getByTestId('files-pane-button')
|
||||
await filesPaneButton.click()
|
||||
await waitForPageLoad()
|
||||
await openFilePanel()
|
||||
await openKclCodePanel()
|
||||
|
||||
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
|
||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
||||
await expect(file).toBeVisible({ timeout: 20_000 })
|
||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step(`Test file navigation`, async () => {
|
||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
||||
await file.click()
|
||||
const kclComment = page.getByText('Lego 2x4 Brick')
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await textToCadFileButton.click()
|
||||
// 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 () => {
|
||||
@ -741,6 +749,9 @@ test(
|
||||
`Successfully deleted file "lego-2x4.kcl"`
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
expect(fileExists()).toBeFalsy()
|
||||
// Confirm we've navigated back to the main.kcl file after deletion
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
|
||||
83
electron-builder.yml
Normal file
@ -0,0 +1,83 @@
|
||||
appId: dev.zoo.modeling-app
|
||||
|
||||
directories:
|
||||
output: out
|
||||
buildResources: assets
|
||||
|
||||
files:
|
||||
- .vite/**
|
||||
|
||||
mac:
|
||||
category: public.app-category.developer-tools
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
notarize:
|
||||
teamId: 92H8YB3B95
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
rank: Owner
|
||||
|
||||
win:
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: msi
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
signingHashAlgorithms:
|
||||
- sha256
|
||||
sign: "./sign-win.js"
|
||||
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
|
||||
icon: "assets/icon.ico"
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
|
||||
msi:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: true
|
||||
allowElevation: true
|
||||
installerIcon: "assets/icon.ico"
|
||||
include: "./installer.nsh"
|
||||
|
||||
linux:
|
||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||
target:
|
||||
- target: appImage
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
fileAssociations:
|
||||
- ext: kcl
|
||||
name: kcl
|
||||
mimeType: text/vnd.zoo.kcl
|
||||
description: Zoo KCL File
|
||||
role: Editor
|
||||
|
||||
publish:
|
||||
- provider: generic
|
||||
url: https://dl.zoo.dev/releases/modeling-app
|
||||
channel: latest
|
||||
8
installer.nsh
Normal file
@ -0,0 +1,8 @@
|
||||
!macro preInit
|
||||
SetRegView 64
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
SetRegView 32
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
!macroend
|
||||
2
interface.d.ts
vendored
@ -30,8 +30,6 @@ export interface IElectronAPI {
|
||||
join: typeof path.join
|
||||
sep: typeof path.sep
|
||||
rename: (prev: string, next: string) => typeof fs.rename
|
||||
setBaseUrl: (value: string) => void
|
||||
loadProjectAtStartup: () => Promise<ProjectState | null>
|
||||
packageJson: {
|
||||
name: string
|
||||
}
|
||||
|
||||
28
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.24.12",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -34,11 +34,12 @@
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@ -50,7 +51,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"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-modal": "^3.16.1",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
@ -63,7 +64,7 @@
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^4.38.2"
|
||||
"xstate": "^5.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -83,11 +84,10 @@
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "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-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && 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\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf 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",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||
@ -98,7 +98,9 @@
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -151,7 +153,6 @@
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
@ -162,10 +163,12 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^32.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"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",
|
||||
"happy-dom": "^14.3.10",
|
||||
"http-server": "^14.1.1",
|
||||
@ -173,7 +176,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.43",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"setimmediate": "^1.0.5",
|
||||
@ -186,7 +189,6 @@
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"wasm-pack": "^0.13.0",
|
||||
"ws": "^8.17.0",
|
||||
"yarn": "^1.22.22"
|
||||
|
||||
@ -72,6 +72,7 @@ export class LanguageServerClient {
|
||||
async initialize() {
|
||||
// Start the client in the background.
|
||||
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.client.start()
|
||||
|
||||
this.ready = true
|
||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
|
||||
}
|
||||
|
||||
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) => {
|
||||
let value = view.plugin(plugin)
|
||||
if (!value) return false
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
value.requestFormatting()
|
||||
return true
|
||||
},
|
||||
|
||||
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
|
||||
this.processLspNotification = options.processLspNotification
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.initialize({
|
||||
documentText: this.getDocText(),
|
||||
})
|
||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
|
||||
async initialize({ documentText }: { documentText: string }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
if (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()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
}
|
||||
|
||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.requestSemanticTokens()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
timeout: 120_000, // override the default 30s timeout
|
||||
testDir: './e2e/playwright',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Do not retry */
|
||||
retries: process.env.CI ? 0 : 0,
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 1 : 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
[process.env.CI ? 'dot' : 'list'],
|
||||
['json', { outputFile: './test-results/report.json' }],
|
||||
['html'],
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
actionTimeout: 15000,
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
})
|
||||
BIN
public/wheel-loop-dark.mp4
Normal file
BIN
public/wheel-loop.mp4
Normal file
38
sign-win.js
Normal file
@ -0,0 +1,38 @@
|
||||
// From https://github.com/OpenBuilds/OpenBuilds-CONTROL/blob/4800540ffaa517925fc2cff26670809efa341ffe/signWin.js
|
||||
const { execSync } = require('node:child_process')
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
if (!process.env.SM_API_KEY) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: SM_API_KEY ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.WINDOWS_CERTIFICATE_THUMBPRINT) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: FINGERPRINT ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!configuration.path) {
|
||||
throw new Error(
|
||||
`Signing using signWin.js script: failed: TARGET PATH NOT FOUND`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`smctl sign --fingerprint="${
|
||||
process.env.WINDOWS_CERTIFICATE_THUMBPRINT
|
||||
}" --input "${String(configuration.path)}"`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
}
|
||||
)
|
||||
console.log('Signing using signWin.js script: successful')
|
||||
} catch (error) {
|
||||
console.error('Signing using signWin.js script: failed:', error)
|
||||
}
|
||||
}
|
||||
14
src/App.tsx
@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
@ -80,7 +81,7 @@ export function App() {
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager.sendSceneCommand(message)
|
||||
engineCommandManager.sendSceneCommand(message).catch(reportRejection)
|
||||
}, 1000 / 15)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (state.matches('Sketch')) {
|
||||
@ -95,7 +96,7 @@ export function App() {
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (state.matches({ idle: 'showPlanes' })) return
|
||||
if (context.store?.buttonDownInStream !== undefined) return
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -119,6 +120,15 @@ export function App() {
|
||||
paneOpacity +
|
||||
(context.store?.buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
// Override the electron window draggable region behavior as well
|
||||
// when the button is down in the stream
|
||||
style={
|
||||
isDesktop() && context.store?.buttonDownInStream
|
||||
? ({
|
||||
'-webkit-app-region': 'no-drag',
|
||||
} as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
/>
|
||||
|
||||
@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
|
||||
import { coreDump } from 'lang/wasm'
|
||||
import { useMemo } from 'react'
|
||||
import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -69,19 +70,6 @@ const router = createRouter([
|
||||
path: PATHS.INDEX,
|
||||
loader: async () => {
|
||||
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
|
||||
? redirect(PATHS.HOME)
|
||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
@ -186,21 +174,23 @@ function CoreDump() {
|
||||
[]
|
||||
)
|
||||
useHotkeyWrapper(['mod + shift + .'], () => {
|
||||
toast.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
{
|
||||
loading: 'Starting 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,
|
||||
toast
|
||||
.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
{
|
||||
loading: 'Starting 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,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch(reportRejection)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
ToolbarItemResolved,
|
||||
ToolbarModeName,
|
||||
} from 'lib/toolbar'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -68,12 +70,12 @@ export function Toolbar({
|
||||
*/
|
||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||
() => ({
|
||||
modelingStateMatches: state.matches,
|
||||
modelingState: state,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state.matches, send, commandBarSend, sketchPathId]
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
)
|
||||
|
||||
/**
|
||||
@ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
return (
|
||||
<Tooltip
|
||||
inert={false}
|
||||
wrapperStyle={
|
||||
isDesktop()
|
||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
position="bottom"
|
||||
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"
|
||||
@ -337,6 +344,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
<li key={link.label} className="contents">
|
||||
<a
|
||||
href={link.url}
|
||||
onClick={openExternalBrowserIfDesktop(link.url)}
|
||||
target="_blank"
|
||||
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"
|
||||
|
||||
@ -22,11 +22,12 @@ import {
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { toSync, uuidv4 } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||
const FRAMES_TO_ANIMATE_IN = 30
|
||||
@ -100,6 +101,7 @@ export class CameraControls {
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
@ -127,6 +129,7 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(cmd)
|
||||
}, 1000 / 30)
|
||||
|
||||
@ -139,6 +142,7 @@ export class CameraControls {
|
||||
...convertThreeCamValuesToEngineCam(threeValues),
|
||||
},
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(cmd)
|
||||
}, 1000 / 15)
|
||||
|
||||
@ -151,6 +155,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveCmd &&
|
||||
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
||||
this.lastPerspectiveCmdTime = Date.now()
|
||||
}
|
||||
@ -218,6 +223,7 @@ export class CameraControls {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
}
|
||||
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||
@ -249,6 +255,7 @@ export class CameraControls {
|
||||
const doZoom = () => {
|
||||
if (this.zoomDataFromLastFrame !== undefined) {
|
||||
this.handleStart()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -266,6 +273,7 @@ export class CameraControls {
|
||||
|
||||
const doMove = () => {
|
||||
if (this.moveDataFromLastFrame !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -459,6 +467,7 @@ export class CameraControls {
|
||||
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
this.camera.updateProjectionMatrix()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -929,6 +938,7 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
if (isReducedMotion()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
onComplete()
|
||||
return
|
||||
}
|
||||
@ -937,7 +947,7 @@ export class CameraControls {
|
||||
.to({ t: tweenEnd }, duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onUpdate(({ t }) => cameraAtTime(t))
|
||||
.onComplete(onComplete)
|
||||
.onComplete(toSync(onComplete, reportRejection))
|
||||
.start()
|
||||
})
|
||||
}
|
||||
@ -962,6 +972,7 @@ export class CameraControls {
|
||||
// Decrease the FOV
|
||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||
this.camera.updateProjectionMatrix()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else if (frameWaitOnFinish > 0) {
|
||||
@ -991,6 +1002,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov = 4
|
||||
let currentFov = 4
|
||||
const initialCameraUp = this.camera.up.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
const tempVec = new Vector3()
|
||||
|
||||
@ -999,6 +1011,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
||||
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
||||
this.camera.up.copy(currentUp)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.dollyZoom(currentFov)
|
||||
}
|
||||
|
||||
@ -1027,6 +1040,7 @@ export class CameraControls {
|
||||
this.lastPerspectiveFov = 4
|
||||
let currentFov = 4
|
||||
const initialCameraUp = this.camera.up.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.usePerspectiveCamera()
|
||||
const tempVec = new Vector3()
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
import { throttle, toSync } from 'lib/utils'
|
||||
import {
|
||||
sceneInfra,
|
||||
kclManager,
|
||||
@ -34,17 +34,15 @@ import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import { ConstrainInfo } from 'lang/std/stdTypes'
|
||||
import { getConstraintInfo } from 'lang/std/sketch'
|
||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import toast from 'react-hot-toast'
|
||||
import { InstanceProps, create } from 'react-modal-promise'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import {
|
||||
deleteSegmentFromPipeExpression,
|
||||
makeRemoveSingleConstraintInput,
|
||||
removeSingleConstraintInfo,
|
||||
} from 'lang/modifyAst'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
@ -124,9 +122,10 @@ export const ClientSideScene = ({
|
||||
} else if (context.mouseState.type === 'isDragging') {
|
||||
cursor = 'grabbing'
|
||||
} else if (
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' }) ||
|
||||
state.matches({ Sketch: 'Circle tool' })
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -214,9 +213,9 @@ const Overlay = ({
|
||||
overlay.visible &&
|
||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||
!(
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
state.matches({ Sketch: 'Line tool' }) ||
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' })
|
||||
)
|
||||
|
||||
return (
|
||||
@ -514,6 +513,11 @@ const ConstraintSymbol = ({
|
||||
displayName: 'Intersection Offset',
|
||||
iconName: 'intersection-offset',
|
||||
},
|
||||
radius: {
|
||||
varName: 'radius',
|
||||
displayName: 'Radius',
|
||||
iconName: 'dimension',
|
||||
},
|
||||
|
||||
// implicit constraints
|
||||
vertical: {
|
||||
@ -542,12 +546,10 @@ const ConstraintSymbol = ({
|
||||
iconName: 'dimension',
|
||||
},
|
||||
}
|
||||
const varName =
|
||||
_type in varNameMap ? varNameMap[_type as LineInputsType].varName : 'var'
|
||||
const name: CustomIconName = varNameMap[_type as LineInputsType].iconName
|
||||
const displayName = varNameMap[_type as LineInputsType]?.displayName
|
||||
const implicitDesc =
|
||||
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
|
||||
const varName = _type in varNameMap ? varNameMap[_type].varName : 'var'
|
||||
const name: CustomIconName = varNameMap[_type].iconName
|
||||
const displayName = varNameMap[_type]?.displayName
|
||||
const implicitDesc = varNameMap[_type]?.implicitConstraintDesc
|
||||
|
||||
const _node = useMemo(
|
||||
() => getNodeFromPath<Expr>(kclManager.ast, pathToNode),
|
||||
@ -582,7 +584,7 @@ const ConstraintSymbol = ({
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
@ -604,25 +606,23 @@ const ConstraintSymbol = ({
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const shallowPath = _node1.shallowPath
|
||||
|
||||
const input = makeRemoveSingleConstraintInput(
|
||||
argPosition,
|
||||
shallowPath
|
||||
)
|
||||
if (!input || !context.sketchDetails) return
|
||||
if (!context.sketchDetails || !argPosition) return
|
||||
const transform = removeSingleConstraintInfo(
|
||||
input,
|
||||
shallowPath,
|
||||
argPosition,
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
if (!transform) return
|
||||
const { modifiedAst } = transform
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
toast.success('Constraint removed')
|
||||
}
|
||||
}}
|
||||
}, reportRejection)}
|
||||
>
|
||||
<CustomIcon name={name} />
|
||||
</button>
|
||||
@ -688,7 +688,7 @@ const ConstraintSymbol = ({
|
||||
|
||||
const throttled = throttle((a: ReactCameraProperties) => {
|
||||
if (a.type === 'perspective' && a.fov) {
|
||||
sceneInfra.camControls.dollyZoom(a.fov)
|
||||
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
|
||||
}
|
||||
}, 1000 / 15)
|
||||
|
||||
@ -718,6 +718,7 @@ export const CamDebugSettings = () => {
|
||||
if (camSettings.type === 'perspective') {
|
||||
sceneInfra.camControls.useOrthographicCamera()
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sceneInfra.camControls.usePerspectiveCamera(true)
|
||||
}
|
||||
}}
|
||||
@ -725,7 +726,7 @@ export const CamDebugSettings = () => {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset Camera Position
|
||||
|
||||
@ -62,9 +62,10 @@ import {
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import {
|
||||
circleSegment,
|
||||
createArcGeometry,
|
||||
dashedStraight,
|
||||
profileStart,
|
||||
createProfileStartHandle,
|
||||
straightSegment,
|
||||
tangentialArcToSegment,
|
||||
} from './segments'
|
||||
@ -72,6 +73,7 @@ import {
|
||||
addCallExpressionsToPipe,
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
changeCircleArguments,
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
@ -87,6 +89,7 @@ import {
|
||||
createArrayExpression,
|
||||
createCallExpressionStdLib,
|
||||
createLiteral,
|
||||
createObjectExpression,
|
||||
createPipeExpression,
|
||||
createPipeSubstitution,
|
||||
findUniqueName,
|
||||
@ -103,7 +106,7 @@ import {
|
||||
updateRectangleSketch,
|
||||
} from 'lib/rectangleTool'
|
||||
import { getThemeColorForThreeJs } 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 { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||
|
||||
@ -119,9 +122,22 @@ export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const CIRCLE_SEGMENT = 'circle-segment'
|
||||
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
|
||||
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
|
||||
export const CIRCLE_CENTER_HANDLE = 'circle-center-handle'
|
||||
export const SEGMENT_WIDTH_PX = 1.6
|
||||
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
|
||||
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
|
||||
export const SEGMENT_BODIES = [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
CIRCLE_SEGMENT,
|
||||
]
|
||||
export const SEGMENT_BODIES_PLUS_PROFILE_START = [
|
||||
...SEGMENT_BODIES,
|
||||
PROFILE_START,
|
||||
]
|
||||
|
||||
type Vec3Array = [number, number, number]
|
||||
|
||||
@ -186,6 +202,26 @@ export class SceneEntities {
|
||||
})
|
||||
)
|
||||
}
|
||||
if (
|
||||
segment.userData.from &&
|
||||
segment.userData.to &&
|
||||
segment.userData.center &&
|
||||
segment.userData.radius &&
|
||||
segment.userData.type === CIRCLE_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateCircleSegment({
|
||||
prevSegment: segment.userData.prevSegment,
|
||||
from: segment.userData.from,
|
||||
to: segment.userData.to,
|
||||
center: segment.userData.center,
|
||||
radius: segment.userData.radius,
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
}
|
||||
@ -324,6 +360,7 @@ export class SceneEntities {
|
||||
)
|
||||
}
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -421,19 +458,21 @@ export class SceneEntities {
|
||||
maybeModdedAst,
|
||||
sketchGroup.start.__geoMeta.sourceRange
|
||||
)
|
||||
const _profileStart = profileStart({
|
||||
from: sketchGroup.start.from,
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
if (sketchGroup?.value?.[0].type !== 'Circle') {
|
||||
const _profileStart = createProfileStartHandle({
|
||||
from: sketchGroup.start.from,
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
}
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
@ -498,6 +537,32 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else if (segment.type === 'Circle') {
|
||||
seg = circleSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
id: segment.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
isSelected,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateCircleSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
from: segment.from,
|
||||
@ -602,16 +667,18 @@ export class SceneEntities {
|
||||
kclManager.programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sg)) return sg
|
||||
const lastSeg = sg.value?.slice(-1)[0] || sg.start
|
||||
if (err(sg)) return Promise.reject(sg)
|
||||
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
|
||||
|
||||
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
||||
|
||||
const mod = addNewSketchLn({
|
||||
node: _ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [lastSeg.to[0], lastSeg.to[1]],
|
||||
from: [lastSeg.to[0], lastSeg.to[1]],
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
to: lastSeg.to,
|
||||
from: lastSeg.to,
|
||||
},
|
||||
fnName: segmentName,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
@ -634,6 +701,7 @@ export class SceneEntities {
|
||||
draftExpressionsIndices,
|
||||
})
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -681,8 +749,11 @@ export class SceneEntities {
|
||||
const tmp = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [intersection2d.x, intersection2d.y],
|
||||
from: [lastSegment.to[0], lastSegment.to[1]],
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
to: [intersection2d.x, intersection2d.y],
|
||||
from: lastSegment.to,
|
||||
},
|
||||
fnName:
|
||||
lastSegment.type === 'TangentialArcTo'
|
||||
? 'tangentialArcTo'
|
||||
@ -701,7 +772,7 @@ export class SceneEntities {
|
||||
if (profileStart) {
|
||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||
} else {
|
||||
this.setUpDraftSegment(
|
||||
await this.setUpDraftSegment(
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
@ -746,6 +817,11 @@ export class SceneEntities {
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const sg = sketchGroupFromKclValue(
|
||||
kclManager.programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sg)) return sg
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
findUniqueName(_ast, 'rectangleSegmentB'),
|
||||
@ -771,6 +847,7 @@ export class SceneEntities {
|
||||
})
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onMove: async (args) => {
|
||||
// Update the width and height of the draft rectangle
|
||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||
@ -802,7 +879,7 @@ export class SceneEntities {
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketchGroup)) return sketchGroup
|
||||
if (err(sketchGroup)) return Promise.reject(sketchGroup)
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -818,6 +895,7 @@ export class SceneEntities {
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onClick: async (args) => {
|
||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||
const cornerPoint = args.intersectionPoint?.twoD
|
||||
@ -858,7 +936,7 @@ export class SceneEntities {
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketchGroup)) return sketchGroup
|
||||
if (err(sketchGroup)) return Promise.reject(sketchGroup)
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -879,6 +957,151 @@ export class SceneEntities {
|
||||
},
|
||||
})
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
up: [number, number, number],
|
||||
sketchOrigin: [number, number, number],
|
||||
circleCenter: [x: number, y: number]
|
||||
) => {
|
||||
let _ast = structuredClone(kclManager.ast)
|
||||
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
createCallExpressionStdLib('circle', [
|
||||
createObjectExpression({
|
||||
center: createArrayExpression([
|
||||
createLiteral(roundOff(circleCenter[0])),
|
||||
createLiteral(roundOff(circleCenter[1])),
|
||||
]),
|
||||
radius: createLiteral(1),
|
||||
}),
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
])
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
// do a quick mock execution to get the program memory up-to-date
|
||||
await kclManager.executeAstMock(_ast)
|
||||
|
||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
position: sketchOrigin,
|
||||
maybeModdedAst: _ast,
|
||||
draftExpressionsIndices: { start: 0, end: 0 },
|
||||
})
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
onMove: async (args) => {
|
||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||
pathToNodeTwo[1][0] = 0
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
truncatedAst,
|
||||
pathToNodeTwo || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
let modded = structuredClone(truncatedAst)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
|
||||
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
const moddedResult = changeCircleArguments(
|
||||
modded,
|
||||
kclManager.programMemory,
|
||||
[..._node.deepPath, ['body', 'PipeExpression'], [1, 'index']],
|
||||
circleCenter,
|
||||
Math.sqrt(x ** 2 + y ** 2)
|
||||
)
|
||||
if (err(moddedResult)) return Promise.reject(moddedResult)
|
||||
modded = moddedResult.modifiedAst
|
||||
}
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
ast: modded,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = sketchGroupFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketchGroup)) return sketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
this.updateSegment(
|
||||
sketchGroup.start,
|
||||
0,
|
||||
0,
|
||||
_ast,
|
||||
orthoFactor,
|
||||
sketchGroup
|
||||
)
|
||||
sgPaths.forEach((seg, index) =>
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||
)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||
const cornerPoint = args.intersectionPoint?.twoD
|
||||
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
||||
|
||||
const x = roundOff((cornerPoint.x || 0) - circleCenter[0])
|
||||
const y = roundOff((cornerPoint.y || 0) - circleCenter[1])
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
let modded = structuredClone(_ast)
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
const moddedResult = changeCircleArguments(
|
||||
modded,
|
||||
kclManager.programMemory,
|
||||
[..._node.deepPath, ['body', 'PipeExpression'], [1, 'index']],
|
||||
circleCenter,
|
||||
Math.sqrt(x ** 2 + y ** 2)
|
||||
)
|
||||
if (err(moddedResult)) return Promise.reject(moddedResult)
|
||||
modded = moddedResult.modifiedAst
|
||||
|
||||
let _recastAst = parse(recast(modded))
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish circle' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
setupSketchIdleCallbacks = ({
|
||||
pathToNode,
|
||||
up,
|
||||
@ -892,9 +1115,11 @@ export class SceneEntities {
|
||||
}) => {
|
||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||
sceneInfra.setCallbacks({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onDragEnd: async () => {
|
||||
if (addingNewSegmentStatus !== 'nothing') {
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
@ -911,6 +1136,7 @@ export class SceneEntities {
|
||||
})
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onDrag: async ({
|
||||
selected,
|
||||
intersectionPoint,
|
||||
@ -944,8 +1170,11 @@ export class SceneEntities {
|
||||
const mod = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
|
||||
from: [prevSegment.from[0], prevSegment.from[1]],
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
|
||||
from: prevSegment.from,
|
||||
},
|
||||
// TODO assuming it's always a straight segments being added
|
||||
// as this is easiest, and we'll need to add "tabbing" behavior
|
||||
// to support other segment types
|
||||
@ -958,6 +1187,7 @@ export class SceneEntities {
|
||||
|
||||
await kclManager.executeAstMock(mod.modifiedAst)
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
@ -1043,11 +1273,8 @@ export class SceneEntities {
|
||||
? new Vector2(profileStart.position.x, profileStart.position.y)
|
||||
: _intersection2d
|
||||
|
||||
const group = getParentGroup(object, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
||||
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
|
||||
if (!group) return
|
||||
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
|
||||
const varDecIndex = pathToNode[1][0]
|
||||
@ -1065,7 +1292,7 @@ export class SceneEntities {
|
||||
group.userData.from[0],
|
||||
group.userData.from[1],
|
||||
]
|
||||
const to: [number, number] = [intersection2d.x, intersection2d.y]
|
||||
const dragTo: [number, number] = [intersection2d.x, intersection2d.y]
|
||||
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
||||
|
||||
const _node = getNodeFromPath<CallExpression>(
|
||||
@ -1088,17 +1315,59 @@ export class SceneEntities {
|
||||
modded = updateStartProfileAtArgs({
|
||||
node: modifiedAst,
|
||||
pathToNode,
|
||||
to,
|
||||
from,
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
to: dragTo,
|
||||
from,
|
||||
},
|
||||
previousProgramMemory: kclManager.programMemory,
|
||||
})
|
||||
} else if (
|
||||
group.name === CIRCLE_SEGMENT &&
|
||||
// !subGroup treats grabbing the outer circumference of the circle
|
||||
// as a drag of the center handle
|
||||
(!subGroup || subGroup?.name === ARROWHEAD)
|
||||
) {
|
||||
// is dragging the radius handle
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
{
|
||||
type: 'arc-segment',
|
||||
from,
|
||||
center: group.userData.center,
|
||||
radius: Math.sqrt(
|
||||
(group.userData.center[0] - dragTo[0]) ** 2 +
|
||||
(group.userData.center[0] - dragTo[0]) ** 2
|
||||
),
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
group.name === CIRCLE_SEGMENT &&
|
||||
subGroup?.name === CIRCLE_CENTER_HANDLE
|
||||
) {
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
{
|
||||
type: 'arc-segment',
|
||||
from,
|
||||
center: dragTo,
|
||||
radius: group.userData.radius,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
from
|
||||
{
|
||||
type: 'straight-segment',
|
||||
from,
|
||||
to: dragTo,
|
||||
}
|
||||
)
|
||||
}
|
||||
if (trap(modded)) return
|
||||
@ -1161,7 +1430,7 @@ export class SceneEntities {
|
||||
)
|
||||
)
|
||||
sceneInfra.overlayCallbacks(callBacks)
|
||||
})()
|
||||
})().catch(reportRejection)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1216,6 +1485,20 @@ export class SceneEntities {
|
||||
group,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (
|
||||
type === CIRCLE_SEGMENT &&
|
||||
'center' in segment &&
|
||||
'radius' in segment
|
||||
) {
|
||||
return this.updateCircleSegment({
|
||||
prevSegment: sgPaths[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
group,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (type === PROFILE_START) {
|
||||
group.position.set(segment.from[0], segment.from[1], 0)
|
||||
group.scale.set(factor, factor, factor)
|
||||
@ -1241,6 +1524,9 @@ export class SceneEntities {
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (!prevSegment) {
|
||||
console.trace('prevSegment is undefined')
|
||||
}
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
@ -1336,6 +1622,111 @@ export class SceneEntities {
|
||||
angle,
|
||||
})
|
||||
}
|
||||
updateCircleSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
to,
|
||||
center,
|
||||
radius,
|
||||
group,
|
||||
scale = 1,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
center: [number, number]
|
||||
radius: number
|
||||
group: Group
|
||||
scale?: number
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const circleCenterHandle = group.getObjectByName(
|
||||
CIRCLE_CENTER_HANDLE
|
||||
) as Group
|
||||
|
||||
const pxLength = (2 * radius * Math.PI) / scale
|
||||
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||
|
||||
const hoveredParent =
|
||||
sceneInfra.hoveredObject &&
|
||||
getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT])
|
||||
let isHandlesVisible = !shouldHideIdle
|
||||
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(
|
||||
center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
0
|
||||
)
|
||||
|
||||
const arrowheadAngle = Math.PI / 4
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
arrowGroup.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (circleCenterHandle) {
|
||||
circleCenterHandle.position.set(center[0], center[1], 0)
|
||||
circleCenterHandle.scale.set(scale, scale, scale)
|
||||
circleCenterHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const circleSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (circleSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
scale,
|
||||
})
|
||||
circleSegmentBody.geometry = newGeo
|
||||
}
|
||||
const circleSegmentBodyDashed = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_SEGMENT_DASH
|
||||
) as Mesh
|
||||
if (circleSegmentBodyDashed) {
|
||||
// consider throttling the whole updateTangentialArcToSegment
|
||||
// if there are more perf considerations going forward
|
||||
this.throttledUpdateDashedArcGeo({
|
||||
// ...arcInfo,
|
||||
center,
|
||||
radius,
|
||||
ccw: true,
|
||||
// make the start end where the handle is
|
||||
startAngle: Math.PI * 0.25,
|
||||
endAngle: Math.PI * 2.25,
|
||||
mesh: circleSegmentBodyDashed,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from: to,
|
||||
to: [center[0], center[1]],
|
||||
angle: Math.PI / 4,
|
||||
})
|
||||
}
|
||||
throttledUpdateDashedArcGeo = throttle(
|
||||
(
|
||||
args: Parameters<typeof createArcGeometry>[0] & {
|
||||
@ -1470,7 +1861,7 @@ export class SceneEntities {
|
||||
}
|
||||
private _tearDownSketch(
|
||||
callDepth = 0,
|
||||
resolve: (val: unknown) => void,
|
||||
resolve: any,
|
||||
reject: () => void,
|
||||
{ removeAxis = true }: { removeAxis?: boolean }
|
||||
) {
|
||||
@ -1500,7 +1891,7 @@ export class SceneEntities {
|
||||
this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis })
|
||||
}, delay)
|
||||
} else {
|
||||
reject()
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
sceneInfra.camControls.enableRotate = true
|
||||
@ -1512,7 +1903,7 @@ export class SceneEntities {
|
||||
removeAxis = true,
|
||||
}: {
|
||||
removeAxis?: boolean
|
||||
} = {}) {
|
||||
} = {}): Promise<void | Error> {
|
||||
// I think promisifying this is mostly a side effect of not having
|
||||
// "setupSketch" correctly capture a promise when it's done
|
||||
// so we're effectively waiting for to be finished setting up the scene just to tear it down
|
||||
@ -1530,11 +1921,10 @@ export class SceneEntities {
|
||||
mat.color.set(obj.userData.baseColor)
|
||||
mat.color.offsetHSL(0, 0, 0.5)
|
||||
}
|
||||
const parent = getParentGroup(selected, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
const parent = getParentGroup(
|
||||
selected,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START
|
||||
)
|
||||
if (parent?.userData?.pathToNode) {
|
||||
const updatedAst = parse(recast(kclManager.ast))
|
||||
if (trap(updatedAst)) return
|
||||
@ -1578,6 +1968,16 @@ export class SceneEntities {
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (parent.name === CIRCLE_SEGMENT) {
|
||||
this.updateCircleSegment({
|
||||
prevSegment: parent.userData.prevSegment,
|
||||
from: parent.userData.from,
|
||||
to: parent.userData.to,
|
||||
center: parent.userData.center,
|
||||
radius: parent.userData.radius,
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1585,11 +1985,10 @@ export class SceneEntities {
|
||||
},
|
||||
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
const parent = getParentGroup(selected, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
const parent = getParentGroup(
|
||||
selected,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START
|
||||
)
|
||||
if (parent) {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -1613,6 +2012,16 @@ export class SceneEntities {
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (parent.name === CIRCLE_SEGMENT) {
|
||||
this.updateCircleSegment({
|
||||
prevSegment: parent.userData.prevSegment,
|
||||
from: parent.userData.from,
|
||||
to: parent.userData.to,
|
||||
center: parent.userData.center,
|
||||
radius: parent.userData.radius,
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
}
|
||||
const isSelected = parent?.userData?.isSelected
|
||||
@ -1775,7 +2184,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
|
||||
export function getParentGroup(
|
||||
object: any,
|
||||
stopAt: string[] = [STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT]
|
||||
stopAt: string[] = SEGMENT_BODIES
|
||||
): Group | null {
|
||||
if (stopAt.includes(object?.userData?.type)) {
|
||||
return object
|
||||
@ -1822,10 +2231,7 @@ function colorSegment(object: any, color: number) {
|
||||
})
|
||||
return
|
||||
}
|
||||
const straightSegmentBody = getParentGroup(object, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
const straightSegmentBody = getParentGroup(object, SEGMENT_BODIES)
|
||||
if (straightSegmentBody) {
|
||||
straightSegmentBody.traverse((child) => {
|
||||
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
|
||||
|
||||
@ -111,21 +111,21 @@ export class SceneInfra {
|
||||
}
|
||||
extraSegmentTexture: Texture
|
||||
lastMouseState: MouseState = { type: 'idle' }
|
||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
||||
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
|
||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
onDragStartCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||
onDragEndCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||
onMoveCallback: (arg: OnMoveCallbackArgs) => any = () => {}
|
||||
onClickCallback: (arg: OnClickCallbackArgs) => any = () => {}
|
||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => any = () => {}
|
||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => any = () => {}
|
||||
setCallbacks = (callbacks: {
|
||||
onDragStart?: (arg: OnDragCallbackArgs) => void
|
||||
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||
onMove?: (arg: OnMoveCallbackArgs) => void
|
||||
onClick?: (arg: OnClickCallbackArgs) => void
|
||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
onDragStart?: (arg: OnDragCallbackArgs) => any
|
||||
onDragEnd?: (arg: OnDragCallbackArgs) => any
|
||||
onDrag?: (arg: OnDragCallbackArgs) => any
|
||||
onMove?: (arg: OnMoveCallbackArgs) => any
|
||||
onClick?: (arg: OnClickCallbackArgs) => any
|
||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => any
|
||||
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => any
|
||||
}) => {
|
||||
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
||||
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
||||
|
||||
@ -24,6 +24,10 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||
import {
|
||||
CIRCLE_CENTER_HANDLE,
|
||||
CIRCLE_SEGMENT,
|
||||
CIRCLE_SEGMENT_BODY,
|
||||
CIRCLE_SEGMENT_DASH,
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
EXTRA_SEGMENT_OFFSET_PX,
|
||||
HIDE_SEGMENT_LENGTH,
|
||||
@ -46,7 +50,7 @@ import {
|
||||
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { roundOff } from 'lib/utils'
|
||||
|
||||
export function profileStart({
|
||||
export function createProfileStartHandle({
|
||||
from,
|
||||
id,
|
||||
pathToNode,
|
||||
@ -225,6 +229,28 @@ function createArrowhead(scale = 1, theme: Themes, color?: number): Group {
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
return arrowGroup
|
||||
}
|
||||
function createCircleCenterHandle(
|
||||
scale = 1,
|
||||
theme: Themes,
|
||||
color?: number
|
||||
): Group {
|
||||
const circleCenterGroup = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
circleCenterGroup.add(mesh)
|
||||
|
||||
circleCenterGroup.userData = {
|
||||
type: CIRCLE_CENTER_HANDLE,
|
||||
baseColor,
|
||||
}
|
||||
circleCenterGroup.name = CIRCLE_CENTER_HANDLE
|
||||
circleCenterGroup.scale.set(scale, scale, scale)
|
||||
return circleCenterGroup
|
||||
}
|
||||
|
||||
function createExtraSegmentHandle(
|
||||
scale: number,
|
||||
@ -300,6 +326,86 @@ function createLengthIndicator({
|
||||
return lengthIndicatorGroup
|
||||
}
|
||||
|
||||
export function circleSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
to,
|
||||
center,
|
||||
radius,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
texture,
|
||||
theme,
|
||||
isSelected,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: Coords2d
|
||||
center: Coords2d
|
||||
radius: number
|
||||
to: Coords2d
|
||||
id: string
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
texture: Texture
|
||||
theme: Themes
|
||||
isSelected?: boolean
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
mesh.userData.type = isDraftSegment
|
||||
? CIRCLE_SEGMENT_DASH
|
||||
: CIRCLE_SEGMENT_BODY
|
||||
|
||||
group.userData = {
|
||||
type: CIRCLE_SEGMENT,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
radius,
|
||||
center,
|
||||
ccw: true,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = CIRCLE_SEGMENT
|
||||
|
||||
const arrowGroup = createArrowhead(scale, theme, color)
|
||||
arrowGroup.position.set(
|
||||
center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
0
|
||||
)
|
||||
|
||||
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
|
||||
circleCenterGroup.position.set(center[0], center[1], 0)
|
||||
const arrowheadAngle = Math.PI / 4
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
|
||||
group.add(mesh, arrowGroup, circleCenterGroup)
|
||||
return group
|
||||
}
|
||||
export function tangentialArcToSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
|
||||
@ -42,7 +42,13 @@ export type ActionButtonProps =
|
||||
|
||||
export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
||||
const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
props.iconStart ? (props.iconEnd ? 'px-0' : 'pr-2') : 'px-2'
|
||||
props.iconStart
|
||||
? props.iconEnd
|
||||
? 'px-0'
|
||||
: 'pr-2'
|
||||
: props.iconEnd
|
||||
? 'px-2'
|
||||
: 'pl-2'
|
||||
} ${props.className ? props.className : ''}`
|
||||
|
||||
switch (props.Element) {
|
||||
|
||||
@ -35,7 +35,7 @@ export const ActionIcon = ({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`w-fit inline-grid place-content-center ${className} ` +
|
||||
`w-fit self-stretch inline-grid place-content-center ${className} ` +
|
||||
computedBgClassName
|
||||
}
|
||||
>
|
||||
|
||||
@ -4,21 +4,14 @@
|
||||
*/
|
||||
.header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
-webkit-app-region: drag; /* Make the header of the app draggable */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.header button {
|
||||
-webkit-app-region: no-drag; /* Make the button not draggable */
|
||||
}
|
||||
|
||||
.header a {
|
||||
-webkit-app-region: no-drag; /* Make the link not draggable */
|
||||
}
|
||||
|
||||
.header textarea {
|
||||
-webkit-app-region: no-drag; /* Make the textarea not draggable */
|
||||
}
|
||||
|
||||
.header input {
|
||||
-webkit-app-region: no-drag; /* Make the input not draggable */
|
||||
.header.desktopApp {
|
||||
/* 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
|
||||
* all interactive elements opt-out of this behavior by default in src/index.css
|
||||
*/
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { RefreshButton } from 'components/RefreshButton'
|
||||
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
project?: Omit<IndexLoaderData, 'code'>
|
||||
className?: string
|
||||
enableMenu?: boolean
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const AppHeader = ({
|
||||
@ -19,6 +21,7 @@ export const AppHeader = ({
|
||||
project,
|
||||
children,
|
||||
className = '',
|
||||
style,
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
@ -30,9 +33,12 @@ export const AppHeader = ({
|
||||
className={
|
||||
'w-full grid ' +
|
||||
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
|
||||
}
|
||||
style={style}
|
||||
>
|
||||
<ProjectSidebarMenu
|
||||
enableMenu={enableMenu}
|
||||
|
||||
@ -151,6 +151,7 @@ export function useCalc({
|
||||
})
|
||||
if (trap(error)) return
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||
import { throttle, isReducedMotion } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const updateDollyZoom = throttle(
|
||||
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||
@ -16,8 +17,8 @@ export const CamToggle = () => {
|
||||
useEffect(() => {
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.SceneReady,
|
||||
async () => {
|
||||
sceneInfra.camControls.dollyZoom(fov)
|
||||
() => {
|
||||
sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
@ -26,11 +27,11 @@ export const CamToggle = () => {
|
||||
if (isPerspective) {
|
||||
isReducedMotion()
|
||||
? sceneInfra.camControls.useOrthographicCamera()
|
||||
: sceneInfra.camControls.animateToOrthographic()
|
||||
: sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
|
||||
} else {
|
||||
isReducedMotion()
|
||||
? sceneInfra.camControls.usePerspectiveCamera()
|
||||
: sceneInfra.camControls.animateToPerspective()
|
||||
? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
|
||||
: sceneInfra.camControls.animateToPerspective().catch(reportRejection)
|
||||
}
|
||||
setIsPerspective(!isPerspective)
|
||||
}
|
||||
|
||||
@ -71,6 +71,17 @@ function CommandArgOptionInput({
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
}, [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,
|
||||
// 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 { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { createContext, useEffect } from 'react'
|
||||
import { EventFrom, StateFrom } from 'xstate'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type CommandsContextType = {
|
||||
commandBarState: StateFrom<typeof commandBarMachine>
|
||||
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,
|
||||
export const CommandsContext = createActorContext(
|
||||
commandBarMachine.provide({
|
||||
guards: {
|
||||
'Command has no arguments': (context, _event) => {
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
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(
|
||||
(argConfig) => argConfig.skip
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
editorManager.setCommandBarSend(commandBarSend)
|
||||
})
|
||||
|
||||
export const CommandBarProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<CommandsContext.Provider
|
||||
value={{
|
||||
commandBarState,
|
||||
commandBarSend,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandsContext.Provider>
|
||||
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||
</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()
|
||||
commandBarSend({
|
||||
type: 'Submit command',
|
||||
data: argumentsToSubmit,
|
||||
output: argumentsToSubmit,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const initSelectionsByType = useCallback(() => {
|
||||
const selectionsByType = useMemo(() => {
|
||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||
? 'none'
|
||||
: getSelectionType(selection)
|
||||
}, [selection, code])
|
||||
const selectionsByType = initSelectionsByType()
|
||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||
canSubmitSelectionArg(selectionsByType, arg)
|
||||
const canSubmitSelection = useMemo<boolean>(
|
||||
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||
[selectionsByType]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
console.log('selection input effect', {
|
||||
selectionsByType,
|
||||
canSubmitSelection,
|
||||
arg,
|
||||
})
|
||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||
handleSubmit({
|
||||
preventDefault: () => {},
|
||||
} as React.FormEvent<HTMLFormElement>)
|
||||
handleSubmit()
|
||||
}
|
||||
}, [selectionsByType, arg])
|
||||
}, [canSubmitSelection])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!canSubmitSelection) {
|
||||
setHasSubmitted(true)
|
||||
|
||||
@ -11,6 +11,7 @@ export function CommandBarOpenButton() {
|
||||
<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"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
data-testid="command-bar-open-button"
|
||||
>
|
||||
<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">
|
||||
|
||||
@ -135,16 +135,15 @@ interface ContextMenuItemProps {
|
||||
icon?: ActionIconProps['icon']
|
||||
onClick?: () => void
|
||||
hotkey?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
export function ContextMenuItem({
|
||||
children,
|
||||
icon,
|
||||
onClick,
|
||||
hotkey,
|
||||
}: ContextMenuItemProps) {
|
||||
export function ContextMenuItem(props: ContextMenuItemProps) {
|
||||
const { children, icon, onClick, hotkey } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
data-testid={props['data-testid']}
|
||||
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@ -332,7 +332,7 @@ const CustomIconMap = {
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 4C4.11929 4 3 5.11929 3 6.5V7C3 10.0376 5.46243 12.5 8.5 12.5H8.96482C9.46635 12.5 9.93469 12.2493 10.2129 11.8321L10.5173 11.3755C11.1396 12.0849 12.0423 12.5 13 12.5H13.75H15V14C15 14.2626 14.9483 14.5227 14.8478 14.7654C14.7472 15.008 14.5999 15.2285 14.4142 15.4142C14.2285 15.5999 14.008 15.7472 13.7654 15.8478C13.5227 15.9483 13.2626 16 13 16C12.7374 16 12.4773 15.9483 12.2346 15.8478C11.992 15.7472 11.7715 15.5999 11.5858 15.4142C11.4001 15.2285 11.2528 15.008 11.1522 14.7654C11.1164 14.6789 11.0868 14.5902 11.0635 14.5H11.8544C11.9168 14.6431 12.0056 14.7734 12.1161 14.8839C12.2322 15 12.37 15.092 12.5216 15.1548C12.6733 15.2177 12.8358 15.25 13 15.25C13.1642 15.25 13.3267 15.2177 13.4784 15.1548C13.63 15.092 13.7678 15 13.8839 14.8839C14 14.7678 14.092 14.63 14.1548 14.4784C14.2177 14.3267 14.25 14.1642 14.25 14V13H13.25V14C13.25 14.0328 13.2435 14.0653 13.231 14.0957C13.2184 14.126 13.2 14.1536 13.1768 14.1768C13.1536 14.2 13.126 14.2184 13.0957 14.231C13.0653 14.2435 13.0328 14.25 13 14.25C12.9672 14.25 12.9347 14.2435 12.9043 14.231C12.874 14.2184 12.8464 14.2 12.8232 14.1768C12.8 14.1536 12.7816 14.126 12.769 14.0957C12.7565 14.0653 12.75 14.0328 12.75 14V13.5H12.25H10.5H10V14C10 14.394 10.0776 14.7841 10.2284 15.1481C10.3791 15.512 10.6001 15.8427 10.8787 16.1213C11.1573 16.3999 11.488 16.6209 11.8519 16.7716C12.2159 16.9224 12.606 17 13 17C13.394 17 13.7841 16.9224 14.1481 16.7716C14.512 16.6209 14.8427 16.3999 15.1213 16.1213C15.3999 15.8427 15.6209 15.512 15.7716 15.1481C15.9224 14.7841 16 14.394 16 14V12.5H17V11.5H16V8.5C16 6.01472 13.9853 4 11.5 4H5.5ZM11.084 10.4746L10.9226 10.2326L9.42875 7.74275L8.57125 8.25725L9.90846 10.4859L9.38084 11.2773C9.28811 11.4164 9.13199 11.5 8.96482 11.5H8.5C6.01472 11.5 4 9.48528 4 7V6.5C4 5.67157 4.67157 5 5.5 5H11.5C13.433 5 15 6.567 15 8.5V11.5H13.75H13C12.2301 11.5 11.5111 11.1152 11.084 10.4746ZM13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5Z"
|
||||
fill="black"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { CommandLog } from 'lang/std/engineConnection'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function useEngineCommands(): [CommandLog[], () => void] {
|
||||
export function useEngineCommands(): [CommandLog[], () => void] {
|
||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
|
||||
engineCommandManager.commandLogs
|
||||
)
|
||||
@ -77,9 +78,11 @@ export const EngineCommands = () => {
|
||||
/>
|
||||
<button
|
||||
data-testid="custom-cmd-send-button"
|
||||
onClick={() =>
|
||||
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
||||
}
|
||||
onClick={() => {
|
||||
engineCommandManager
|
||||
.sendSceneCommand(JSON.parse(customCmd))
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Send custom command
|
||||
</button>
|
||||
|
||||
@ -5,25 +5,28 @@ import { PATHS } from 'lib/paths'
|
||||
import React, { createContext } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
EventFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
fromPromise,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||
import {
|
||||
DEFAULT_FILE_NAME,
|
||||
DEFAULT_PROJECT_KCL_FILE,
|
||||
FILE_EXT,
|
||||
} from 'lib/constants'
|
||||
import { getProjectInfo } from 'lib/desktop'
|
||||
import { getNextDirName, getNextFileName } from 'lib/desktopFS'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const FileContext = createContext(
|
||||
@ -39,199 +42,234 @@ export const FileMachineProvider = ({
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
|
||||
const [state, send] = useMachine(fileMachine, {
|
||||
context: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
actions: {
|
||||
navigateToFile: (context, event) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
commandBarSend({ type: 'Close' })
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory +
|
||||
window.electron.path.sep +
|
||||
event.data.name
|
||||
)}`
|
||||
const [state, send] = useMachine(
|
||||
fileMachine.provide({
|
||||
actions: {
|
||||
renameToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
createToastSuccess: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastSuccess: ({ event }) => {
|
||||
if (
|
||||
event.type !== 'xstate.done.actor.rename-file' &&
|
||||
event.type !== 'xstate.done.actor.delete-file'
|
||||
)
|
||||
} else if (
|
||||
event.data &&
|
||||
'path' in event.data &&
|
||||
event.data.path.endsWith(FILE_EXT)
|
||||
) {
|
||||
// Don't navigate to newly created directories
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
||||
}
|
||||
return
|
||||
toast.success(event.output.message)
|
||||
},
|
||||
toastError: ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||
toast.error(event.output.message)
|
||||
},
|
||||
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({
|
||||
itemsBeingRenamed: (context, event) => [
|
||||
...context.itemsBeingRenamed,
|
||||
event.data.path,
|
||||
],
|
||||
}),
|
||||
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
|
||||
actors: {
|
||||
readFiles: fromPromise(async ({ input }) => {
|
||||
const newFiles =
|
||||
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
|
||||
[]
|
||||
return {
|
||||
...input,
|
||||
children: newFiles,
|
||||
}
|
||||
}),
|
||||
createAndOpenFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.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
|
||||
)
|
||||
|
||||
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,
|
||||
oldPath,
|
||||
}
|
||||
},
|
||||
deleteFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
||||
) => {
|
||||
const isDir = !!event.data.children
|
||||
|
||||
if (isDir) {
|
||||
await window.electron
|
||||
.rm(event.data.path, {
|
||||
recursive: true,
|
||||
if (input.makeDir) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
})
|
||||
.catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await window.electron
|
||||
.rm(event.data.path)
|
||||
.catch((e) => console.error('Error deleting file', e))
|
||||
}
|
||||
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 ?? '')
|
||||
}
|
||||
|
||||
// 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 {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
path: createdPath,
|
||||
}
|
||||
}),
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
event.data.name
|
||||
}"`
|
||||
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,
|
||||
oldPath,
|
||||
}
|
||||
}),
|
||||
deleteFile: fromPromise(async ({ input }) => {
|
||||
const isDir = !!input.children
|
||||
|
||||
if (isDir) {
|
||||
await window.electron
|
||||
.rm(input.path, {
|
||||
recursive: true,
|
||||
})
|
||||
.catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await window.electron
|
||||
.rm(input.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 {
|
||||
message: 'No more files in project, created main.kcl',
|
||||
}
|
||||
}
|
||||
|
||||
// If we just deleted the current file or one of its parent directories,
|
||||
// navigate to the project root
|
||||
if (
|
||||
(input.path === file?.path || file?.path.includes(input.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
input.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
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
project,
|
||||
selectedDirectory: project,
|
||||
},
|
||||
'Is not silent': (_, event) => !event.data?.silent,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FileContext.Provider
|
||||
|
||||
@ -176,13 +176,12 @@ const FileTreeItem = ({
|
||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||
codeManager.code
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
codeManager.writeToFile()
|
||||
|
||||
// Prevent seeing the model built one piece at a time when changing files
|
||||
kclManager.isFirstRender = true
|
||||
kclManager.executeCode(true).then(() => {
|
||||
kclManager.isFirstRender = false
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.executeCode(true)
|
||||
} else {
|
||||
// Let the lsp servers know we closed a file.
|
||||
onFileClose(currentFile?.path || null, project?.path || null)
|
||||
@ -246,13 +245,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
@ -299,13 +298,13 @@ const FileTreeItem = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -358,10 +357,18 @@ function FileTreeContextMenu({
|
||||
<ContextMenu
|
||||
menuTargetElement={itemRef}
|
||||
items={[
|
||||
<ContextMenuItem onClick={onRename} hotkey="Enter">
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-rename"
|
||||
onClick={onRename}
|
||||
hotkey="Enter"
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-delete"
|
||||
onClick={onDelete}
|
||||
hotkey={metaKey + ' + Del'}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
@ -383,14 +390,14 @@ interface FileTreeProps {
|
||||
export const FileTreeMenu = () => {
|
||||
const { send } = useFileContext()
|
||||
|
||||
async function createFile() {
|
||||
function createFile() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: false },
|
||||
})
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
function createFolder() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: true },
|
||||
@ -477,7 +484,7 @@ export const FileTreeInner = ({
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileContext.project,
|
||||
directory: fileContext.project,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
} from './ContextMenu'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -67,7 +68,9 @@ export default function Gizmo() {
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
@ -75,7 +78,7 @@ export default function Gizmo() {
|
||||
)),
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
@ -299,7 +302,7 @@ const initializeMouseEvents = (
|
||||
const handleClick = () => {
|
||||
if (raycasterIntersect.current) {
|
||||
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 { useLspContext } from './LspProvider'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
const HelpMenuDivider = () => (
|
||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
if (isInProject) {
|
||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||
} else {
|
||||
createAndOpenNewProject({ onProjectOpen, navigate })
|
||||
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
|
||||
reportRejection
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -11,6 +11,8 @@ import {
|
||||
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
import { Spinner } from './Spinner'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
@ -65,17 +67,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
data-testid="loading"
|
||||
>
|
||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||
<circle
|
||||
cx="5"
|
||||
cy="5"
|
||||
r="4"
|
||||
stroke="var(--primary)"
|
||||
fill="none"
|
||||
strokeDasharray="4, 4"
|
||||
className="animate-spin origin-center"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
|
||||
<p
|
||||
className={
|
||||
|
||||
@ -11,6 +11,8 @@ import toast from 'react-hot-toast'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
import { ModelStateIndicator } from './ModelStateIndicator'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
@ -24,7 +26,7 @@ export function LowerRightControls({
|
||||
const linkOverrideClassName =
|
||||
'!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
|
||||
stopPropagation: () => void
|
||||
}) {
|
||||
@ -33,7 +35,9 @@ export function LowerRightControls({
|
||||
|
||||
if (!coreDumpManager) {
|
||||
// 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 {
|
||||
toast
|
||||
.promise(
|
||||
@ -55,7 +59,7 @@ export function LowerRightControls({
|
||||
if (err) {
|
||||
openWindow(
|
||||
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||
)
|
||||
).catch(reportRejection)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -65,6 +69,7 @@ export function LowerRightControls({
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||
{children}
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
||||
|
||||
@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
// Update the folding ranges, since the AST has changed.
|
||||
// This is a hack since codemirror does not support async foldService.
|
||||
// When they do we can delete this.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.updateFoldingRanges()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.requestSemanticTokens()
|
||||
break
|
||||
case 'kcl/memoryUpdated':
|
||||
|
||||
45
src/components/ModelStateIndicator.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useEngineCommands } from './EngineCommands'
|
||||
import { Spinner } from './Spinner'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export const ModelStateIndicator = () => {
|
||||
const [commands] = useEngineCommands()
|
||||
|
||||
const lastCommandType = commands[commands.length - 1]?.type
|
||||
|
||||
let className = 'w-6 h-6 '
|
||||
let icon = <Spinner className={className} />
|
||||
let dataTestId = 'model-state-indicator'
|
||||
|
||||
if (lastCommandType === 'receive-reliable') {
|
||||
className +=
|
||||
'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 + '-receive-reliable'}
|
||||
name="checkmark"
|
||||
/>
|
||||
)
|
||||
} else if (lastCommandType === 'execution-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 + '-execution-done'}
|
||||
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 (
|
||||
<div className={className} data-testid="model-state-indicator">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => handleConvertToVarClick()}
|
||||
onClick={() => {
|
||||
handleConvertToVarClick().catch(reportRejection)
|
||||
}}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
|
||||
@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
icon: 'printer3d',
|
||||
iconClassName: '!p-0',
|
||||
keybinding: 'Ctrl + Shift + M',
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
action: async () => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
|
||||
@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
|
||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
|
||||
</div>
|
||||
{issues[name as ConnectingTypeGroup] && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify(error, null, 2) || ''
|
||||
)
|
||||
setHasCopied(true)
|
||||
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"
|
||||
>
|
||||
{hasCopied ? 'Copied' : 'Copy Error'}
|
||||
|
||||
@ -49,7 +49,11 @@ export const NetworkMachineIndicator = ({
|
||||
{Object.entries(machineManager.machines).map(
|
||||
([hostname, machine]) => (
|
||||
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
|
||||
<p className="">{machine.model || machine.manufacturer}</p>
|
||||
<p className="">
|
||||
{machine.make_model.model ||
|
||||
machine.make_model.manufacturer ||
|
||||
'Unknown Machine'}
|
||||
</p>
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
Hostname {hostname}
|
||||
</p>
|
||||
|
||||
@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
|
||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
||||
import { Project } from 'lib/project'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
function ProjectCard({
|
||||
project,
|
||||
@ -165,10 +167,10 @@ function ProjectCard({
|
||||
{isConfirmingDelete && (
|
||||
<DeleteConfirmationDialog
|
||||
title="Delete Project"
|
||||
onConfirm={async () => {
|
||||
onConfirm={toSync(async () => {
|
||||
await handleDeleteProject(project)
|
||||
setIsConfirmingDelete(false)
|
||||
}}
|
||||
}, reportRejection)}
|
||||
onDismiss={() => setIsConfirmingDelete(false)}
|
||||
>
|
||||
<p className="my-4">
|
||||
|
||||
@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
|
||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
// Window may not be available in some environments
|
||||
window?.location.reload()
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||
|
||||
@ -20,6 +20,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
|
||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
interface AllSettingsFieldsProps {
|
||||
searchParamTab: SettingsLevel
|
||||
@ -54,7 +56,7 @@ export const AllSettingsFields = forwardRef(
|
||||
)
|
||||
: undefined
|
||||
|
||||
async function restartOnboarding() {
|
||||
function restartOnboarding() {
|
||||
send({
|
||||
type: `set.app.onboardingStatus`,
|
||||
data: { level: 'user', value: '' },
|
||||
@ -82,6 +84,7 @@ export const AllSettingsFields = forwardRef(
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
navigateToOnboardingStart()
|
||||
}, [isFileSettings, navigate, state])
|
||||
|
||||
@ -190,7 +193,7 @@ export const AllSettingsFields = forwardRef(
|
||||
{isDesktop() && (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
const paths = await getSettingsFolderPaths(
|
||||
projectPath ? decodeURIComponent(projectPath) : undefined
|
||||
)
|
||||
@ -199,7 +202,7 @@ export const AllSettingsFields = forwardRef(
|
||||
return new Error('finalPath undefined')
|
||||
}
|
||||
window.electron.showInFolder(finalPath)
|
||||
}}
|
||||
}, reportRejection)}
|
||||
iconStart={{
|
||||
icon: 'folder',
|
||||
size: 'sm',
|
||||
@ -211,14 +214,14 @@ export const AllSettingsFields = forwardRef(
|
||||
)}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={async () => {
|
||||
onClick={toSync(async () => {
|
||||
const defaultDirectory = await getInitialDefaultDir()
|
||||
send({
|
||||
type: 'Reset settings',
|
||||
defaultDirectory,
|
||||
})
|
||||
toast.success('Settings restored to default')
|
||||
}}
|
||||
}, reportRejection)}
|
||||
iconStart={{
|
||||
icon: 'refresh',
|
||||
size: 'sm',
|
||||
|
||||