Compare commits

...

51 Commits

Author SHA1 Message Date
08b0408be2 Try again for windows with json copy 2024-07-18 05:36:01 -04:00
77a67b3413 v1Compatible 2024-07-16 21:37:02 -04:00
2f474e6f40 Dummy updater-test 99.99.99 2024-07-16 19:53:23 -04:00
a185005f5c Update last_update / last_download gen for win arm 2024-07-16 19:44:56 -04:00
21c72073a9 Add createUpdaterArtifacts property (breaking change in cli) 2024-07-16 19:42:10 -04:00
fe21d66c0d Add missed if case 2024-07-16 18:39:07 -04:00
e4567b43f8 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 22:00:55 +00:00
90e7741e47 Fix macos release build 2024-07-16 17:51:56 -04:00
a60f0d1355 Test build release 2024-07-16 17:38:33 -04:00
9e336b4654 Cleaning up upload 2024-07-16 16:54:31 -04:00
bef7467703 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 11:09:34 +00:00
6444fe50e7 Trigger CI 2024-07-16 06:59:52 -04:00
93c7a6a293 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 10:51:24 +00:00
eaadc0e39e Trigger CI 2024-07-16 06:45:52 -04:00
933850e13b A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-07-16 09:11:55 +00:00
07b9401f2d Merge branch 'main' into pierremtb/issue2805 2024-07-16 05:06:18 -04:00
d1a2bd01ca Lower threshold for 2020 tests (#3030)
* Lower threshold for 2020 tests

Now that the tests zoom into the model and center it before taking a
snapshot, they should be less sensitive.

* Genuine, nontrivial changes to the integration test images
2024-07-15 17:41:41 -05:00
aca13d087b add in benchmarks for digest code (#3031)
(initial results look good!)
2024-07-15 13:45:55 -04:00
fcdde3e482 Bump syn from 2.0.70 to 2.0.71 in /src/wasm-lib (#3029)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.70 to 2.0.71.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.70...2.0.71)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-15 09:26:18 -07:00
dc5277c48b Merge branch 'main' into pierremtb/issue2805 2024-07-15 05:27:50 -04:00
a1df3d0ffc Fillet UI (#2718)
* draft: fillet ast mod + test

* Kurt's rejig

* playwright

* update button enable logic

* remove fillet button in production build

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* trigger CI

* fix typo

* give a way to turn on fillets

---------

Co-authored-by: max-mrgrsk <margorskyi@gmail.com>
Co-authored-by: max-mrgrsk <156543465+max-mrgrsk@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-15 19:20:32 +10:00
1852e6167b Fix argument persistence and accidental submission command bar bugs (#3021) 2024-07-14 18:15:34 -04:00
29bf77bb82 Show descriptions for generated commands, make them look better and sort better (#3023) 2024-07-12 17:48:38 -04:00
3a4e323c45 Rename binary to zoo-modeling-app, enable msi bundle for windows-aarch64 2024-07-12 17:11:55 -04:00
e81b614523 Lf94/save settings between reconnects (#2997)
* Keep settings between reconnects

* Set idle timeout to 2 minutes

* Put idle behind flags

* Remove pauses

* Fix online->offline->online

* Revert "Remove pauses"

This reverts commit 267ef4ff4b86f2d8014bfb2a8e8a633adc8001dc.

* ci

* call correct setmediastream
2024-07-12 20:42:23 +00:00
5a5fe3bb95 Add sketch tools back to the command bar (#3008)
* Make machine command type names more explicit

* Prepare "change tool" event for command bar

* Make it so that state machine events can each map to multiple command configs

* Make commands with all skippable args possible

* Add back the tools to the command bar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Update to use new `groupId` property name

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Oops didn't save this other instance of `ownerMachine`

* Add a playwright test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-12 16:16:26 -04:00
0710f6e5f2 Add format code to the command palette (#3001)
* Add format code to the command palette

* Fix to use renamed groupId parameter

* Add icon to format code command

* Fix to remove commands during teardown

* Fix dependencies

* Change formatting
2024-07-12 17:08:01 +00:00
c9d5633647 Bump thiserror from 1.0.61 to 1.0.62 in /src/wasm-lib (#3016)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.62.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 09:11:49 -07:00
bbe4f50b17 Merge branch 'main' into pierremtb/issue2805 2024-07-12 08:15:41 -04:00
e376e397db Win aarch64 bundle 2024-07-12 07:55:22 -04:00
c7c747a109 Clean up 2024-07-12 07:11:16 -04:00
f9419a98b5 Cut release v0.24.1 (#3014) 2024-07-11 21:51:58 -04:00
999f72bccf mediaStream (#3013)
* mediaStream

* make vitest happy

* fmt
2024-07-11 20:57:27 -04:00
9dbe74e008 cleaner hack (#3012) 2024-07-12 09:41:39 +10:00
88d9cdc52b Codemirror deferrers (#3006)
* Force document update requests when necessary

* fix up codemirror deferrers

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* lock

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups kcl/index

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix copilot

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
2024-07-11 16:05:19 -07:00
2dd1f0f213 refactor: Rename ownerMachine to groupId in Command (#3010)
* refactor: Rename ownerMachine to groupId in Command

Commands don't need to be part of a state machine.

* Fix formatting
2024-07-11 18:10:47 -04:00
dd3d604924 Merge branch 'main' into pierremtb/issue2805 2024-07-11 08:20:58 -04:00
b19e89effc More clean up, fix CUT_RELEASE_PR eval 2024-07-11 07:53:13 -04:00
416002e84a Clean up 2024-07-11 07:23:50 -04:00
5c3571226b Clean up 2024-07-11 07:22:56 -04:00
d9eead1641 Break apart ci.yml into build-test-web and build-test-publish-apps 2024-07-11 07:21:54 -04:00
468e86a864 Add windows arm64 releases
Fixes #2805
2024-07-11 07:02:45 -04:00
3f977c7bf9 Merge branch 'main' into pierremtb/issue2805 2024-07-10 07:42:43 -04:00
7d7572b36a Merge branch 'main' into pierremtb/issue2805 2024-07-03 08:24:42 -04:00
e05bce6145 Upload the right app.exe 2024-06-26 08:15:17 -04:00
c58c0e7089 WIP 2024-06-26 06:50:20 -04:00
83ece965d9 No bundles for arm 2024-06-26 06:04:11 -04:00
de7e958e22 Add aarch64 target for Windows 2024-06-26 05:07:51 -04:00
1beadc063d Break down build-test-apps per platform 2024-06-26 05:00:41 -04:00
3bc2a00f4e Merge branch 'pierremtb/fix-ci-runners-versions' into pierremtb/issue2805 2024-06-26 04:34:33 -04:00
0f256ea6c5 Pin CI runner versions (I don't love 'latest') 2024-06-11 10:07:44 +02:00
105 changed files with 4002 additions and 1032 deletions

View File

@ -1,95 +1,21 @@
name: CI
name: build-test-publish-apps
on:
pull_request:
push:
branches:
- main
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
- pierremtb/issue2805-push-updater-windows-aarch64
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
CUT_RELEASE_PR: false
BUILD_RELEASE: true
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
@ -100,6 +26,10 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set dummy version
run: |
VERSION=99.99.99 yarn bump-jsons
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
@ -112,13 +42,11 @@ jobs:
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
@ -129,54 +57,21 @@ jobs:
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-apps:
build-test-app-macos:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-14, ubuntu-latest, windows-latest]
env:
# Specific Apple Universal target for macos
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
# Only build executable on linux (no appimage or deb)
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
runs-on: macos-14
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
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Update WebView2 on Windows
if: matrix.os == 'windows-latest'
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
@ -197,60 +92,25 @@ jobs:
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
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Run vite build (build:both)
- name: Run vite build
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Install x86 target for Universal builds (MacOS only)
if: matrix.os == 'macos-14'
- name: Install x86 target for Universal builds
run: |
rustup target add x86_64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build --debug --target universal-apple-darwin"
- name: Build for Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
shell: bash
run: |
unset APPLE_SIGNING_IDENTITY
@ -314,7 +174,7 @@ jobs:
- name: 'Upload to Mac TestFlight (nightly)'
uses: apple-actions/upload-testflight-build@v1
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
with:
app-path: 'src-tauri/target/universal-apple-darwin/release/bundle/macos/Zoo Modeling App.pkg'
issuer-id: ${{ secrets.APPLE_STORE_ISSUER_ID }}
@ -324,7 +184,7 @@ jobs:
- name: Clean up after Mac TestFlight (nightly)
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
if: ${{ github.event_name == 'schedule' }}
shell: bash
run: |
git status
@ -350,35 +210,155 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
run: "yarn tauri build --config src-tauri/tauri.release.conf.json --target universal-apple-darwin"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
path: "src-tauri/target/universal-apple-darwin/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Run e2e tests (linux only)
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: "cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json"
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: "yarn tauri build -c src-tauri/tauri.release.conf.json -b dmg --target universal-apple-darwin"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
path: "src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg"
name: updater-test
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
run: |
cargo install tauri-driver --force
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Run e2e tests (windows only)
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Update WebView2 on Windows
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- 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
- name: Setup Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Install aarch64 target
run: rustup target add aarch64-pc-windows-msvc
- 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
- name: Run vite build
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug) for x86_64
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn run tauri build --debug"
- name: Build the app (release) and sign for x86_64
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build --config src-tauri\\tauri.release.conf.json"
- uses: actions/upload-artifact@v3
with:
path: "src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Build the app (debug) for aarch64
if: ${{ env.BUILD_RELEASE == 'false' }}
run: yarn run tauri build --debug --target aarch64-pc-windows-msvc
- name: Build the app (release) and sign for aarch64
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: "yarn tauri build --config src-tauri\\tauri.release.conf.json --target aarch64-pc-windows-msvc"
- uses: actions/upload-artifact@v3
with:
path: "src-tauri/target/aarch64-pc-windows-msvc/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/bundle/*/*"
- name: Run e2e tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
yarn wdio run wdio.conf.ts
env:
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\zoo-modeling-app.exe"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true
@ -389,39 +369,32 @@ jobs:
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
run: "cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json"
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: "yarn tauri build -c src-tauri\\tauri.release.conf.json -b msi"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
path: "src-tauri/target/release/bundle/msi/*.msi"
name: updater-test
publish-apps-release:
runs-on: ubuntu-22.04
permissions:
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows]
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 }}
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' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
VERSION: ${{ needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event.repository.updated_at }}
NOTES: ${{ format('Dummy build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ 'dl.kittycad.io/releases/modeling-app/updater-test' }}
WEBSITE_DIR: ${{ 'dl.zoo.dev/releases/modeling-app/updater-test' }}
URL_CODED_NAME: ${{ 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/download-artifact@v3
@ -429,7 +402,8 @@ jobs:
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.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}" \
@ -437,8 +411,10 @@ jobs:
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--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,
@ -453,8 +429,12 @@ jobs:
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
"signature": $windows_x86_64_sig,
"url": $windows_x86_64_url
},
"windows-aarch64": {
"signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
}
}
}' > last_update.json
@ -468,7 +448,8 @@ jobs:
--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_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--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" \
'{
"version": $version,
"pub_date": $pub_date,
@ -478,7 +459,10 @@ jobs:
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_url
"url": $windows_x86_64_url
},
"msi-aarch64": {
"url": $windows_aarch64_url
}
}
}' > last_download.json
@ -522,7 +506,7 @@ jobs:
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: github.event_name == 'release'
steps:
- name: Check out code

78
.github/workflows/build-test-web.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: build-test-web
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-22.04'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch

View File

@ -7,7 +7,7 @@ on:
jobs:
create-release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: read

View File

@ -325,7 +325,7 @@ $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"
$env:E2E_APPLICATION=".\src-tauri\target\debug\zoo-modeling-app.exe"
Stop-Process -Name msedgedriver
yarn wdio run wdio.conf.ts
```

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
).not.toBeDisabled()
})
test('Fillet button states test', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
const selectClose = () => page.getByText(`close(%)`).click()
const clickEmpty = () => page.mouse.click(950, 100)
// expect fillet button without any bodies in the scene
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
const extrude001 = extrude(10, sketch001)`
await u.codeLocator.fill(codeToAdd)
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await selectClose()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
})
const removeAfterFirstParenthesis = (inputString: string) => {
const index = inputString.indexOf('(')
if (index !== -1) {
@ -3500,11 +3543,62 @@ test.describe('Command bar tests', () => {
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
)
})
test('Command bar works and can change a setting', async ({ page }) => {
test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands')
const themeOption = page.getByRole('option', {
name: 'theme',
exact: false,
})
const commandLevelArgButton = page.getByRole('button', { name: 'level' })
const commandThemeArgButton = page.getByRole('button', { name: 'value' })
// This selector changes after we set the setting
let commandOptionInput = page.getByPlaceholder('Select an option')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -3515,23 +3609,17 @@ test.describe('Command bar tests', () => {
.or(page.getByRole('button', { name: '⌘K' }))
.click()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await cmdSearchBar.fill('theme')
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
@ -3553,6 +3641,24 @@ test.describe('Command bar tests', () => {
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
commandOptionInput = page.getByPlaceholder('system')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click()
await cmdSearchBar.focus()
await cmdSearchBar.fill('theme')
await themeOption.click()
await expect(commandThemeArgButton).toBeDisabled()
await commandOptionInput.focus()
await commandOptionInput.fill('lig')
await commandLevelArgButton.click()
await expect(commandLevelArgButton).toBeDisabled()
// Test case for https://github.com/KittyCAD/modeling-app/issues/2881
await commandThemeArgButton.click()
await expect(commandThemeArgButton).toBeDisabled()
await expect(commandLevelArgButton).toHaveText('level: project')
})
test('Command bar keybinding works from code editor and can change a setting', async ({
@ -3577,7 +3683,7 @@ test.describe('Command bar tests', () => {
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
@ -3648,7 +3754,9 @@ test.describe('Command bar tests', () => {
await page.mouse.click(700, 200)
// Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
@ -3663,11 +3771,12 @@ test.describe('Command bar tests', () => {
// Review step and argument hotkeys
await expect(submitButton).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(submitButton).toBeFocused()
await submitButton.press('Backspace')
// Assert we're back on the distance step
await expect(
page.getByRole('button', { name: 'Distance 5', exact: false })
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
await continueButton.click()
@ -3691,6 +3800,47 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
) // remove newlines
)
})
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.move(700, 300, { steps: 5 })
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
})
test.describe('Regression tests', () => {
@ -4642,10 +4792,10 @@ test.describe('Sketch tests', () => {
// click extrude
await page.getByRole('button', { name: 'Extrude' }).click()
// sketch selection should already have been made. "Selection 1 face" only show up when the selection has been made already
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection.
await expect(
page.getByRole('button', { name: 'Selection 1 face' })
page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible()
})
test("Existing sketch with bad code delete user's code", async ({ page }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.24.0",
"name": "zoo-modeling-app",
"version": "0.24.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",
@ -113,7 +113,7 @@
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.45.1",
"@tauri-apps/cli": "==2.0.0-beta.13",
"@tauri-apps/cli": "==2.0.0-beta.22",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/mocha": "^10.0.6",

View File

@ -93,23 +93,10 @@ export class LanguageServerPlugin implements PluginValue {
private doSemanticTokens: boolean = false
private doFoldingRanges: boolean = false
private _defferer = deferExecution((code: string) => {
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: code }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}, this.changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduled: number | null = null
constructor(options: LanguageServerOptions, private view: EditorView) {
this.client = options.client
@ -152,14 +139,9 @@ export class LanguageServerPlugin implements PluginValue {
}
update(viewUpdate: ViewUpdate) {
// If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
return
if (viewUpdate.docChanged) {
this.scheduleSendDoc()
}
this.sendChange({
documentText: viewUpdate.state.doc.toString(),
})
}
destroy() {
@ -184,16 +166,6 @@ export class LanguageServerPlugin implements PluginValue {
this.updateFoldingRanges()
}
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
this._defferer(documentText)
}
requestDiagnostics() {
this.sendChange({ documentText: this.getDocText() })
}
async requestHoverTooltip(
view: EditorView,
{ line, character }: { line: number; character: number }
@ -204,7 +176,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.sendChange({ documentText: this.getDocText() })
this.ensureDocSent()
const result = await this.client.textDocumentHover({
textDocument: { uri: this.getDocUri() },
position: { line, character },
@ -227,6 +199,42 @@ export class LanguageServerPlugin implements PluginValue {
return { pos, end, create: (view) => ({ dom }), above: true }
}
scheduleSendDoc() {
if (this.sendScheduled != null) window.clearTimeout(this.sendScheduled)
this.sendScheduled = window.setTimeout(
() => this.sendDoc(),
this.changesDelay
)
}
sendDoc() {
if (this.sendScheduled != null) {
window.clearTimeout(this.sendScheduled)
this.sendScheduled = null
}
if (!this.client.ready) return
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.view.state.doc.toString() }],
})
this.requestSemanticTokens()
this.updateFoldingRanges()
} catch (e) {
console.error(e)
}
}
ensureDocSent() {
if (this.sendScheduled != null) this.sendDoc()
}
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
if (
!this.doFoldingRanges ||
@ -284,13 +292,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.client.textDocumentDidChange({
textDocument: {
uri: this.getDocUri(),
version: this.documentVersion++,
},
contentChanges: [{ text: this.getDocText() }],
})
this.ensureDocSent()
const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.getDocUri() },
@ -330,9 +332,7 @@ export class LanguageServerPlugin implements PluginValue {
)
return null
this.sendChange({
documentText: context.state.doc.toString(),
})
this.ensureDocSent()
const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.getDocUri() },

1528
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
[package]
name = "app"
name = "zoo-modeling-app"
version = "0.1.0"
description = "The Zoo Modeling App"
authors = ["Zoo Engineers <eng@zoo.dev>"]
license = ""
repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app"
default-run = "zoo-modeling-app"
edition = "2021"
rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-beta.18", features = [] }
tauri-bundler = { git = "https://github.com/tauri-apps/tauri", rev = "1a88fc1a9b81bd09fc24cf0dfed95d20dd72a1bb" }
[dependencies]
anyhow = "1"

View File

@ -80,5 +80,5 @@
}
},
"productName": "Zoo Modeling App",
"version": "0.24.0"
"version": "0.24.1"
}

View File

@ -1,6 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"createUpdaterArtifacts": "v1Compatible",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",

View File

@ -39,3 +39,32 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
</AppStateContext.Provider>
)
}
interface AppStream {
mediaStream: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
}
const AppStreamContext = createContext<AppStream>({
mediaStream: undefined as unknown as MediaStream,
setMediaStream: () => {},
})
export const useAppStream = () => useContext(AppStreamContext)
export const AppStreamProvider = ({ children }: { children: ReactNode }) => {
const [mediaStream, setMediaStream] = useState<MediaStream>(
undefined as unknown as MediaStream
)
return (
<AppStreamContext.Provider
value={{
mediaStream,
setMediaStream,
}}
>
{children}
</AppStreamContext.Provider>
)
}

View File

@ -16,6 +16,7 @@ import {
canRectangleTool,
isEditingExistingSketch,
} from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({
className = '',
@ -60,7 +61,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'line',
data: { tool: 'line' },
}),
{ enabled: !disableLineButton, scopes: ['sketch'] }
)
@ -75,7 +76,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'tangentialArc',
data: { tool: 'tangentialArc' },
}),
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
)
@ -89,7 +90,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'rectangle',
data: { tool: 'rectangle' },
}),
{ enabled: !disableRectangle, scopes: ['sketch'] }
)
@ -114,10 +115,20 @@ export function Toolbar({
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' },
data: { name: 'Extrude', groupId: 'modeling' },
}),
{ enabled: !disableAllButtons, scopes: ['modeling'] }
)
const disableFillet = !state.can('Fillet') || disableAllButtons
useHotkeys(
'f',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
{ enabled: !disableFillet, scopes: ['modeling'] }
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current
@ -263,7 +274,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'line',
data: { tool: 'line' },
})
}
aria-pressed={state?.matches('Sketch.Line tool')}
@ -293,7 +304,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'tangentialArc',
data: { tool: 'tangentialArc' },
})
}
aria-pressed={state.matches('Sketch.Tangential arc to')}
@ -323,7 +334,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'rectangle',
data: { tool: 'rectangle' },
})
}
aria-pressed={state.matches('Sketch.Rectangle tool')}
@ -378,7 +389,7 @@ export function Toolbar({
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' },
data: { name: 'Extrude', groupId: 'modeling' },
})
}
disabled={!state.can('Extrude') || disableAllButtons}
@ -404,6 +415,36 @@ export function Toolbar({
</ActionButton>
</li>
)}
{state.matches('idle') && (DEV || (window as any)._enableFillet) && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
})
}
disabled={disableFillet}
title={disableFillet ? 'fillet' : "edge can't be filleted"}
iconStart={{
icon: 'fillet', // todo: add fillet icon
iconClassName,
bgClassName,
}}
>
Fillet
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: F
</Tooltip>
</ActionButton>
</li>
)}
</ul>
</menu>
)

View File

@ -41,6 +41,7 @@ function CommandArgOptionInput({
)
const inputRef = useRef<HTMLInputElement>(null)
const formRef = useRef<HTMLFormElement>(null)
const [shouldSubmitOnChange, setShouldSubmitOnChange] = useState(false)
const [selectedOption, setSelectedOption] = useState<
CommandArgumentOption<unknown>
>(currentOption || resolvedOptions[0])
@ -82,8 +83,10 @@ function CommandArgOptionInput({
// We deal with the whole option object internally
setSelectedOption(option)
// But we only submit the value
onSubmit(option.value)
// But we only submit the value itself
if (shouldSubmitOnChange) {
onSubmit(option.value)
}
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
@ -94,7 +97,18 @@ function CommandArgOptionInput({
}
return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<form
id="arg-form"
onSubmit={handleSubmit}
ref={formRef}
onKeyDownCapture={(e) => {
if (e.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}}
>
<Combobox
value={selectedOption}
onChange={handleSelectOption}
@ -118,6 +132,12 @@ function CommandArgOptionInput({
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
}
if (event.key === 'Enter') {
setShouldSubmitOnChange(true)
} else {
setShouldSubmitOnChange(false)
}
}}
value={query}
placeholder={
@ -136,6 +156,9 @@ function CommandArgOptionInput({
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
onMouseDown={() => {
setShouldSubmitOnChange(true)
}}
>
{filteredOptions?.map((option) => (
<Combobox.Option

View File

@ -114,6 +114,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
>
{argName}
</span>
<span className="sr-only">:&nbsp;</span>
{argValue ? (
arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections)

View File

@ -28,6 +28,11 @@ export const CommandBarProvider = ({
Object.keys(context.selectedCommand?.args).length === 0
)
},
'All arguments are skippable': (context, _event) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
},
})

View File

@ -11,6 +11,25 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate'
const semanticEntityNames = {
face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: string[]) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
if (entityTypes.includes(type)) {
semanticSelectionType.add(entity)
}
})
})
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges
@ -85,7 +104,9 @@ function CommandBarSelectionInput({
>
{canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected'
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`}
: `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
<input
id="selection"
name="selection"

View File

@ -70,19 +70,23 @@ function CommandComboBox({
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.name}
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<p className="flex-grow">{option.displayName || option.name} </p>
{option.description && (
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40">
{option.description}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
)}
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>

View File

@ -131,6 +131,16 @@ const CustomIconMap = {
/>
</svg>
),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 5L7.77734 14.7794L8.73527 15.0663L11.665 5.28698L10.7071 5ZM2.35356 9.64644L5.85362 6.14644L6.56072 6.85355L3.41423 10L6.56072 13.1464L5.85362 13.8536L2.35356 10.3536L2 10L2.35356 9.64644ZM17.0607 9.64644L13.5607 6.14644L12.8536 6.85355L16 10L12.8536 13.1464L13.5607 13.8535L17.0607 10.3536L17.4142 10L17.0607 9.64644Z"
fill="currentColor"
/>
</svg>
),
dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -177,6 +187,22 @@ const CustomIconMap = {
/>
</svg>
),
fillet: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H5V15H15V12C15 8.13401 11.866 5 8 5ZM5 4H4V5V15V16H5H15H16V15V12C16 7.58172 12.4183 4 8 4H5Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.5 3.5H5.5H8.5C12.9183 3.5 16.5 7.08172 16.5 11.5V14.5V15.5H16V12C16 7.58172 12.4182 4 7.99996 4H4.5V3.5Z"
fill="currentColor"
/>
</svg>
),
file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -33,6 +33,7 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import {
Selections,
canExtrudeSelection,
canFilletSelection,
handleSelectionBatch,
isSelectionLastLine,
isRangeInbetweenCharacters,
@ -42,7 +43,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import {
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
@ -72,6 +73,7 @@ import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -444,6 +446,12 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false
return true
},
'has valid fillet selection': ({ selectionRanges }) =>
hasValidFilletSelection({
selectionRanges,
ast: kclManager.ast,
code: codeManager.code,
}),
'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
@ -494,7 +502,6 @@ export const ModelingMachineProvider = ({
kclManager.ast,
data.sketchPathToNode,
data.extrudePathToNode,
kclManager.programMemory,
data.cap
)
if (trap(sketched)) return Promise.reject(sketched)
@ -920,7 +927,7 @@ export const ModelingMachineProvider = ({
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically

View File

@ -82,11 +82,11 @@ function ProjectMenuPopover({
}) {
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', ownerMachine: 'modeling' }
const findCommand = (obj: { name: string; ownerMachine: string }) =>
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
const findCommand = (obj: { name: string; groupId: string }) =>
Boolean(
commandBarState.context.commands.find(
(c) => c.name === obj.name && c.ownerMachine === obj.ownerMachine
(c) => c.name === obj.name && c.groupId === obj.groupId
)
)

View File

@ -1,3 +1,4 @@
import { DEV } from 'env'
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
@ -9,6 +10,7 @@ import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
export const Stream = () => {
const [isLoading, setIsLoading] = useState(true)
@ -17,9 +19,12 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const IDLE = true
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
@ -51,7 +56,7 @@ export const Stream = () => {
capture: true,
})
const IDLE_TIME_MS = 1000 * 20
const IDLE_TIME_MS = 1000 * 60 * 2
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => {
@ -60,19 +65,21 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true })
engineCommandManager.tearDown()
})
}
// Teardown everything if we go hidden or reconnect
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
if (IDLE && DEV) {
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
}
}
}
@ -80,35 +87,44 @@ export const Stream = () => {
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => {
if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true)
}
// Clear both timers
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
if (IDLE && DEV) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
if (IDLE && DEV) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
return () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true,
})
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
if (IDLE && DEV) {
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
onAnyInput
)
globalThis?.window?.document?.removeEventListener(
'mousedown',
onAnyInput
)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
}
}, [])
@ -124,10 +140,10 @@ export const Stream = () => {
)
return
if (!videoRef.current) return
if (!context.store?.mediaStream) return
if (!mediaStream) return
// Do not immediately play the stream!
videoRef.current.srcObject = context.store.mediaStream
videoRef.current.srcObject = mediaStream
videoRef.current.pause()
send({
@ -136,7 +152,7 @@ export const Stream = () => {
videoElement: videoRef.current,
},
})
}, [context.store?.mediaStream])
}, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return

View File

@ -195,14 +195,15 @@ export class CompletionRequester implements PluginValue {
private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => {
this.requestCompletions()
}, changesDelay)
private _deffererUserSelect = deferExecution(() => {
this.rejectSuggestionCommand()
}, changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduledInput: number | null = null
constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client
}
@ -245,7 +246,34 @@ export class CompletionRequester implements PluginValue {
}
this.lastPos = this.view.state.selection.main.head
if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
if (viewUpdate.docChanged) this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
this.requestCompletions()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
}
ghostText(): GhostText | null {

View File

@ -27,13 +27,10 @@ export class KclPlugin implements PluginValue {
this.client = client
}
private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
kclManager.executeCode()
}, changesDelay)
// When a doc update needs to be sent to the server, this holds the
// timeout handle for it. When null, the server has the up-to-date
// document.
private sendScheduledInput: number | null = null
private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
@ -101,7 +98,34 @@ export class KclPlugin implements PluginValue {
codeManager.code = newCode
codeManager.writeToFile()
this._deffererCodeUpdate(true)
this.scheduleUpdateDoc()
}
scheduleUpdateDoc() {
if (this.sendScheduledInput != null)
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = window.setTimeout(
() => this.updateDoc(),
changesDelay
)
}
updateDoc() {
if (this.sendScheduledInput != null) {
window.clearTimeout(this.sendScheduledInput)
this.sendScheduledInput = null
}
if (!this.client.ready) return
try {
kclManager.executeCode()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {
if (this.sendScheduledInput != null) this.updateDoc()
}
async updateUnits(

View File

@ -1,10 +1,10 @@
import { useLayoutEffect, useEffect, useRef } from 'react'
import { useLayoutEffect, useEffect, useRef, useState } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useAppState } from 'AppState'
import { useAppState, useAppStream } from 'AppState'
export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
@ -28,9 +28,7 @@ export function useSetupEngineManager(
}
) {
const { setAppState } = useAppState()
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const { setMediaStream } = useAppStream()
const hasSetNonZeroDimensions = useRef<boolean>(false)
@ -40,59 +38,60 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool
}
const startEngineInstance = () => {
const startEngineInstance = (restart: boolean = false) => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions(
streamWidth,
streamHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
!hasSetNonZeroDimensions.current &&
quadHeight &&
quadWidth &&
settings.modelingSend
) {
engineCommandManager.start({
setMediaStream: (mediaStream) =>
settings.modelingSend({
type: 'Set context',
data: { mediaStream },
}),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
if (restart) {
kclManager.isFirstRender = false
}
engineCommandManager.start({
restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
}
useLayoutEffect(startEngineInstance, [
useLayoutEffect(() => {
const { width: quadWidth, height: quadHeight } = getDimensions(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
startEngineInstance()
}
}, [
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight,
settings.modelingSend,
@ -101,8 +100,8 @@ export function useSetupEngineManager(
useEffect(() => {
const handleResize = deferExecution(() => {
const { width, height } = getDimensions(
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
settings.modelingContext.store.streamDimensions.streamWidth !== width ||
@ -125,10 +124,37 @@ export function useSetupEngineManager(
}, 500)
const onOnline = () => {
startEngineInstance()
startEngineInstance(true)
}
const onVisibilityChange = () => {
if (window.document.visibilityState === 'visible') {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
}
window.document.addEventListener('visibilitychange', onVisibilityChange)
const onAnyInput = () => {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
const onOffline = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown()
}
@ -136,11 +162,30 @@ export function useSetupEngineManager(
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize)
return () => {
window.document.removeEventListener(
'visibilitychange',
onVisibilityChange
)
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize)
}
}, [])
// Engine relies on many settings so we should rebind events when it changes
// We have to list out the ones we care about because the settings object holds
// non-settings too...
}, [
settings.enableSSAO,
settings.highlightEdges,
settings.showScaleGrid,
settings.theme,
settings.pool,
])
}
function getDimensions(streamWidth?: number, streamHeight?: number) {

View File

@ -6,7 +6,11 @@ import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
Command,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from 'lib/commandTypes'
import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
@ -21,20 +25,20 @@ export type AllMachines =
interface UseStateMachineCommandsArgs<
T extends AllMachines,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
> {
machineId: T['id']
state: StateFrom<T>
send: Function
actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean
onCancel?: () => void
}
export default function useStateMachineCommands<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
>({
machineId,
state,
@ -58,9 +62,10 @@ export default function useStateMachineCommands<
const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) =>
.flatMap((type) =>
createMachineCommand<T, S>({
ownerMachine: machineId,
// The group is the owner machine's ID.
groupId: machineId,
type,
state,
send,

View File

@ -13,6 +13,7 @@ import {
UpdaterRestartModal,
createUpdaterRestartModal,
} from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState'
// uncomment for xstate inspector
// import { DEV } from 'env'
@ -26,28 +27,30 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<HotkeysProvider>
<Router />
<Toaster
position="bottom-center"
toastOptions={{
style: {
borderRadius: '3px',
},
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
success: {
iconTheme: {
primary: 'oklch(89% 0.16 143.4deg)',
secondary: 'oklch(48.62% 0.1654 142.5deg)',
<AppStreamProvider>
<Router />
<Toaster
position="bottom-center"
toastOptions={{
style: {
borderRadius: '3px',
},
duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',
success: {
iconTheme: {
primary: 'oklch(89% 0.16 143.4deg)',
secondary: 'oklch(48.62% 0.1654 142.5deg)',
},
duration:
window?.localStorage.getItem('playwright') === 'true'
? 10 // speed up e2e tests
: 1500,
},
}}
/>
<ModalContainer />
</AppStreamProvider>
</HotkeysProvider>
)

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({
code: codeManager?.code || '',
@ -35,6 +37,7 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
codeManager.registerCallBacks({
@ -50,6 +53,28 @@ export function KclContextProvider({
})
}, [])
// Add format code to command palette.
useEffect(() => {
const commands: Command[] = [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: (data) => {
kclManager.format()
},
},
]
commandBarSend({ type: 'Add commands', data: { commands } })
return () => {
commandBarSend({ type: 'Remove commands', data: { commands } })
}
}, [kclManager, commandBarSend])
return (
<KclContext.Provider
value={{

View File

@ -304,7 +304,6 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = parse(code)
if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
@ -321,8 +320,7 @@ describe('testing sketchOnExtrudedFace', () => {
const extruded = sketchOnExtrudedFace(
ast,
segmentPathToNode,
extrudePathToNode,
programMemory
extrudePathToNode
)
if (err(extruded)) throw extruded
const { modifiedAst } = extruded
@ -345,7 +343,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = parse(code)
if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
@ -362,8 +359,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
const extruded = sketchOnExtrudedFace(
ast,
segmentPathToNode,
extrudePathToNode,
programMemory
extrudePathToNode
)
if (err(extruded)) throw extruded
const { modifiedAst } = extruded
@ -386,7 +382,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = parse(code)
if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [
code.indexOf(sketchSnippet),
@ -404,7 +399,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast,
sketchPathToNode,
extrudePathToNode,
programMemory,
'end'
)
if (err(extruded)) throw extruded
@ -436,7 +430,6 @@ const sketch001 = startSketchOn(part001, 'END')`)
const part001 = extrude(5 + 7, sketch001)`
const ast = parse(code)
if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
@ -453,8 +446,7 @@ const sketch001 = startSketchOn(part001, 'END')`)
const updatedAst = sketchOnExtrudedFace(
ast,
segmentPathToNode,
extrudePathToNode,
programMemory
extrudePathToNode
)
if (err(updatedAst)) throw updatedAst
const newCode = recast(updatedAst.modifiedAst)

View File

@ -349,7 +349,6 @@ export function sketchOnExtrudedFace(
node: Program,
sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode,
programMemory: ProgramMemory,
cap: 'none' | 'start' | 'end' = 'none'
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node }
@ -388,7 +387,6 @@ export function sketchOnExtrudedFace(
if (cap === 'none') {
const __tag = addTagForSketchOnFace(
{
previousProgramMemory: programMemory,
pathToNode: sketchPathToNode,
node: _node,
},

View File

@ -0,0 +1,315 @@
import {
parse,
recast,
initPromise,
PathToNode,
Value,
Program,
CallExpression,
} from '../wasm'
import { addFillet, isTagUsedInFillet } from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests
})
const runFilletTest = async (
code: string,
segmentSnippet: string,
extrudeSnippet: string,
radius = createLiteral(5) as Value,
expectedCode: string
) => {
const astOrError = parse(code)
if (astOrError instanceof Error) {
return new Error('AST not found')
}
const ast = astOrError as Program
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
const pathToSegmentNode: PathToNode = getNodePathFromSourceRange(
ast,
segmentRange
)
const extrudeRange: [number, number] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
]
const pathToExtrudeNode: PathToNode = getNodePathFromSourceRange(
ast,
extrudeRange
)
if (pathToExtrudeNode instanceof Error) {
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Value
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (result instanceof Error) {
return result
}
const { modifiedAst } = result
const newCode = recast(modifiedAst)
expect(newCode).toContain(expectedCode)
}
describe('Testing addFillet', () => {
/**
* 1. Ideal Case
*/
it('should add a fillet to a specific segment after extrusion, clean', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 2. Case of existing tag in the other line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in any other line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg02)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg02] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 3. Case of existing tag in the fillet line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in that exact line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([-87.24, -47.08], %, $seg03)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg03] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 4. Case of existing fillet on some other segment
*/
it('should add another fillet after the existing fillet', async () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
})
describe('Testing isTagUsedInFillet', () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.72, 4.13], %)
|> line([7.11, 3.48], %, $seg01)
|> line([-3.29, -13.85], %)
|> line([-6.37, 3.88], %, $seg02)
|> close(%)
const extrude001 = extrude(-5, sketch001)
|> fillet({
radius: 1.11,
tags: [
getOppositeEdge(seg01, %),
seg01,
getPreviousAdjacentEdge(seg02, %)
]
}, %)
`
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual([])
})
})

View File

@ -0,0 +1,405 @@
import {
ArrayExpression,
CallExpression,
ObjectExpression,
PathToNode,
Program,
Value,
VariableDeclaration,
VariableDeclarator,
} from '../wasm'
import {
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
createIdentifier,
createPipeExpression,
} from '../modifyAst'
import {
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
traverse,
} from '../queryAst'
import {
addTagForSketchOnFace,
getTagFromCallExpression,
sketchLineHelperMap,
} from '../std/sketch'
import { err } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
// import { forEach } from 'jszip'
export function addFillet(
node: Program,
pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Value
// shouldPipe = false, // TODO: Implement this feature
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// close ast to make mutations safe
let _node: Program = JSON.parse(JSON.stringify(node))
/**
* Add Tag to the Segment Expression
*/
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
_node,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment')
}
// Add tag to the sketch segment or use existing tag
const taggedSegment = addTagForSketchOnFace(
{
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode,
node: _node,
},
sketchSegmentNode.callee.name
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment
/**
* Find Extrude Expression automatically
*/
// 1. Get the sketch name
/**
* Add Fillet to the Extrude expression
*/
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
tags: createArrayExpression([createIdentifier(tag)]),
}),
createPipeSubstitution(),
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
// CallExpression - no fillet
// PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = (
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode => {
let pathToFilletObj: any
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
const hasTag = node.properties.some((prop) => {
const isTagProp = prop.key.name === 'tags'
if (isTagProp && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
if (extrudeInit.type === 'CallExpression') {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
return new Error('Fillet CallExpression not found.')
}
// check if the existing fillet has the same tag as the new fillet
let filletTag = null
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
if (filletTag !== tag) {
extrudeInit.body.push(filletCall)
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
}
} else {
return new Error('Unsupported extrude type.')
}
return new Error('Unsupported extrude type.')
}
export const hasValidFilletSelection = ({
selectionRanges,
ast,
code,
}: {
selectionRanges: Selections
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'extrude') {
extrudeExists = true
}
},
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
return false
}
}
return canFilletSelection(selectionRanges)
}
type EdgeTypes =
| 'baseEdge'
| 'getNextAdjacentEdge'
| 'getPreviousAdjacentEdge'
| 'getOppositeEdge'
export const isTagUsedInFillet = ({
ast,
callExp,
}: {
ast: Program
callExp: CallExpression
}): Array<EdgeTypes> => {
const tag = getTagFromCallExpression(callExp)
if (err(tag)) return []
let inFillet = false
let inObj = false
let inTagHelper: EdgeTypes | '' = ''
const edges: Array<EdgeTypes> = []
traverse(ast, {
enter: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = node.callee.name
}
if (
inObj &&
inFillet &&
!inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push('baseEdge')
}
if (
inObj &&
inFillet &&
inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push(inTagHelper)
}
},
leave: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = ''
}
},
})
return edges
}

View File

@ -302,6 +302,30 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream
freezeFrame: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceEvent
) {}
onIceCandidateError = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceErrorEvent
) {}
onConnectionStateChange = function (this: RTCPeerConnection, event: Event) {}
onDataChannelOpen = function (this: RTCDataChannel, event: Event) {}
onDataChannelClose = function (this: RTCDataChannel, event: Event) {}
onDataChannelError = function (this: RTCDataChannel, event: Event) {}
onDataChannelMessage = function (this: RTCDataChannel, event: MessageEvent) {}
onDataChannel = function (
this: RTCPeerConnection,
event: RTCDataChannelEvent
) {}
onTrack = function (this: RTCPeerConnection, event: RTCTrackEvent) {}
onWebSocketOpen = function (event: Event) {}
onWebSocketClose = function (event: Event) {}
onWebSocketError = function (event: Event) {}
onWebSocketMessage = function (event: MessageEvent) {}
onNetworkStatusReady = () => {}
private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh,
}
@ -346,6 +370,7 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval>
constructor({
engineCommandManager,
@ -368,7 +393,7 @@ class EngineConnection extends EventTarget {
// Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// happens on mouse move
setInterval(() => {
this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return
switch (this.state.type as EngineConnectionStateType) {
@ -434,6 +459,44 @@ class EngineConnection extends EventTarget {
tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
this.disconnectAll()
clearInterval(this.pingIntervalId)
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener('icecandidateerror', this.onIceCandidateError)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener('message', this.onWebSocketMessage)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit },
@ -477,7 +540,7 @@ class EngineConnection extends EventTarget {
},
}
this.pc.addEventListener('icecandidate', (event) => {
this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate === null) {
return
}
@ -499,18 +562,20 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined,
},
})
})
}
this.pc.addEventListener('icecandidate', this.onIceCandidate)
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
})
}
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => {
this.onConnectionStateChange = (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and
@ -539,9 +604,13 @@ class EngineConnection extends EventTarget {
default:
break
}
})
}
this.pc.addEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc.addEventListener('track', (event) => {
this.onTrack = (event) => {
const mediaStream = event.streams[0]
this.state = {
@ -625,9 +694,10 @@ class EngineConnection extends EventTarget {
// to pass it to the rest of the application.
this.mediaStream = mediaStream
})
}
this.pc.addEventListener('track', this.onTrack)
this.pc.addEventListener('datachannel', (event) => {
this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel
this.state = {
@ -638,7 +708,7 @@ class EngineConnection extends EventTarget {
},
}
this.unreliableDataChannel.addEventListener('open', (event) => {
this.onDataChannelOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -654,14 +724,22 @@ class EngineConnection extends EventTarget {
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
)
})
}
this.unreliableDataChannel?.addEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel.addEventListener('close', (event) => {
this.onDataChannelClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.unreliableDataChannel?.addEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel.addEventListener('error', (event) => {
this.onDataChannelError = (event) => {
this.disconnectAll()
this.state = {
@ -674,8 +752,13 @@ class EngineConnection extends EventTarget {
},
},
}
})
this.unreliableDataChannel.addEventListener('message', (event) => {
}
this.unreliableDataChannel?.addEventListener(
'error',
this.onDataChannelError
)
this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || {}
@ -697,8 +780,13 @@ class EngineConnection extends EventTarget {
}
}
)
})
})
}
this.unreliableDataChannel.addEventListener(
'message',
this.onDataChannelMessage
)
}
this.pc.addEventListener('datachannel', this.onDataChannel)
}
const createWebSocketConnection = () => {
@ -712,7 +800,7 @@ class EngineConnection extends EventTarget {
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => {
this.onWebSocketOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -733,14 +821,16 @@ class EngineConnection extends EventTarget {
// Send an initial ping
this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
})
}
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket.addEventListener('close', (event) => {
this.onWebSocketClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.websocket.addEventListener('close', this.onWebSocketClose)
this.websocket.addEventListener('error', (event) => {
this.onWebSocketError = (event) => {
this.disconnectAll()
this.state = {
@ -753,9 +843,10 @@ class EngineConnection extends EventTarget {
},
},
}
})
}
this.websocket.addEventListener('error', this.onWebSocketError)
this.websocket.addEventListener('message', (event) => {
this.onWebSocketMessage = (event) => {
// In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer
@ -960,15 +1051,20 @@ class EngineConnection extends EventTarget {
})
break
}
})
}
this.websocket.addEventListener('message', this.onWebSocketMessage)
}
if (reconnecting) {
createWebSocketConnection()
} else {
window.addEventListener('use-network-status-ready', () => {
this.onNetworkStatusReady = () => {
createWebSocketConnection()
})
}
window.addEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
}
}
// Do not change this back to an object or any, we should only be sending the
@ -1154,7 +1250,15 @@ export class EngineCommandManager extends EventTarget {
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
private onEngineConnectionStarted = ({ detail: engineConnection }: any) => {}
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
start({
restart,
setMediaStream,
setIsStreamReady,
width,
@ -1170,6 +1274,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false,
},
}: {
restart?: boolean
setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void
width: number
@ -1215,162 +1320,168 @@ export class EngineCommandManager extends EventTarget {
})
)
this.onEngineConnectionOpened = () => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.engineConnection.addEventListener(
EngineConnectionEvents.Opened,
() => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.onEngineConnectionOpened
)
this.onEngineConnectionClosed = () => {
setIsStreamReady(false)
}
this.engineConnection.addEventListener(
EngineConnectionEvents.Closed,
() => {
setIsStreamReady(false)
}
this.onEngineConnectionClosed
)
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
this.onEngineConnectionStarted = ({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
callback(result)
}
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
callback(result)
}
}
)
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
}
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
}
}) as EventListener)
}
}) as EventListener)
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
this.onEngineConnectionNewTrack = ({
detail: { mediaStream },
}: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
setMediaStream(mediaStream)
}) as EventListener
)
this.engineConnection?.connect()
setMediaStream(mediaStream)
}
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.connect()
}
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
}
@ -1629,7 +1740,26 @@ export class EngineCommandManager extends EventTarget {
}
tearDown() {
if (this.engineConnection) {
this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
this.onEngineConnectionOpened
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.Closed,
this.onEngineConnectionClosed
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown()
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is
// only really for tests.
// @ts-ignore

View File

@ -221,7 +221,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace(
{
previousProgramMemory: programMemory,
// previousProgramMemory: programMemory, // redundant?
pathToNode,
node: ast,
},

View File

@ -28,7 +28,6 @@ import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import {
SketchLineHelper,
ModifyAstBase,
TransformCallback,
ConstrainInfo,
RawValues,
@ -37,6 +36,7 @@ import {
SingleValueInput,
VarValueKeys,
ArrayOrObjItemInput,
AddTagInfo,
} from 'lang/std/stdTypes'
import {
@ -308,6 +308,18 @@ function singleRawValueHelper(
]
}
function getTag(index = 2): SketchLineHelper['getTag'] {
return (callExp: CallExpression) => {
if (callExp.type !== 'CallExpression')
return new Error('Not a CallExpression')
const arg = callExp.arguments?.[index]
if (!arg) return new Error('No argument')
if (arg.type !== 'TagDeclarator')
return new Error('Tag not a TagDeclarator')
return arg.value
}
}
export const lineTo: SketchLineHelper = {
add: ({
node,
@ -377,6 +389,7 @@ export const lineTo: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -503,6 +516,7 @@ export const line: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -563,6 +577,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper(
@ -623,6 +638,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper(
@ -682,6 +698,7 @@ export const xLine: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper(
@ -738,6 +755,7 @@ export const yLine: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper(
@ -830,6 +848,7 @@ export const tangentialArcTo: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return []
@ -948,6 +967,7 @@ export const angledLine: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -1044,6 +1064,7 @@ export const angledLineOfXLength: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -1140,6 +1161,7 @@ export const angledLineOfYLength: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -1227,6 +1249,7 @@ export const angledLineToX: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -1316,6 +1339,7 @@ export const angledLineToY: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
@ -1440,6 +1464,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return []
@ -1792,10 +1817,7 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode }
}
export function addTagForSketchOnFace(
a: ModifyAstBase,
expressionName: string
) {
export function addTagForSketchOnFace(a: AddTagInfo, expressionName: string) {
if (expressionName === 'close') {
return addTag(1)(a)
}
@ -1806,6 +1828,17 @@ export function addTagForSketchOnFace(
return new Error(`"${expressionName}" is not a sketch line helper`)
}
export function getTagFromCallExpression(
callExp: CallExpression
): string | Error {
if (callExp.callee.name === 'close') return getTag(1)(callExp)
if (callExp.callee.name in sketchLineHelperMap) {
const { getTag } = sketchLineHelperMap[callExp.callee.name]
return getTag(callExp)
}
return new Error(`"${callExp.callee.name}" is not a sketch line helper`)
}
function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression'
? isLiteralArrayOrStatic(lineArugement.elements[0])
@ -1816,9 +1849,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
: false
}
type addTagFn = (
a: ModifyAstBase
) => { modifiedAst: Program; tag: string } | Error
type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => {

View File

@ -32,6 +32,11 @@ export interface ModifyAstBase {
pathToNode: PathToNode
}
export interface AddTagInfo {
node: Program
pathToNode: PathToNode
}
interface addCall extends ModifyAstBase {
to: [number, number]
from: [number, number]
@ -127,7 +132,8 @@ export interface SketchLineHelper {
pathToNode: PathToNode
}
| Error
addTag: (a: ModifyAstBase) =>
getTag: (a: CallExpression) => string | Error
addTag: (a: AddTagInfo) =>
| {
modifiedAst: Program
tag: string

View File

@ -1,9 +1,9 @@
import { CommandSetConfig } from 'lib/commandTypes'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine'
type AuthCommandSchema = {}
export const authCommandBarConfig: CommandSetConfig<
export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine,
AuthCommandSchema
> = {

View File

@ -1,4 +1,4 @@
import { CommandSetConfig } from 'lib/commandTypes'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { homeMachine } from 'machines/homeMachine'
export type HomeCommandSchema = {
@ -17,7 +17,7 @@ export type HomeCommandSchema = {
}
}
export const homeCommandBarConfig: CommandSetConfig<
export const homeCommandBarConfig: StateMachineCommandSetConfig<
typeof homeMachine,
HomeCommandSchema
> = {

View File

@ -1,8 +1,8 @@
import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
@ -27,9 +27,17 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
Fillet: {
// todo
selection: Selections
radius: KclCommandValue
}
'change tool': {
tool: SketchTool
}
}
export const modelingMachineConfig: CommandSetConfig<
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
typeof modelingMachine,
ModelingCommandSchema
> = {
@ -37,22 +45,47 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.',
icon: 'sketch',
},
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc
// 'Equip Line tool': {
// description: 'Start drawing straight lines.',
// icon: 'line',
// displayName: 'Line',
// },
// 'Equip tangential arc to': {
// description: 'Start drawing an arc tangent to the current segment.',
// icon: 'arc',
// displayName: 'Tangential Arc',
// },
// 'Equip rectangle tool': {
// description: 'Start drawing a rectangle.',
// icon: 'rectangle',
// displayName: 'Rectangle',
// },
'change tool': [
{
description: 'Start drawing straight lines.',
icon: 'line',
displayName: 'Line',
args: {
tool: {
defaultValue: 'line',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
args: {
tool: {
defaultValue: 'tangentialArc',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
args: {
tool: {
defaultValue: 'rectangle',
required: true,
skip: true,
inputType: 'string',
},
},
},
],
Export: {
description: 'Export the current model.',
icon: 'exportFile',
@ -157,4 +190,36 @@ export const modelingMachineConfig: CommandSetConfig<
},
},
},
Fillet: {
// todo
description: 'Fillet edge',
icon: 'fillet',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: [
'default',
'line-end',
'line-mid',
'extrude-wall', // to fix: accespts only this selection type
'start-cap',
'end-cap',
'point',
'edge',
'line',
'arc',
'all',
],
multiple: true, // TODO: multiple selection like in extrude command
required: true,
skip: true,
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
}

View File

@ -124,7 +124,8 @@ export function createSettingsCommand({
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ',
})}`,
ownerMachine: 'settings',
description: settingConfig.description,
groupId: 'settings',
icon: 'settings',
needsReview: false,
onSubmit: (data) => {

View File

@ -33,13 +33,13 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = (typeof INPUT_TYPES)[number]
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
export type StateMachineCommandSetSchema<T extends AnyStateMachine> = Partial<{
[EventType in EventFrom<T>['type']]: Record<string, any>
}>
export type CommandSet<
export type StateMachineCommandSet<
T extends AllMachines,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: Command<
T,
@ -48,24 +48,28 @@ export type CommandSet<
>
}>
export type CommandSetConfig<
/**
* A configuration object for a set of commands tied to a state machine.
* Each event type can have one or more commands associated with it.
* @param T The state machine type.
* @param Schema The schema for the command set, defined by the developer.
*/
export type StateMachineCommandSetConfig<
T extends AllMachines,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: CommandConfig<
T,
EventFrom<T>['type'],
Schema[EventType]
>
[EventType in EventFrom<T>['type']]:
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>[]
}>
export type Command<
T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = {
name: CommandName
ownerMachine: T['id']
groupId: T['id']
needsReview: boolean
onSubmit: (data?: CommandSchema) => void
onCancel?: () => void
@ -81,10 +85,10 @@ export type Command<
export type CommandConfig<
T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = Omit<
Command<T, CommandName, CommandSchema>,
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & {
needsReview?: true
args?: {

View File

@ -51,6 +51,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch',
EXTRUDE: 'extrude',
SEGMENT: 'seg',
} as const
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`

View File

@ -11,20 +11,20 @@ import {
CommandArgument,
CommandArgumentConfig,
CommandConfig,
CommandSetConfig,
CommandSetSchema,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from './commandTypes'
interface CreateMachineCommandProps<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
> {
type: EventFrom<T>['type']
ownerMachine: T['id']
groupId: T['id']
state: StateFrom<T>
send: Function
actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void
}
@ -32,22 +32,39 @@ interface CreateMachineCommandProps<
// from a more terse Command Bar Meta definition.
export function createMachineCommand<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
>({
ownerMachine,
groupId,
type,
state,
send,
actor,
commandBarConfig,
onCancel,
}: CreateMachineCommandProps<T, S>): Command<
T,
typeof type,
S[typeof type]
> | null {
}: CreateMachineCommandProps<T, S>):
| Command<T, typeof type, S[typeof type]>
| Command<T, typeof type, S[typeof type]>[]
| null {
const commandConfig = commandBarConfig && commandBarConfig[type]
if (!commandConfig) return null
// There may be no command config for this event type,
// or there may be multiple commands to create.
if (!commandConfig) {
return null
} else if (commandConfig instanceof Array) {
return commandConfig
.map((config) =>
createMachineCommand({
groupId,
type,
state,
send,
actor,
commandBarConfig: { [type]: config },
onCancel,
})
)
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
}
// Hide commands based on platform by returning `null`
// so the consumer can filter them out
@ -62,8 +79,9 @@ export function createMachineCommand<
const command: Command<T, typeof type, S[typeof type]> = {
name: type,
ownerMachine: ownerMachine,
groupId,
icon,
description: commandConfig.description,
needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) {
@ -84,6 +102,10 @@ export function createMachineCommand<
command.onCancel = onCancel
}
if ('displayName' in commandConfig) {
command.displayName = commandConfig.displayName
}
return command
}
@ -92,7 +114,7 @@ export function createMachineCommand<
// bundled together into the args for a Command.
function buildCommandArguments<
T extends AnyStateMachine,
S extends CommandSetSchema<T>,
S extends StateMachineCommandSetSchema<T>,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
>(
state: StateFrom<T>,
@ -112,7 +134,7 @@ function buildCommandArguments<
export function buildCommandArgument<
T extends AnyStateMachine,
O extends CommandSetSchema<T> = CommandSetSchema<T>
O extends StateMachineCommandSetSchema<T> = StateMachineCommandSetSchema<T>
>(
arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>,

View File

@ -406,6 +406,17 @@ export function canExtrudeSelection(selection: Selections) {
)
}
export function canFilletSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER
return (
!!isSketchPipe(selection) &&
commonNodes.every((n) => nodeHasClose(n)) &&
commonNodes.every((n) => !nodeHasExtrude(n))
)
}
function canExtrudeSelectionItem(selection: Selections, i: number) {
const commonNode = buildCommonNodeFromSelection(selection, i)

View File

@ -37,4 +37,7 @@ if (typeof window !== 'undefined') {
document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
)
;(window as any).enableFillet = () => {
;(window as any)._enableFillet = true
}
}

View File

@ -6,7 +6,7 @@ import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers'
import { trap } from 'lib/trap'
import { err, trap } from 'lib/trap'
const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -86,6 +86,7 @@ export function useCalculateKclExpression({
const execAstAndSetResult = async () => {
const _code = `const __result__ = ${value}`
const ast = parse(_code)
if (err(ast)) return
if (trap(ast, { suppress: true })) return
const _programMem: any = { root: {}, return: null }

View File

@ -57,7 +57,7 @@ export type CommandBarMachineEvent =
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
data: { name: string; groupId: string }
}
| {
type: 'Change current argument'
@ -66,7 +66,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
predictableActionArguments: true,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: {
@ -120,9 +120,7 @@ export const commandBarMachine = createMachine(
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) =>
c2.name === c.name &&
c2.ownerMachine === c.ownerMachine
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
@ -149,6 +147,10 @@ export const commandBarMachine = createMachine(
cond: 'Command has no arguments',
actions: ['Execute command'],
},
{
target: 'Checking Arguments',
cond: 'All arguments are skippable',
},
{
target: 'Gathering arguments',
actions: ['Set current argument to first non-skippable'],
@ -201,7 +203,7 @@ export const commandBarMachine = createMachine(
'Change current argument': {
target: 'Gathering arguments',
internal: true,
actions: ['Remove current argument and set a new one'],
actions: ['Set current argument'],
},
'Deselect command': {
@ -357,29 +359,13 @@ export const commandBarMachine = createMachine(
switch (event.type) {
case 'Edit argument':
return event.data.arg
case 'Change current argument':
return Object.values(event.data)[0]
default:
return context.currentArgument
}
},
}),
'Remove current argument and set a new one': assign({
argumentsToSubmit: (context, event) => {
if (
event.type !== 'Change current argument' ||
!context.currentArgument
)
return context.argumentsToSubmit
const { name } = context.currentArgument
const { [name]: _, ...rest } = context.argumentsToSubmit
return rest
},
currentArgument: (context, event) => {
if (event.type !== 'Change current argument')
return context.currentArgument
return Object.values(event.data)[0]
},
}),
'Clear argument data': assign({
selectedCommand: undefined,
currentArgument: undefined,
@ -393,9 +379,7 @@ export const commandBarMachine = createMachine(
selectedCommand: (c, e) => {
if (e.type !== 'Find and select command') return c.selectedCommand
const found = c.commands.find(
(cmd) =>
cmd.name === e.data.name &&
cmd.ownerMachine === e.data.ownerMachine
(cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
)
return !!found ? found : c.selectedCommand
@ -514,7 +498,9 @@ export const commandBarMachine = createMachine(
)
function sortCommands(a: Command, b: Command) {
if (b.ownerMachine === 'auth') return -1
if (a.ownerMachine === 'auth') return 1
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
if (a.groupId === 'auth' && !(b.groupId === 'auth')) return 2
if (b.groupId === 'settings' && !(a.groupId === 'settings')) return -1
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
return a.name.localeCompare(b.name)
}

File diff suppressed because one or more lines are too long

View File

@ -169,7 +169,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -180,7 +180,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -191,7 +191,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -431,7 +431,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -631,7 +631,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -642,7 +642,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -697,7 +697,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"synstructure",
]
@ -726,7 +726,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -737,7 +737,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -764,7 +764,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -936,7 +936,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1026,7 +1026,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1448,7 +1448,7 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1824,7 +1824,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"structmeta",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -1842,7 +1842,7 @@ dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml",
@ -1877,7 +1877,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2041,7 +2041,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2054,7 +2054,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2516,7 +2516,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2590,7 +2590,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2601,7 +2601,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2624,7 +2624,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2645,7 +2645,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2782,7 +2782,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2793,7 +2793,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2837,9 +2837,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.70"
version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [
"proc-macro2",
"quote",
@ -2860,7 +2860,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -2928,22 +2928,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3039,7 +3039,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3192,7 +3192,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3220,7 +3220,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3297,7 +3297,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"termcolor",
]
@ -3455,7 +3455,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]
@ -3516,7 +3516,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"wasm-bindgen-shared",
]
@ -3551,7 +3551,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3876,7 +3876,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.70",
"syn 2.0.71",
]
[[package]]

View File

@ -20,7 +20,7 @@ quote = "1"
regex = "1.10"
serde = { version = "1.0.204", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.70", features = ["full"] }
syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.86"

View File

@ -837,7 +837,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
// Read the output file.
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 1.0);
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #test_name_str), &actual, 0.99);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_someFn {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual,
1.0,
0.99,
);
}
@ -176,7 +176,7 @@ mod test_examples_show {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show1"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -88,7 +88,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func0"),
&actual,
1.0,
0.99,
);
}
@ -178,7 +178,7 @@ mod test_examples_my_func {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_my_func1"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -88,7 +88,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to0"),
&actual,
1.0,
0.99,
);
}
@ -178,7 +178,7 @@ mod test_examples_line_to {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_line_to1"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_min {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min0"),
&actual,
1.0,
0.99,
);
}
@ -176,7 +176,7 @@ mod test_examples_min {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_min1"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_import {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_import0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -86,7 +86,7 @@ mod test_examples_show {
twenty_twenty::assert_image(
&format!("tests/outputs/{}.png", "serial_test_example_show0"),
&actual,
1.0,
0.99,
);
}
}

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.70", features = ["full"] }
syn = { version = "2.0.71", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@ -35,7 +35,7 @@ schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"]
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
sha2 = "0.10.8"
thiserror = "1.0.61"
thiserror = "1.0.62"
toml = "0.8.14"
ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }
@ -98,6 +98,10 @@ harness = false
name = "compiler_benchmark_iai"
harness = false
[[bench]]
name = "digest_benchmark"
harness = false
[[bench]]
name = "lsp_semantic_tokens_benchmark_criterion"
harness = false

View File

@ -0,0 +1,31 @@
use criterion::{criterion_group, criterion_main, Criterion};
pub fn bench_digest(c: &mut Criterion) {
for (name, file) in [
("pipes_on_pipes", PIPES_PROGRAM),
("big_kitt", KITT_PROGRAM),
("cube", CUBE_PROGRAM),
("math", MATH_PROGRAM),
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
] {
let tokens = kcl_lib::token::lexer(file).unwrap();
let prog = kcl_lib::parser::Parser::new(tokens).ast().unwrap();
c.bench_function(&format!("digest_{name}"), move |b| {
let prog = prog.clone();
b.iter(move || {
let mut prog = prog.clone();
prog.compute_digest();
});
});
}
}
criterion_group!(benches, bench_digest);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

@ -4,6 +4,10 @@ use kcl_lib::{
settings::types::UnitLength,
};
/// The minimum permissible difference between asserted twenty-twenty images.
/// i.e. how different the current model snapshot can be from the previous saved one.
const MIN_DIFF: f64 = 0.99;
// mod server;
async fn new_context(units: UnitLength) -> Result<ExecutorContext> {
@ -83,35 +87,35 @@ const part002 = startSketchOn(part001, here)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_riddle_small() {
let code = include_str!("inputs/riddle_small.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_lego() {
let code = include_str!("inputs/lego.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_pipe_as_arg() {
let code = include_str!("inputs/pipe_as_arg.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipe_as_arg.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/pipe_as_arg.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_pentagon_fillet_sugar() {
let code = include_str!("inputs/pentagon_fillet_sugar.kcl");
let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_sugar.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_sugar.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -139,14 +143,14 @@ const part002 = startSketchOn(part001, "start")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_mike_stress_lines() {
let code = include_str!("inputs/mike_stress_test.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/mike_stress_test.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/mike_stress_test.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -174,7 +178,7 @@ const part002 = startSketchOn(part001, "END")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -205,7 +209,7 @@ const part002 = startSketchOn(part001, "END")
twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_end_negative_extrude.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -242,7 +246,7 @@ async fn serial_test_basic_fillet_cube_start() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -259,7 +263,7 @@ async fn serial_test_basic_fillet_cube_end() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -279,7 +283,7 @@ async fn serial_test_basic_fillet_cube_close_opposite() {
twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_close_opposite.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -299,7 +303,7 @@ async fn serial_test_basic_fillet_cube_next_adjacent() {
twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_next_adjacent.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -319,7 +323,7 @@ async fn serial_test_basic_fillet_cube_previous_adjacent() {
twenty_twenty::assert_image(
"tests/executor/outputs/basic_fillet_cube_previous_adjacent.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -341,7 +345,7 @@ const fnBox = box(3, 6, 10)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -364,7 +368,7 @@ const thing = box([0,0], 3, 6, 10)"#;
twenty_twenty::assert_image(
"tests/executor/outputs/function_sketch_with_position.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -382,7 +386,7 @@ async fn serial_test_execute_with_angled_line() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -408,7 +412,7 @@ const bracket = startSketchOn('XY')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -442,7 +446,7 @@ const bracket = startSketchAt([0, 0])
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -470,7 +474,7 @@ async fn serial_test_execute_i_shape() {
let code = include_str!("inputs/i_shape.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/i_shape.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/i_shape.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -479,7 +483,7 @@ async fn serial_test_execute_pipes_on_pipes() {
let code = include_str!("inputs/pipes_on_pipes.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -487,7 +491,7 @@ async fn serial_test_execute_cylinder() {
let code = include_str!("inputs/cylinder.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cylinder.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/cylinder.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -495,7 +499,7 @@ async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -523,7 +527,7 @@ const pt2 = b2.value[0]
twenty_twenty::assert_image(
"tests/executor/outputs/member_expression_sketch_group.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -536,7 +540,7 @@ async fn serial_test_helix_defaults() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_defaults.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/helix_defaults.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -551,7 +555,7 @@ async fn serial_test_helix_defaults_negative_extrude() {
twenty_twenty::assert_image(
"tests/executor/outputs/helix_defaults_negative_extrude.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -564,7 +568,7 @@ async fn serial_test_helix_ccw() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_ccw.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/helix_ccw.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -576,7 +580,7 @@ async fn serial_test_helix_with_length() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_with_length.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/helix_with_length.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -591,7 +595,7 @@ async fn serial_test_dimensions_match() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/dimensions_match.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/dimensions_match.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -608,7 +612,7 @@ const body = startSketchOn('XY')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -634,7 +638,7 @@ let thing = box(-12, -15, 10)
box(-20, -5, 10)"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -647,7 +651,7 @@ async fn serial_test_basic_tangential_arc() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -660,7 +664,11 @@ async fn serial_test_basic_tangential_arc_with_point() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_with_point.png", &result, 0.999);
twenty_twenty::assert_image(
"tests/executor/outputs/tangential_arc_with_point.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -673,7 +681,7 @@ async fn serial_test_basic_tangential_arc_to() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -703,7 +711,7 @@ box(-20, -5, 10, 'xy')"#;
twenty_twenty::assert_image(
"tests/executor/outputs/different_planes_same_drawing.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -762,7 +770,7 @@ const part004 = startSketchOn('YZ')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -779,7 +787,7 @@ async fn serial_test_holes() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -798,7 +806,7 @@ async fn optional_params() {
const thing = other_circle([2, 2], 20)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -834,7 +842,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -842,7 +850,7 @@ async fn serial_test_top_level_expression() {
let code = r#"startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -859,7 +867,7 @@ const part = startSketchOn('XY')
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_with_math.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -872,7 +880,7 @@ async fn serial_test_patterns_linear_basic() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -888,7 +896,7 @@ async fn serial_test_patterns_linear_basic_3d() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -903,7 +911,7 @@ async fn serial_test_patterns_linear_basic_negative_distance() {
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_distance.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -919,7 +927,7 @@ async fn serial_test_patterns_linear_basic_negative_axis() {
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_axis.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -941,7 +949,11 @@ const rectangle = startSketchOn('XY')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999);
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_holes.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -953,7 +965,11 @@ async fn serial_test_patterns_circular_basic_2d() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999);
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_basic_2d.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -969,7 +985,11 @@ async fn serial_test_patterns_circular_basic_3d() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999);
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_basic_3d.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -988,7 +1008,7 @@ async fn serial_test_patterns_circular_3d_tilted_axis() {
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_3d_tilted_axis.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -1009,7 +1029,7 @@ async fn serial_test_import_obj_with_mtl() {
let code = r#"const model = import("tests/executor/inputs/cube.obj")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1017,7 +1037,11 @@ async fn serial_test_import_obj_with_mtl_units() {
let code = r#"const model = import("tests/executor/inputs/cube.obj", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl_units.png", &result, 0.999);
twenty_twenty::assert_image(
"tests/executor/outputs/import_obj_with_mtl_units.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1025,7 +1049,7 @@ async fn serial_test_import_gltf_with_bin() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1033,7 +1057,7 @@ async fn serial_test_import_gltf_embedded() {
let code = r#"const model = import("tests/executor/inputs/cube-embedded.gltf")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1041,7 +1065,7 @@ async fn serial_test_import_glb() {
let code = r#"const model = import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1049,7 +1073,7 @@ async fn serial_test_import_glb_no_assign() {
let code = r#"import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1082,7 +1106,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_mm.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_mm.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1103,7 +1127,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::Cm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_cm.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_cm.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1124,7 +1148,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::M).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_m.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_m.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1145,7 +1169,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::In).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_in.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_in.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1166,7 +1190,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::Ft).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_ft.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_ft.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1187,7 +1211,7 @@ const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, UnitLength::Yd).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1256,7 +1280,7 @@ const part003 = startSketchOn(part002, "end")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1301,7 +1325,7 @@ const part002 = startSketchOn(part001, "end")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1325,7 +1349,11 @@ const part002 = startSketchOn(part001, "end")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle_tagged.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_circle_tagged.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1391,7 +1419,7 @@ async fn serial_test_big_number_angle_to_match_length_x() {
twenty_twenty::assert_image(
"tests/executor/outputs/big_number_angle_to_match_length_x.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -1412,7 +1440,7 @@ async fn serial_test_big_number_angle_to_match_length_y() {
twenty_twenty::assert_image(
"tests/executor/outputs/big_number_angle_to_match_length_y.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -1433,7 +1461,7 @@ async fn serial_test_simple_revolve() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1453,7 +1481,7 @@ async fn serial_test_simple_revolve_uppercase() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_uppercase.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_uppercase.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1473,7 +1501,7 @@ async fn serial_test_simple_revolve_negative() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_negative.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_negative.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1543,7 +1571,11 @@ async fn serial_test_simple_revolve_custom_angle() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_angle.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_custom_angle.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1563,7 +1595,11 @@ async fn serial_test_simple_revolve_custom_axis() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_axis.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_custom_axis.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1587,7 +1623,7 @@ const sketch001 = startSketchOn(box, "end")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_edge.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_edge.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1638,7 +1674,11 @@ const sketch001 = startSketchOn(box, "END")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle_edge.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/revolve_on_face_circle_edge.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1660,7 +1700,7 @@ const sketch001 = startSketchOn(box, "END")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1686,7 +1726,7 @@ const sketch001 = startSketchOn(box, "end")
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1700,7 +1740,7 @@ async fn serial_test_basic_revolve_circle() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_revolve_circle.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/basic_revolve_circle.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1727,7 +1767,11 @@ const part002 = startSketchOn(part001, 'end')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_sketch_on_edge.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/simple_revolve_sketch_on_edge.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1788,7 +1832,7 @@ const plumbus1 = make_circle(p, b,$arc_b, [0, 0], 2.5)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/plumbus_fillets.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/plumbus_fillets.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1826,7 +1870,11 @@ capScrew([0, 0.5, 0], 50, 37.5, 50, 25)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/member_expression_in_params.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/member_expression_in_params.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -1944,7 +1992,7 @@ async fn serial_test_xz_plane() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/xz_plane.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/xz_plane.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1958,7 +2006,7 @@ async fn serial_test_neg_xz_plane() {
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -1985,7 +2033,11 @@ const pattn2 = patternLinear3d({
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/linear_pattern3d_a_pattern.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/linear_pattern3d_a_pattern.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -2008,7 +2060,11 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/circular_pattern3d_a_pattern.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_a_pattern.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -2037,7 +2093,7 @@ extrude(10, sketch001)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/array_of_sketches.png", &result, 1.0);
twenty_twenty::assert_image("tests/executor/outputs/array_of_sketches.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -2103,7 +2159,7 @@ const sketch001 = startSketchOn(bracket, 'seg01')
twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_after_fillets_referencing_face.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2140,7 +2196,11 @@ const pattn1 = patternLinear3d({
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pattern3d_array_of_extrudes.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/pattern3d_array_of_extrudes.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -2191,7 +2251,7 @@ const baseExtrusion = extrude(width, sketch001)
twenty_twenty::assert_image(
"tests/executor/outputs/fillets_referencing_other_fillets.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2243,7 +2303,7 @@ const baseExtrusion = extrude(width, sketch001)
twenty_twenty::assert_image(
"tests/executor/outputs/chamfers_referencing_other_chamfers.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2302,7 +2362,7 @@ const pattn1 = patternLinear3d({
twenty_twenty::assert_image(
"tests/executor/outputs/linear_pattern3d_filleted_sketch.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2333,7 +2393,7 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_filleted_sketch.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2364,7 +2424,7 @@ const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repet
twenty_twenty::assert_image(
"tests/executor/outputs/circular_pattern3d_chamfered_sketch.png",
&result,
1.0,
MIN_DIFF,
);
}
@ -2429,7 +2489,11 @@ const sketch001 = startSketchOn(part001, 'chamfer1')
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_chamfer.png", &result, 1.0);
twenty_twenty::assert_image(
"tests/executor/outputs/sketch_on_face_of_chamfer.png",
&result,
MIN_DIFF,
);
}
#[tokio::test(flavor = "multi_thread")]
@ -2463,21 +2527,21 @@ let p = triangle(200)
async fn serial_test_global_tags() {
let code = include_str!("inputs/global-tags.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/global_tags.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/global_tags.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_pattern_vase() {
let code = include_str!("inputs/pattern_vase.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_scoped_tags() {
let code = include_str!("inputs/scoped-tags.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/scoped_tags.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/scoped_tags.png", &result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]
@ -2487,7 +2551,7 @@ async fn serial_test_order_sketch_extrude_in_order() {
twenty_twenty::assert_image(
"tests/executor/outputs/order-sketch-extrude-in-order.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -2498,7 +2562,7 @@ async fn serial_test_order_sketch_extrude_out_of_order() {
twenty_twenty::assert_image(
"tests/executor/outputs/order-sketch-extrude-out-of-order.png",
&result,
0.999,
MIN_DIFF,
);
}
@ -2506,5 +2570,5 @@ async fn serial_test_order_sketch_extrude_out_of_order() {
async fn serial_test_extrude_custom_plane() {
let code = include_str!("inputs/extrude-custom-plane.kcl");
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/extrude-custom-plane.png", &result, 0.999);
twenty_twenty::assert_image("tests/executor/outputs/extrude-custom-plane.png", &result, MIN_DIFF);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Some files were not shown because too many files have changed in this diff Show More