Compare commits

...

33 Commits

Author SHA1 Message Date
9d99b5be7f bump kcl (#4144) 2024-10-11 14:26:46 -04:00
85a39109f8 Don't mutate the user's codeBasedSelections when they apply a multi-selection constraint (#4141)
* Don't mutate the user's `codeBasedSelections` when they apply a multi-selection constraint

* Fix to not mutate the input parameter

* Format

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-11 12:53:33 -04:00
23c2aa948a Fix bug with selection order on multi-select constraints (#4138)
* Sort selections in order of appearance before applying `transformSecondarySketchLinesTagFirst`

* Add an integration test for this sorting behavior

* Remove console logs from test
2024-10-11 09:49:58 -04:00
1fd4aa9ede Upload release-notes.md on release (#4123)
* Upload release-notes.md on release
Contributes to #3984

* Test release builds with updater-test-release-notes

* Change to releaseNotesFile conf, tested locally

* Clean up for merge
2024-10-11 06:16:39 -04:00
e8a9fb7f55 Revert "Sort selections in order of appearance before applying transformSecondarySketchLinesTagFirst"
This reverts commit b1ccc6df0f.
2024-10-10 20:35:15 -04:00
cc4345b7c3 Revert "Add an integration test for this sorting behavior"
This reverts commit 6035e834c2.
2024-10-10 20:35:07 -04:00
6035e834c2 Add an integration test for this sorting behavior 2024-10-10 20:32:52 -04:00
b1ccc6df0f Sort selections in order of appearance before applying transformSecondarySketchLinesTagFirst 2024-10-10 20:32:35 -04:00
9563bd322c Bump js-sys from 0.3.70 to 0.3.71 in /src/wasm-lib (#4133)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.70 to 0.3.71.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  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-10-10 10:04:30 -07:00
1e35c03dc8 Bump wasm-bindgen-futures from 0.4.43 to 0.4.44 in /src/wasm-lib (#4132)
Bumps [wasm-bindgen-futures](https://github.com/rustwasm/wasm-bindgen) from 0.4.43 to 0.4.44.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: wasm-bindgen-futures
  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-10-10 10:04:13 -07:00
7caa0aff7b Add a loading toast when auto-updater download begins (#3995)
* Add a loading toast when update download begins

* Temporarily turn on artifact builds to test

* fmt

* Oops forgot to rename where I used a type

* Update src/index.tsx

* Update src/index.tsx

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

* Add an `update-error` listener

* Flatten autoupdater hooks

* Revert "Temporarily turn on artifact builds to test"

This reverts commit e2e1991977.

* Switch branch to build-publish-apps on to this one

* Make the loading toast not attempt to display a version number

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

* fmt

* Update .github/workflows/build-test-publish-apps.yml

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-10-10 12:16:45 -04:00
accbc1fc3b Remove a few E2E workflows for test setup that have proven flaky on Windows (#4135)
* Remove a few E2E workflows for test setup that have proven flaky on Windows

* fix lint
2024-10-10 11:38:24 -04:00
05b21f100c Update README.md (#4130)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-10-10 12:59:19 +13:00
0fb5ff7f10 Change artifact IDs to be stable across KCL executions (#4101)
* Add ID generator to ExecState

* Change default plane IDs to be hardcoded

* Fix lint warning

* Add exposing ID generator as output of executor

* Change to use generated definition of ExecState in TS

* Fix IdGenerator to use camel case in TS

* Fix TS type errors

* Add exposing id_generator parameter

* Add using the previously generated ID generator

* wip: Add display of feature tree in debug pane

* Remove artifact graph augmentation

* Change default planes to use id generator instead of hardcoded UUIDs

* Fix to reuse previously generated IDs

* Add e2e test

* Change feature tree to be collapsed by default

* Remove debug prints

* Fix unit test to use execState

* Fix type to be more general

* Remove outdated comment

* Update derive-docs output

* Fix object display component to be more general

* Remove unused ArtifactId type

* Fix test to be less brittle

* Remove codeRef and pathToNode from display

* Fix to remove test.only

Co-authored-by: Frank Noirot <frank@zoo.dev>

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

* Move plane conversion code to be next to type

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

This reverts commit 3455cc951b.

* Rename file

* Rename components and add doc comments

* Revive the collapse button

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

* Confirm

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

* Confirm

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

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

* Confirm

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-09 19:38:40 -04:00
e525b319d0 Bump clap from 4.5.19 to 4.5.20 in /src/wasm-lib (#4122)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.19 to 4.5.20.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.19...clap_complete-v4.5.20)

---
updated-dependencies:
- dependency-name: clap
  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-10-09 09:43:53 -07:00
01c6774c54 Nadro/2608/sketch mode scene state improvements (#3866)
* bug: fixing multiple state issues with the engine and modeling app to enable/disable planes/axis/delete code

* fix: yarn tsc fmt lint xgen

* fix: adding a comment back that I deleted on accident

* fix: adding formatting back?

* fix: reverting syntax

* fix: removing click line tool because the line tool is automatically selected. Clicking this will exit

* fix: Fixed a E2E test that had a line tool workflow with no points

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-09 09:33:20 -05:00
b745cec079 KCL docs: Better docs for KclValue (#4096)
* KCL docs: Better docs for KclValue

* Update docs

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-09 03:56:38 +00:00
90af99abf4 fix: added more documentation on the cut and release process (#4048)
* fix: added more documentation on the cut and release process

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-08 11:25:37 -05:00
max
3c5bf70269 Add Warning Message for Fillet Engine Limitations in CommandBar (#4076) 2024-10-08 16:27:58 +02:00
24cd1b2ea5 Reload user settings when changed externally (#4097)
* Reload user settings when changed externally

* Fix to not use any

* Make sure listener doesn't already exist

* Fix up projects reloading

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-07 23:07:18 -04:00
7de0b74c16 Cut release v0.25.6 (#4111)
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-10-07 20:46:55 -04:00
e5c20debfe Revert "Split artifacts per arch and re-enable updater for nightly builds" (#4114)
Revert "Split artifacts per arch and re-enable updater for nightly builds (#3…"

This reverts commit 9ca49c6366.
2024-10-07 19:28:02 -04:00
2de3ad7457 Bump once_cell from 1.20.1 to 1.20.2 in /src/wasm-lib (#4106)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.20.1 to 1.20.2.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.20.1...v1.20.2)

---
updated-dependencies:
- dependency-name: once_cell
  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-10-07 15:13:12 -07:00
9038dc4104 Bump futures from 0.3.30 to 0.3.31 in /src/wasm-lib (#4108)
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.30 to 0.3.31.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.30...0.3.31)

---
updated-dependencies:
- dependency-name: futures
  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-10-07 13:49:15 -04:00
1491e80153 Disable msi builds for now (#4084)
Fixes #4083
2024-10-07 05:10:29 -04:00
bdf45f92aa link to download in readme (#4100) 2024-10-04 14:27:54 -07:00
d104ca2b05 Add menu item and hotkey to center view on current selection (#4068)
* tentatively adding this

* Update src/components/ModelingMachineProvider.tsx

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Show shortcut in UI dialog

* Move command into modelingMachine action

* Add a menu item to the view menu

* Switch gizmo tests to use "deprecated" test setup in prep for new fixture-based test

* Add e2e test for center view to selection

* Bump @kittycad/lib to latest and fix tsc

* Bump @kittycad/lib to v2.0.7 to fix electron building

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

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

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

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-04 16:47:44 -04:00
ec8cacb788 KCL: Reduce can take and return any KCL values (#4094)
Previously it only took Array of Number and could only return Sketch.

Now it has been unshackled from the chains of poor type signatures.
2024-10-04 13:26:16 -05:00
4e0dd12f5a Update machine-api spec (#4091)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-04 08:34:06 -07:00
bcf2572739 chore: implemented web playwright test to catch this regression (#3982) 2024-10-04 09:12:40 -04:00
074c285e04 Add syntax highlighting for if-else (#4090)
* Add syntax highlighting for if-else

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

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-04 08:34:09 -04:00
d7bc92afd9 Fix CodeMirror syntax highlighting of variables without keyword (#4089)
* Fix CodeMirror syntax highlighting of variables without keyword

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

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

* Confirm

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-04 01:29:13 +00:00
11dfd87240 adhoc/Bug fixing the last-failed retries across jobs (#4086)
* chore: adding Kurt's fix for electron in the new CI CD files. Adding a forced failure to test

* chore: increasing max retry for electron to match the playwright browser retry count

* fix: debugging ci cd for playwright last report

* fix: changing the output dir for snapshot to a custom one to not overwrite the previous job runs failure

* fix: found out hidden files are excluded automatically, was a breaking change :(

* fix: output typo

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

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

* Delete test-results-snapshots/.last-run.json

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

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

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

* fix: cleanup

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

* Delete test-results-snapshots/.last-run.json

* fix: removing this folder, should have been git ignored

* fix: do not need these anymore since the hidden files is fixed

* fix: removed hard coded failure for debugging

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-03 20:09:30 -05:00
127 changed files with 14457 additions and 2237 deletions

View File

@ -19,7 +19,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
fi fi
retry=1 retry=1
max_retrys=2 max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do while [[ $retry -le $max_retrys ]]; do

View File

@ -25,6 +25,7 @@ jobs:
runs-on: ubuntu-22.04 # 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: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -51,35 +52,35 @@ jobs:
run: | run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: prepared-files name: prepared-files
path: | path: |
package.json package.json
src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg/wasm_lib*
release-notes.md
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly - id: export_notes
if: ${{ github.event_name == 'schedule' }} run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' }}
with:
name: prepared-files-nightly
path: |
electron-builder.yml
- name: Prepare electron-builder.yml file for updater test - name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: | run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files-updater-test name: prepared-files-updater-test
path: | path: |
@ -118,16 +119,7 @@ jobs:
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
mkdir src/wasm-lib/pkg mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
cp prepared-files/release-notes.md release-notes.md
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
name: prepared-files-nightly
- name: Copy updated electron-builder.yml file for nightly build
if: ${{ github.event_name == 'schedule' }}
run: |
ls -R prepared-files-nightly
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -173,17 +165,11 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: out-arm64-${{ matrix.os }} name: out-${{ matrix.os }}
path: | path: |
out/Zoo*arm64*.* out/Zoo*.*
out/latest*.yml out/latest*.yml
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.os }}
path: |
out/Zoo*x*64*.*
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
@ -203,16 +189,10 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: updater-test-arm64-${{ matrix.os }} name: updater-test-${{ matrix.os }}
path: | path: |
out/Zoo*arm64*.* out/Zoo*.*
out/latest*.yml
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.os }}
path: |
out/Zoo*x64*.*
publish-apps-release: publish-apps-release:
@ -225,7 +205,7 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} 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('Non-release build, commit {0}', github.sha) }} NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
@ -234,32 +214,17 @@ jobs:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-arm64-windows-2022 name: out-windows-2022
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-x64-windows-2022 name: out-macos-14
path: out path: out
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: out-arm64-macos-14 name: out-ubuntu-22.04
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-ubuntu-22.04
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-ubuntu-22.04
path: out path: out
- name: Generate the download static endpoint - name: Generate the download static endpoint

View File

@ -142,6 +142,7 @@ jobs:
with: with:
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- name: Clean up test-results - name: Clean up test-results
@ -177,6 +178,7 @@ jobs:
with: with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
@ -207,6 +209,7 @@ jobs:
with: with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -214,6 +217,7 @@ jobs:
with: with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
@ -313,7 +317,7 @@ jobs:
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-${{ matrix.os }}-${{ github.sha }} name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run electron tests (with retries) - name: Run electron tests (with retries)
id: retry id: retry
@ -339,6 +343,7 @@ jobs:
with: with:
name: test-results-electron-${{ matrix.os }}-${{ github.sha }} name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/ path: test-results/
include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -346,5 +351,6 @@ jobs:
with: with:
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }} name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true

View File

@ -2,7 +2,7 @@
## Zoo Modeling App ## Zoo Modeling App
live at [app.zoo.dev](https://app.zoo.dev/) download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev). A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
@ -57,7 +57,7 @@ yarn install
followed by: followed by:
``` ```
yarn build:wasm-dev yarn build:wasm
``` ```
or if you have the gh cli installed or if you have the gh cli installed
@ -66,15 +66,15 @@ or if you have the gh cli installed
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle ./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
``` ```
That will build the WASM binary and put in the `public` dir (though gitignored) That will build the WASM binary and put in the `public` dir (though gitignored).
finally, to run the web app only, run: Finally, to run the web app only, run:
``` ```
yarn start yarn start
``` ```
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again. If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to localhost:3000 again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
### Development environment variables ### Development environment variables
@ -91,13 +91,13 @@ Third-Party Cookies".
## Desktop ## Desktop
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then To spin up the desktop app, `yarn install` and `yarn build:wasm` need to have been done before hand then
``` ```
yarn electron:start yarn tron:start
``` ```
This will start the application and hot-reload on changed. This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I. Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
@ -128,7 +128,18 @@ Before you submit a contribution PR to this repo, please ensure that:
## Release a new version ## Release a new version
#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR #### 1. Bump the versions by running `./make-release.sh`
The `./make-release.sh` script has git commands to pull main but to be sure you can run the following git commands to have a fresh `main` locally.
```
git branch -D main
git checkout main
git pull origin
./make-release.sh
# Copy within the back ticks and paste the stdout of the change log
git push --set-upstream origin <branch name created from ./make-release.sh>
```
That will create the branch with the updated json files for you: That will create the branch with the updated json files for you:
- run `./make-release.sh` or `./make-release.sh patch` for a patch update; - run `./make-release.sh` or `./make-release.sh patch` for a patch update;
@ -137,28 +148,32 @@ That will create the branch with the updated json files for you:
After it runs you should just need the push the branch and open a PR. After it runs you should just need the push the branch and open a PR.
**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate. #### 2. Create a Cut Release PR
When you open the PR copy the change log from the output of the `./make-release.sh` script into the description of the PR.
**Important:** Pull request title needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate.
The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing. The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing.
#### 2. Smoke test artifacts from the Cut Release PR #### 3. Manually test artifacts from the Cut Release PR
The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch. The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch.
We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR.
The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows). The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows).
#### 3. Merge the Cut Release PR #### 4. Merge the Cut Release PR
This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description. This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description.
#### 4. Publish the release #### 5. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_. Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_.
#### 5. Profit #### 6. Profit
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter.
@ -319,7 +334,16 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
```bash ```bash
cd src/wasm-lib cd src/wasm-lib
cargo test KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
```
Where `XXX` is an API token from the production engine (NOT the dev environment).
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
```
cd src/wasm-lib
KITTYCAD_API_TOKEN=XXX cargo run nextest
``` ```
### Mapping CI CD jobs to local commands ### Mapping CI CD jobs to local commands

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
--- ---
title: "KclValue" title: "KclValue"
excerpt: "A memory item." excerpt: "Any KCL value."
layout: manual layout: manual
--- ---
A memory item. Any KCL value.
@ -80,7 +80,7 @@ A plane.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Plane`| | No | | `type` |enum: `Plane`| | No |
| `id` |`string`| The id of the plane. | No | | `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
@ -183,8 +183,8 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No | | `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No | | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -0,0 +1,80 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
function countNewlines(input: string): number {
let count = 0
for (const char of input) {
if (char === '\n') {
count++
}
}
return count
}
test.describe('Debug pane', () => {
test('Artifact IDs in the artifact graph are stable across code edits', async ({
page,
context,
}) => {
const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([1, 1], %)
`
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const tree = page.getByTestId('debug-feature-tree')
const segment = tree.locator('li', {
hasText: 'segIds:',
hasNotText: 'paths:',
})
await test.step('Test setup', async () => {
await u.waitForAuthSkipAppStart()
await u.openKclCodePanel()
await u.openDebugPanel()
// Set the code in the code editor.
await u.codeLocator.click()
await page.keyboard.type(code, { delay: 0 })
// Scroll to the feature tree.
await tree.scrollIntoViewIfNeeded()
// Expand the feature tree.
await tree.getByText('Feature Tree').click()
// Just expanded the details, making the element taller, so scroll again.
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
})
// Extract the artifact IDs from the debug feature tree.
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
// The artifact ID should include a UUID.
expect(initialSegmentIds).toMatch(
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
)
await test.step('Move cursor to the bottom of the code editor', async () => {
// Focus on the code editor.
await u.codeLocator.click()
// Make sure the cursor is at the end of the code.
const lines = countNewlines(code) + 1
for (let i = 0; i < lines; i++) {
await page.keyboard.press('ArrowDown')
}
})
await test.step('Enter a comment', async () => {
await page.keyboard.type('|> line([2, 2], %)', { delay: 0 })
// Wait for keyboard input debounce and updated artifact graph.
await page.waitForTimeout(1000)
})
const newSegmentIds = await segment.innerText()
// Strip off the closing bracket.
const initialIds = initialSegmentIds.slice(0, initialSegmentIds.length - 1)
expect(newSegmentIds.slice(0, initialIds.length)).toEqual(initialIds)
})
})

View File

@ -13,6 +13,13 @@ type mouseParams = {
pixelDiff: number pixelDiff: number
} }
type SceneSerialised = {
camera: {
position: [number, number, number]
target: [number, number, number]
}
}
export class SceneFixture { export class SceneFixture {
public page: Page public page: Page
@ -22,6 +29,22 @@ export class SceneFixture {
this.page = page this.page = page
this.reConstruct(page) this.reConstruct(page)
} }
private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo()
return {
camera,
}
}
expectState = async (expected: SceneSerialised) => {
return expect
.poll(() => this._serialiseScene(), {
message: `Expected scene state to match`,
})
.toEqual(expected)
}
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
@ -31,7 +54,7 @@ export class SceneFixture {
makeMouseHelpers = ( makeMouseHelpers = (
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 5000 } { steps }: { steps: number } = { steps: 20 }
) => ) =>
[ [
(clickParams?: mouseParams) => { (clickParams?: mouseParams) => {
@ -87,6 +110,36 @@ export class SceneFixture {
) )
await closeDebugPanel(this.page) await closeDebugPanel(this.page)
} }
/** Forces a refresh of the camera position and target displayed
* in the debug panel and then returns the values of the fields
*/
async getCameraInfo() {
await openAndClearDebugPanel(this.page)
await sendCustomCmd(this.page, {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await this.waitForExecutionDone()
const position = await Promise.all([
this.page.getByTestId('cam-x-position').inputValue().then(Number),
this.page.getByTestId('cam-y-position').inputValue().then(Number),
this.page.getByTestId('cam-z-position').inputValue().then(Number),
])
const target = await Promise.all([
this.page.getByTestId('cam-x-target').inputValue().then(Number),
this.page.getByTestId('cam-y-target').inputValue().then(Number),
this.page.getByTestId('cam-z-target').inputValue().then(Number),
])
await closeDebugPanel(this.page)
return {
position,
target,
}
}
waitForExecutionDone = async () => { waitForExecutionDone = async () => {
await expect(this.exeIndicator).toBeVisible() await expect(this.exeIndicator).toBeVisible()
} }
@ -114,4 +167,17 @@ export class SceneFixture {
) )
}) })
} }
get gizmo() {
return this.page.locator('[aria-label*=gizmo]')
}
async clickGizmoMenuItem(name: string) {
await this.gizmo.click({ button: 'right' })
const buttonToTest = this.page.getByRole('button', {
name: name,
})
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
}
} }

View File

@ -1,4 +1,4 @@
import { test, expect, Page } from '@playwright/test' import { test, expect } from '@playwright/test'
import { import {
doExport, doExport,
executorInputPath, executorInputPath,
@ -618,31 +618,30 @@ test(
'Deleting projects, can delete individual project, can still create projects after deleting all', 'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('delete the middle project, i.e. the bracket project', async () => { await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket') const project = page.getByText('bracket')
@ -744,8 +743,26 @@ test(
'Can sort projects on home page', 'Can sort projects on home page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'],
]
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct
for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true })
await fsp.copyFile(
executorInputPath(file),
join(dir, name, `main.kcl`)
)
// Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000))
}
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -753,24 +770,6 @@ test(
page.on('console', console.log) page.on('console', console.log)
const createProjectAndRenameItTest = async ({
name,
page,
}: {
name: string
page: Page
}) => {
await test.step(`Create and rename project ${name}`, async () => {
await createProjectAndRenameIt({ name, page })
})
}
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameItTest({ name: 'router-template-slate', page })
await createProjectAndRenameItTest({ name: 'bracket', page })
await createProjectAndRenameItTest({ name: 'lego', page })
await test.step('should be shorted by modified initially', async () => { await test.step('should be shorted by modified initially', async () => {
const lastModifiedButton = page.getByRole('button', { const lastModifiedButton = page.getByRole('button', {
name: 'Last Modified', name: 'Last Modified',

View File

@ -1115,6 +1115,102 @@ sketch002 = startSketchOn(extrude001, 'END')
).toHaveAttribute('aria-pressed', 'true') ).toHaveAttribute('aria-pressed', 'true')
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
}) })
test('Can sketch on face when user defined function was used in the sketch', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
// Checking for a regression that performs a sketch when a user defined function
// is declared at the top of the file and used in the sketch that is being drawn on.
// fn in2mm is declared at the top of the file and used rail which does a an extrusion with the function.
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`fn in2mm = (inches) => {
return inches * 25.4
}
const railTop = in2mm(.748)
const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200)
const rail = startSketchOn('XZ')
|> startProfileAt([
-railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railWideWidth / 2,
railClampable / 2 + railBaseLength
], %, $seg01)
|> lineTo([railTop / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, railBaseLength], %)
|> lineTo([-railTop / 2, railBaseLength], %)
|> lineTo([
-railWideWidth / 2,
railClampable / 2 + railBaseLength
], %)
|> lineTo([
-railTop / 2,
railClampable + railBaseLength
], %)
|> close(%)
|> extrude(in2mm(2), %)`
)
})
const center = { x: 600, y: 250 }
const rectangleSize = 20
await u.waitForAuthSkipAppStart()
// Start a sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
// Click the top face of this rail
await page.mouse.click(center.x, center.y)
await page.waitForTimeout(1000)
// Draw a rectangle
// top left
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// top right
await page.mouse.click(center.x + rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// bottom right
await page.mouse.click(center.x + rectangleSize, center.y + rectangleSize)
await page.waitForTimeout(250)
// bottom left
await page.mouse.click(center.x - rectangleSize, center.y + rectangleSize)
await page.waitForTimeout(250)
// top left
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// Check execution is done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
})
}) })
test2.describe('Sketch mode should be toleratant to syntax errors', () => { test2.describe('Sketch mode should be toleratant to syntax errors', () => {

View File

@ -521,7 +521,6 @@ test(
const startXPx = 600 const startXPx = 600
// Equip the rectangle tool // Equip the rectangle tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page await page
.getByRole('button', { name: 'rectangle Corner rectangle', exact: true }) .getByRole('button', { name: 'rectangle Corner rectangle', exact: true })
.click() .click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 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: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 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: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,18 +1,18 @@
import { test, expect } from '@playwright/test' import { _test, _expect } from './playwright-deprecated'
import { test } from './fixtures/fixtureSetup'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates' import { TEST_CODE_GIZMO } from './storageStates'
test.beforeEach(async ({ context, page }, testInfo) => { _test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo) await setup(context, page, testInfo)
}) })
test.afterEach(async ({ page }, testInfo) => { _test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
}) })
test.describe('Testing Gizmo', () => { _test.describe('Testing Gizmo', () => {
const cases = [ const cases = [
{ {
testDescription: 'top view', testDescription: 'top view',
@ -57,7 +57,7 @@ test.describe('Testing Gizmo', () => {
expectedCameraTarget, expectedCameraTarget,
testDescription, testDescription,
} of cases) { } of cases) {
test(`check ${testDescription}`, async ({ page, browserName }) => { _test(`check ${testDescription}`, async ({ page, browserName }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => { await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO) localStorage.setItem('persistCode', TEST_CODE_GIZMO)
@ -117,30 +117,30 @@ test.describe('Testing Gizmo', () => {
await Promise.all([ await Promise.all([
// position // position
expect(page.getByTestId('cam-x-position')).toHaveValue( _expect(page.getByTestId('cam-x-position')).toHaveValue(
expectedCameraPosition.x.toString() expectedCameraPosition.x.toString()
), ),
expect(page.getByTestId('cam-y-position')).toHaveValue( _expect(page.getByTestId('cam-y-position')).toHaveValue(
expectedCameraPosition.y.toString() expectedCameraPosition.y.toString()
), ),
expect(page.getByTestId('cam-z-position')).toHaveValue( _expect(page.getByTestId('cam-z-position')).toHaveValue(
expectedCameraPosition.z.toString() expectedCameraPosition.z.toString()
), ),
// target // target
expect(page.getByTestId('cam-x-target')).toHaveValue( _expect(page.getByTestId('cam-x-target')).toHaveValue(
expectedCameraTarget.x.toString() expectedCameraTarget.x.toString()
), ),
expect(page.getByTestId('cam-y-target')).toHaveValue( _expect(page.getByTestId('cam-y-target')).toHaveValue(
expectedCameraTarget.y.toString() expectedCameraTarget.y.toString()
), ),
expect(page.getByTestId('cam-z-target')).toHaveValue( _expect(page.getByTestId('cam-z-target')).toHaveValue(
expectedCameraTarget.z.toString() expectedCameraTarget.z.toString()
), ),
]) ])
}) })
} }
test('Context menu and popover menu', async ({ page }) => { _test('Context menu and popover menu', async ({ page }) => {
const testCase = { const testCase = {
testDescription: 'Right view', testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 }, expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
@ -196,7 +196,7 @@ test.describe('Testing Gizmo', () => {
const buttonToTest = page.getByRole('button', { const buttonToTest = page.getByRole('button', {
name: testCase.testDescription, name: testCase.testDescription,
}) })
await expect(buttonToTest).toBeVisible() await _expect(buttonToTest).toBeVisible()
await buttonToTest.click() await buttonToTest.click()
// Now assert we've moved to the correct view // Now assert we've moved to the correct view
@ -215,23 +215,23 @@ test.describe('Testing Gizmo', () => {
await Promise.all([ await Promise.all([
// position // position
expect(page.getByTestId('cam-x-position')).toHaveValue( _expect(page.getByTestId('cam-x-position')).toHaveValue(
testCase.expectedCameraPosition.x.toString() testCase.expectedCameraPosition.x.toString()
), ),
expect(page.getByTestId('cam-y-position')).toHaveValue( _expect(page.getByTestId('cam-y-position')).toHaveValue(
testCase.expectedCameraPosition.y.toString() testCase.expectedCameraPosition.y.toString()
), ),
expect(page.getByTestId('cam-z-position')).toHaveValue( _expect(page.getByTestId('cam-z-position')).toHaveValue(
testCase.expectedCameraPosition.z.toString() testCase.expectedCameraPosition.z.toString()
), ),
// target // target
expect(page.getByTestId('cam-x-target')).toHaveValue( _expect(page.getByTestId('cam-x-target')).toHaveValue(
testCase.expectedCameraTarget.x.toString() testCase.expectedCameraTarget.x.toString()
), ),
expect(page.getByTestId('cam-y-target')).toHaveValue( _expect(page.getByTestId('cam-y-target')).toHaveValue(
testCase.expectedCameraTarget.y.toString() testCase.expectedCameraTarget.y.toString()
), ),
expect(page.getByTestId('cam-z-target')).toHaveValue( _expect(page.getByTestId('cam-z-target')).toHaveValue(
testCase.expectedCameraTarget.z.toString() testCase.expectedCameraTarget.z.toString()
), ),
]) ])
@ -242,8 +242,60 @@ test.describe('Testing Gizmo', () => {
const gizmoPopoverButton = page.getByRole('button', { const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings', name: 'view settings',
}) })
await expect(gizmoPopoverButton).toBeVisible() await _expect(gizmoPopoverButton).toBeVisible()
await gizmoPopoverButton.click() await gizmoPopoverButton.click()
await expect(buttonToTest).toBeVisible() await _expect(buttonToTest).toBeVisible()
})
})
test.describe(`Testing gizmo, fixture-based`, () => {
test('Center on selection from menu', async ({
app,
cmdBar,
editor,
toolbar,
scene,
}) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
await test.step(`Setup`, async () => {
const file = await app.getInputFile('test-circle-extrude.kcl')
await app.initialise(file)
await scene.expectState({
camera: {
position: [4982.21, -23865.37, 13810.64],
target: [4982.21, 0, 2737.1],
},
})
})
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
await test.step(`Select an edge of this circle`, async () => {
const circleSnippet =
'circle({ center: [318.33, 168.1], radius: 182.8 }, %)'
await moveToCircle()
await clickCircle()
await editor.expectState({
activeLines: [circleSnippet.slice(-5)],
highlightedCode: circleSnippet,
diagnostics: [],
})
})
await test.step(`Center on selection from menu`, async () => {
await scene.clickGizmoMenuItem('Center view on selection')
})
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [0, -23865.37, 11073.54],
target: [0, 0, 0],
},
})
})
}) })
}) })

View File

@ -1208,6 +1208,12 @@ extrude001 = extrude(50, sketch001)
test('Deselecting line tool should mean nothing happens on click', async ({ test('Deselecting line tool should mean nothing happens on click', async ({
page, page,
}) => { }) => {
/**
* If the line tool is clicked when the state is 'No Points' it will exit Sketch mode.
* This is the same exact workflow as pressing ESC.
*
* To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool.
*/
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001)
200 200
) )
// Clicks the XZ Plane in the page
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001)
await page.waitForTimeout(600) await page.waitForTimeout(600)
// Place a point because the line tool will exit if no points are pressed
await page.mouse.click(650, 200)
await page.waitForTimeout(600)
// Code before exiting the tool
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
// deselect the line tool by clicking it // deselect the line tool by clicking it

View File

@ -9,6 +9,7 @@ import {
executorInputPath, executorInputPath,
} from './test-utils' } from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes' import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { import {
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_CORRUPTED,
@ -343,7 +344,7 @@ test.describe('Testing settings', () => {
// Selectors and constants // Selectors and constants
const errorHeading = page.getByRole('heading', { const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred', name: 'An unexpected error occurred',
}) })
const projectDirLink = page.getByText('Loaded from') const projectDirLink = page.getByText('Loaded from')
@ -372,7 +373,7 @@ test.describe('Testing settings', () => {
// Selectors and constants // Selectors and constants
const errorHeading = page.getByRole('heading', { const errorHeading = page.getByRole('heading', {
name: 'An unextected error occurred', name: 'An unexpected error occurred',
}) })
const projectDirLink = page.getByText('Loaded from') const projectDirLink = page.getByText('Loaded from')
@ -384,6 +385,66 @@ test.describe('Testing settings', () => {
} }
) )
// It was much easier to test the logo color than the background stream color.
test(
'user settings reload on external change, on project and modeling view',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const {
electronApp,
page,
dir: projectDirName,
} = await setupElectron({
testInfo,
appSettings: {
app: {
// Doesn't matter what you set it to. It will
// default to 264.5
themeColor: '0',
},
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const logoLink = page.getByTestId('app-logo')
const projectDirLink = page.getByText('Loaded from')
await test.step('Wait for project view', async () => {
await expect(projectDirLink).toBeVisible()
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
})
const changeColor = async (color: string) => {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
let tomlStr = await fsp.readFile(tempSettingsFilePath, 'utf-8')
tomlStr = tomlStr.replace(/(themeColor = ")[0-9]+(")/, `$1${color}$2`)
await fsp.writeFile(tempSettingsFilePath, tomlStr)
}
await test.step('Check color of logo changed', async () => {
await changeColor('99')
await expect(logoLink).toHaveCSS('--primary-hue', '99')
})
await test.step('Check color of logo changed when in modeling view', async () => {
await page.getByRole('button', { name: 'New project' }).click()
await page.getByTestId('project-link').first().click()
await page.getByRole('button', { name: 'Dismiss' }).click()
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})
await test.step('Check going back to projects view still changes the color', async () => {
await logoLink.click()
await expect(projectDirLink).toBeVisible()
await changeColor('21')
await expect(logoLink).toHaveCSS('--primary-hue', '21')
})
await electronApp.close()
}
)
test( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -32,10 +32,10 @@ win:
arch: arch:
- x64 - x64
- arm64 - arm64
- target: msi # - target: msi
arch: # arch:
- x64 # - x64
- arm64 # - arm64
signingHashAlgorithms: signingHashAlgorithms:
- sha256 - sha256
sign: "./sign-win.js" sign: "./sign-win.js"
@ -47,9 +47,9 @@ win:
mimeType: text/vnd.zoo.kcl mimeType: text/vnd.zoo.kcl
description: Zoo KCL File description: Zoo KCL File
role: Editor role: Editor
msi: # msi:
oneClick: false # oneClick: false
perMachine: true # perMachine: true
nsis: nsis:
oneClick: false oneClick: false
perMachine: true perMachine: true
@ -73,3 +73,5 @@ publish:
- provider: generic - provider: generic
url: https://dl.zoo.dev/releases/modeling-app url: https://dl.zoo.dev/releases/modeling-app
channel: latest channel: latest
releaseInfo:
releaseNotesFile: release-notes.md

5
interface.d.ts vendored
View File

@ -23,7 +23,6 @@ export interface IElectronAPI {
callback: (eventType: string, path: string) => void callback: (eventType: string, path: string) => void
) => void ) => void
watchFileOff: (path: string) => void watchFileOff: (path: string) => void
watchFileObliterate: () => void
readFile: (path: string) => ReturnType<fs.readFile> readFile: (path: string) => ReturnType<fs.readFile>
writeFile: ( writeFile: (
path: string, path: string,
@ -70,9 +69,13 @@ export interface IElectronAPI {
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing> listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
onUpdateDownloadStart: (
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: ( onUpdateDownloaded: (
callback: (value: string) => void callback: (value: string) => void
) => Electron.IpcRenderer ) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void appRestart: () => void
} }

View File

@ -113,12 +113,21 @@
], ],
"description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!", "description": "Maximum part size that can be manufactured by this device. This may be some sort of theoretical upper bound, getting close to this limit seems like maybe a bad idea.\n\nThis may be `None` if the maximum size is not knowable by the Machine API.\n\nWhat \"close\" means is up to you!",
"nullable": true "nullable": true
},
"state": {
"allOf": [
{
"$ref": "#/components/schemas/MachineState"
}
],
"description": "Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job."
} }
}, },
"required": [ "required": [
"id", "id",
"machine_type", "machine_type",
"make_model" "make_model",
"state"
], ],
"type": "object" "type": "object"
}, },
@ -143,6 +152,67 @@
}, },
"type": "object" "type": "object"
}, },
"MachineState": {
"description": "Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job.",
"oneOf": [
{
"description": "If a print state can not be resolved at this time, an Unknown may be returned.",
"enum": [
"Unknown"
],
"type": "string"
},
{
"description": "Idle, and ready for another job.",
"enum": [
"Idle"
],
"type": "string"
},
{
"description": "Running a job -- 3D printing or CNC-ing a part.",
"enum": [
"Running"
],
"type": "string"
},
{
"description": "Machine is currently offline or unreachable.",
"enum": [
"Offline"
],
"type": "string"
},
{
"description": "Job is underway but halted, waiting for some action to take place.",
"enum": [
"Paused"
],
"type": "string"
},
{
"description": "Job is finished, but waiting manual action to move back to Idle.",
"enum": [
"Complete"
],
"type": "string"
},
{
"additionalProperties": false,
"description": "The printer has failed and is in an unknown state that may require manual attention to resolve. The inner value is a human readable description of what specifically has failed.",
"properties": {
"Failed": {
"nullable": true,
"type": "string"
}
},
"required": [
"Failed"
],
"type": "object"
}
]
},
"MachineType": { "MachineType": {
"description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.", "description": "Specific technique by which this Machine takes a design, and produces a real-world 3D object.",
"oneOf": [ "oneOf": [

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.5", "version": "0.25.6",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^2.0.1", "@kittycad/lib": "2.0.7",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1", "@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -36,6 +36,7 @@
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^4.1.1", "@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1", "bonjour-service": "^1.2.1",
"chokidar": "^4.0.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",

View File

@ -893,6 +893,7 @@ export class CameraControls {
type: 'zoom_to_fit', type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects padding: 0.2, // padding around the objects
animated: false, // don't animate the zoom for now
}, },
}) })
} }

View File

@ -408,6 +408,7 @@ export async function deleteSegment({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: engineCommandManager, engineCommandManager: engineCommandManager,
}) })

View File

@ -391,12 +391,14 @@ export class SceneEntities {
const { truncatedAst, programMemoryOverride, variableDeclarationName } = const { truncatedAst, programMemoryOverride, variableDeclarationName } =
prepared prepared
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
const sketch = sketchFromPathToNode({ const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
ast: maybeModdedAst, ast: maybeModdedAst,
@ -801,12 +803,14 @@ export class SceneEntities {
updateRectangleSketch(sketchInit, x, y, tags[0]) updateRectangleSketch(sketchInit, x, y, tags[0])
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
@ -848,12 +852,14 @@ export class SceneEntities {
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' }) sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: _ast, ast: _ast,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
// Prepare to update the THREEjs scene // Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
@ -965,12 +971,14 @@ export class SceneEntities {
modded = moddedResult.modifiedAst modded = moddedResult.modifiedAst
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: modded, ast: modded,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue( const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName), programMemory.get(variableDeclarationName),
@ -1317,12 +1325,14 @@ export class SceneEntities {
// don't want to mod the user's code yet as they have't committed to the change yet // don't want to mod the user's code yet as they have't committed to the change yet
// plus this would be the truncated ast being recast, it would be wrong // plus this would be the truncated ast being recast, it would be wrong
codeManager.updateCodeEditor(code) codeManager.updateCodeEditor(code)
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast: truncatedAst, ast: truncatedAst,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
programMemoryOverride, programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
}) })
const programMemory = execState.memory
this.sceneProgramMemory = programMemory this.sceneProgramMemory = programMemory
const maybeSketch = programMemory.get(variableDeclarationName) const maybeSketch = programMemory.get(variableDeclarationName)

View File

@ -157,7 +157,7 @@ export function useCalc({
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: kclManager.programMemory.clone(), programMemoryOverride: kclManager.programMemory.clone(),
}).then(({ programMemory }) => { }).then(({ execState }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
@ -166,7 +166,7 @@ export function useCalc({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
}) })

View File

@ -91,7 +91,7 @@ function CommandBarSelectionInput({
<form id="arg-form" onSubmit={handleSubmit}> <form id="arg-form" onSubmit={handleSubmit}>
<label <label
className={ className={
'relative flex items-center mx-4 my-4 ' + 'relative flex flex-col mx-4 my-4 ' +
(!hasSubmitted || canSubmitSelection || 'text-destroy-50') (!hasSubmitted || canSubmitSelection || 'text-destroy-50')
} }
> >
@ -100,13 +100,18 @@ function CommandBarSelectionInput({
: `Please select ${ : `Please select ${
arg.multiple ? 'one or more ' : 'one ' arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`} }${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
{arg.warningMessage && (
<p className="text-warn-80 bg-warn-10 px-2 py-1 rounded-sm mt-3 mr-2 -mb-2 w-full text-sm cursor-default">
{arg.warningMessage}
</p>
)}
<input <input
id="selection" id="selection"
name="selection" name="selection"
ref={inputRef} ref={inputRef}
required required
placeholder="Select an entity with your mouse" placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" className="absolute inset-0 w-full h-full opacity-0 cursor-default"
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Backspace') { if (event.key === 'Backspace') {
stepBack() stepBack()

View File

@ -0,0 +1,111 @@
import { isArray, isNonNullable } from 'lib/utils'
import { useRef, useState } from 'react'
type Primitive = string | number | bigint | boolean | symbol | null | undefined
export type GenericObj = {
type?: string
[key: string]: GenericObj | Primitive | Array<GenericObj | Primitive>
}
/**
* Display an array of objects or primitives for debug purposes. Nullable values
* are displayed so that relative indexes are preserved.
*/
export function DebugDisplayArray({
arr,
filterKeys,
}: {
arr: Array<GenericObj | Primitive>
filterKeys: string[]
}) {
return (
<>
{arr.map((obj, index) => {
return (
<div className="my-2" key={index}>
{obj && typeof obj === 'object' ? (
<DebugDisplayObj obj={obj} filterKeys={filterKeys} />
) : isNonNullable(obj) ? (
<span>{obj.toString()}</span>
) : (
<span>{obj}</span>
)}
</div>
)
})}
</>
)
}
/**
* Display an object as a tree for debug purposes. Nullable values are omitted.
* The only other property treated specially is the type property, which is
* assumed to be a string.
*/
export function DebugDisplayObj({
obj,
filterKeys,
}: {
obj: GenericObj
filterKeys: string[]
}) {
const ref = useRef<HTMLPreElement>(null)
const hasCursor = false
const [isCollapsed, setIsCollapsed] = useState(false)
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
<button
className="m-0 p-0 border-0 mb-auto"
onClick={() => setIsCollapsed(true)}
>
{'⬇️'}
</button>
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DebugDisplayArray arr={value} filterKeys={filterKeys} />
{']'}
</li>
)
} else if (typeof value === 'object' && value !== null) {
return (
<li key={key}>
{key}:
<DebugDisplayObj obj={value} filterKeys={filterKeys} />
</li>
)
} else if (isNonNullable(value)) {
return (
<li key={key}>
{key}: {value.toString()}
</li>
)
}
return null
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -0,0 +1,45 @@
import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons'
import {
ArtifactGraph,
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() {
const featureTree = useMemo(() => {
return computeTree(engineCommandManager.artifactGraph)
}, [engineCommandManager.artifactGraph])
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
return (
<details data-testid="debug-feature-tree" className="relative">
<summary>Feature Tree</summary>
{featureTree.length > 0 ? (
<pre className="text-xs">
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
</pre>
) : (
<p>(Empty)</p>
)}
</details>
)
}
function computeTree(artifactGraph: ArtifactGraph): GenericObj[] {
let items: GenericObj[] = []
const planes: PlaneArtifactRich[] = []
for (const artifact of artifactGraph.values()) {
if (artifact.type === 'plane') {
planes.push(expandPlane(artifact, artifactGraph))
}
}
const extraRichPlanes: GenericObj[] = planes.map((plane) => {
return plane as any as GenericObj
})
items = items.concat(extraRichPlanes)
return items
}

View File

@ -28,6 +28,7 @@ import {
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { useModelingContext } from 'hooks/useModelingContext'
const CANVAS_SIZE = 80 const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5 const FRUSTUM_SIZE = 0.5
@ -62,6 +63,7 @@ export default function Gizmo() {
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null) const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0) const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0) const raycasterPassiveUpdateTimer = useRef(0)
const { send: modelingSend } = useModelingContext()
const menuItems = useMemo( const menuItems = useMemo(
() => [ () => [
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => ( ...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
@ -76,6 +78,7 @@ export default function Gizmo() {
{axisSemantic} view {axisSemantic} view
</ContextMenuItem> </ContextMenuItem>
)), )),
<ContextMenuDivider />,
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection) sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
@ -83,6 +86,13 @@ export default function Gizmo() {
> >
Reset view Reset view
</ContextMenuItem>, </ContextMenuItem>,
<ContextMenuItem
onClick={() => {
modelingSend({ type: 'Center camera on selection' })
}}
>
Center view on selection
</ContextMenuItem>,
<ContextMenuDivider />, <ContextMenuDivider />,
<ContextMenuItemRefresh />, <ContextMenuItemRefresh />,
], ],

View File

@ -83,6 +83,7 @@ import {
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad' import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { uuidv4 } from 'lib/utils'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -148,6 +149,13 @@ export const ModelingMachineProvider = ({
}, },
'sketch exit execute': ({ context: { store } }) => { 'sketch exit execute': ({ context: { store } }) => {
;(async () => { ;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') { if (cameraProjection.current === 'perspective') {
@ -243,6 +251,17 @@ export const ModelingMachineProvider = ({
return {} return {}
}, },
}), }),
'Center camera on selection': () => {
engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_center_to_selection',
},
})
.catch(reportRejection)
},
'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => { 'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => {
if (event.type !== 'Delete segment') return {} if (event.type !== 'Delete segment') return {}
if (!sketchDetails) return {} if (!sketchDetails) return {}
@ -1037,6 +1056,11 @@ export const ModelingMachineProvider = ({
modelingSend({ type: 'Delete selection' }) modelingSend({ type: 'Delete selection' })
}) })
// Allow ctrl+alt+c to center to selection
useHotkeys(['mod + alt + c'], () => {
modelingSend({ type: 'Center camera on selection' })
})
useStateMachineCommands({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -1,3 +1,4 @@
import { DebugFeatureTree } from 'components/DebugFeatureTree'
import { AstExplorer } from '../../AstExplorer' import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands' import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp' import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
@ -12,6 +13,7 @@ export const DebugPane = () => {
<EngineCommands /> <EngineCommands />
<CamDebugSettings /> <CamDebugSettings />
<AstExplorer /> <AstExplorer />
<DebugFeatureTree />
</div> </div>
</section> </section>
) )

View File

@ -29,8 +29,8 @@ describe('processMemory', () => {
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = parse(code) const ast = parse(code)
const programMemory = await enginelessExecutor(ast, ProgramMemory.empty()) const execState = await enginelessExecutor(ast, ProgramMemory.empty())
const output = processMemory(programMemory) const output = processMemory(execState.memory)
expect(output.myVar).toEqual(5) expect(output.myVar).toEqual(5)
expect(output.otherVar).toEqual(3) expect(output.otherVar).toEqual(3)
expect(output).toEqual({ expect(output).toEqual({

View File

@ -1,9 +1,10 @@
import { trap } from 'lib/trap'
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL' import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect } from 'react' import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands' import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
@ -15,7 +16,6 @@ import {
} from 'lib/theme' } from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { isDesktop } from 'lib/isDesktop'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { import {
kclManager, kclManager,
@ -33,8 +33,14 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes' import { Command } from 'lib/commandTypes'
import { BaseUnit } from 'lib/settings/settingsTypes' import { BaseUnit } from 'lib/settings/settingsTypes'
import { saveSettings } from 'lib/settings/settingsUtils' import {
saveSettings,
loadAndValidateSettings,
} from 'lib/settings/settingsUtils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -99,6 +105,9 @@ export const SettingsAuthProviderBase = ({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const [settingsPath, setSettingsPath] = useState<string | undefined>(
undefined
)
const [settingsState, settingsSend, settingsActor] = useMachine( const [settingsState, settingsSend, settingsActor] = useMachine(
settingsMachine.provide({ settingsMachine.provide({
@ -191,7 +200,11 @@ export const SettingsAuthProviderBase = ({
console.error('Error executing AST after settings change', e) console.error('Error executing AST after settings change', e)
} }
}, },
persistSettings: ({ context }) => { persistSettings: ({ context, event }) => {
// Without this, when a user changes the file, it'd
// create a detection loop with the file-system watcher.
if (event.doNotPersist) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
saveSettings(context, loadedProject?.project?.path) saveSettings(context, loadedProject?.project?.path)
}, },
@ -201,6 +214,23 @@ export const SettingsAuthProviderBase = ({
) )
settingsStateRef = settingsState.context settingsStateRef = settingsState.context
useEffect(() => {
if (!isDesktop()) return
getAppSettingsFilePath().then(setSettingsPath).catch(trap)
}, [])
useFileSystemWatcher(
async () => {
const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({
type: 'Set all settings',
settings: data.settings,
doNotPersist: true,
})
},
settingsPath ? [settingsPath] : []
)
// Add settings commands to the command bar // Add settings commands to the command bar
// They're treated slightly differently than other commands // They're treated slightly differently than other commands
// Because their state machine doesn't have a meaningful .nextEvents, // Because their state machine doesn't have a meaningful .nextEvents,

View File

@ -2,6 +2,7 @@ import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({ export const kclHighlight = styleTags({
'fn var let const': t.definitionKeyword, 'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword,
return: t.controlKeyword, return: t.controlKeyword,
'true false': t.bool, 'true false': t.bool,
nil: t.null, nil: t.null,

View File

@ -16,7 +16,7 @@
statement[@isGroup=Statement] { statement[@isGroup=Statement] {
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } | FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">) VariableDefinition Equals expression } | VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression }
} }
@ -40,6 +40,7 @@ expression[@isGroup=Expression] {
} | } |
UnaryExpression { UnaryOp expression } | UnaryExpression { UnaryOp expression } |
ParenthesizedExpression { "(" expression ")" } | ParenthesizedExpression { "(" expression ")" } |
IfExpression { kw<"if"> expression Body kw<"else"> Body } |
CallExpression { expression !call ArgumentList } | CallExpression { expression !call ArgumentList } |
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } | ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
ObjectExpression { "{" commaSep<ObjectProperty> "}" } | ObjectExpression { "{" commaSep<ObjectProperty> "}" } |

View File

@ -1,4 +1,5 @@
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { reportRejection } from 'lib/trap'
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
type Path = string type Path = string
@ -11,13 +12,13 @@ type Path = string
// watcher.addListener(() => { ... }). // watcher.addListener(() => { ... }).
export const useFileSystemWatcher = ( export const useFileSystemWatcher = (
callback: (path: Path) => void, callback: (path: Path) => Promise<void>,
dependencyArray: Path[] dependencyArray: Path[]
): void => { ): void => {
// Track a ref to the callback. This is how we get the callback updated // Track a ref to the callback. This is how we get the callback updated
// across the NodeJS<->Browser boundary. // across the NodeJS<->Browser boundary.
const callbackRef = useRef<{ fn: (path: Path) => void }>({ const callbackRef = useRef<{ fn: (path: Path) => Promise<void> }>({
fn: (_path) => {}, fn: async (_path) => {},
}) })
useEffect(() => { useEffect(() => {
@ -35,7 +36,9 @@ export const useFileSystemWatcher = (
if (!isDesktop()) return if (!isDesktop()) return
return () => { return () => {
window.electron.watchFileObliterate() for (let path of dependencyArray) {
window.electron.watchFileOff(path)
}
} }
}, []) }, [])
@ -46,6 +49,9 @@ export const useFileSystemWatcher = (
] ]
} }
const hasDiff =
difference(dependencyArray, dependencyArrayTracked)[0].length !== 0
// Removing 1 watcher at a time is only possible because in a filesystem, // Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name). // a path is unique (there can never be two paths with the same name).
// Otherwise we would have to obliterate() the whole list and reconstruct it. // Otherwise we would have to obliterate() the whole list and reconstruct it.
@ -53,6 +59,8 @@ export const useFileSystemWatcher = (
// The hook is useless on web. // The hook is useless on web.
if (!isDesktop()) return if (!isDesktop()) return
if (!hasDiff) return
const [pathsRemoved, pathsRemaining] = difference( const [pathsRemoved, pathsRemaining] = difference(
dependencyArrayTracked, dependencyArrayTracked,
dependencyArray dependencyArray
@ -62,10 +70,10 @@ export const useFileSystemWatcher = (
} }
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked) const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) { for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) => window.electron.watchFileOn(path, (_eventType: string, path: Path) => {
callbackRef.current.fn(path) callbackRef.current.fn(path).catch(reportRejection)
) })
} }
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded)) setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0]) }, [hasDiff])
} }

View File

@ -8,6 +8,7 @@ import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState' import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate' import { ToastUpdate } from 'components/ToastUpdate'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -53,7 +54,22 @@ root.render(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals() reportWebVitals()
isDesktop() && if (isDesktop()) {
// Listen for update download progress to begin
// to show a loading toast.
window.electron.onUpdateDownloadStart(() => {
const message = `Downloading app update...`
console.log(message)
toast.loading(message, { id: AUTO_UPDATER_TOAST_ID })
})
// Listen for update download errors to show
// an error toast and clear the loading toast.
window.electron.onUpdateError(({ error }) => {
console.error(error)
toast.error('An error occurred while downloading the update.', {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded((version: string) => { window.electron.onUpdateDownloaded((version: string) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.` const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message) console.log(message)
@ -64,6 +80,7 @@ isDesktop() &&
window.electron.appRestart() window.electron.appRestart()
}, },
}), }),
{ duration: 30000 } { duration: 30000, id: AUTO_UPDATER_TOAST_ID }
) )
}) })
}

View File

@ -8,6 +8,8 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import { import {
CallExpression, CallExpression,
emptyExecState,
ExecState,
initPromise, initPromise,
parse, parse,
PathToNode, PathToNode,
@ -42,6 +44,7 @@ export class KclManager {
}, },
digest: null, digest: null,
} }
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty() private _programMemory: ProgramMemory = ProgramMemory.empty()
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty() lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
private _logs: string[] = [] private _logs: string[] = []
@ -72,11 +75,21 @@ export class KclManager {
get programMemory() { get programMemory() {
return this._programMemory return this._programMemory
} }
set programMemory(programMemory) { // This is private because callers should be setting the entire execState.
private set programMemory(programMemory) {
this._programMemory = programMemory this._programMemory = programMemory
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
set execState(execState) {
this._execState = execState
this.programMemory = execState.memory
}
get execState() {
return this._execState
}
get logs() { get logs() {
return this._logs return this._logs
} }
@ -253,8 +266,9 @@ export class KclManager {
// Make sure we clear before starting again. End session will do this. // Make sure we clear before starting again. End session will do this.
this.engineCommandManager?.endSession() this.engineCommandManager?.endSession()
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, programMemory, isInterrupted } = await executeAst({ const { logs, errors, execState, isInterrupted } = await executeAst({
ast, ast,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
@ -264,7 +278,7 @@ export class KclManager {
this.lints = await lintAst({ ast: ast }) this.lints = await lintAst({ ast: ast })
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(programMemory, this.engineCommandManager) defaultSelectionFilter(execState.memory, this.engineCommandManager)
if (args.zoomToFit) { if (args.zoomToFit) {
let zoomObjectId: string | undefined = '' let zoomObjectId: string | undefined = ''
@ -282,6 +296,7 @@ export class KclManager {
type: 'zoom_to_fit', type: 'zoom_to_fit',
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: 0.1, // padding around the objects padding: 0.1, // padding around the objects
animated: false, // don't animate the zoom for now
}, },
}) })
} }
@ -294,12 +309,20 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId) this._cancelTokens.delete(currentExecutionId)
return return
} }
// Exit sketch mode if the AST is empty
if (this._isAstEmpty(ast)) {
await this.disableSketchMode()
}
this.logs = logs this.logs = logs
// Do not add the errors since the program was interrupted and the error is not a real KCL error // Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addKclErrors(isInterrupted ? [] : errors) this.addKclErrors(isInterrupted ? [] : errors)
this.programMemory = programMemory // Reset the next ID index so that we reuse the previous IDs next time.
execState.idGenerator.nextId = 0
this.execState = execState
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = programMemory this.lastSuccessfulProgramMemory = execState.memory
} }
this.ast = { ...ast } this.ast = { ...ast }
this._executeCallback() this._executeCallback()
@ -337,17 +360,19 @@ export class KclManager {
await codeManager.writeToFile() await codeManager.writeToFile()
this._ast = { ...newAst } this._ast = { ...newAst }
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, execState } = await executeAst({
ast: newAst, ast: newAst,
idGenerator: this.execState.idGenerator,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
}) })
this._logs = logs this._logs = logs
this._kclErrors = errors this._kclErrors = errors
this._programMemory = programMemory this._execState = execState
this._programMemory = execState.memory
if (!errors.length) { if (!errors.length) {
this.lastSuccessfulProgramMemory = programMemory this.lastSuccessfulProgramMemory = execState.memory
} }
if (updates !== 'artifactRanges') return if (updates !== 'artifactRanges') return
@ -552,6 +577,24 @@ export class KclManager {
defaultSelectionFilter() { defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager) defaultSelectionFilter(this.programMemory, this.engineCommandManager)
} }
/**
* We can send a single command of 'enable_sketch_mode' or send this in a batched request.
* When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished
* code could leave the state of the application in sketch mode on the engine side.
*/
async disableSketchMode() {
await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
}
// Determines if there is no KCL code which means it is executing a blank KCL file
_isAstEmpty(ast: Program) {
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
}
} }
function defaultSelectionFilter( function defaultSelectionFilter(

View File

@ -14,9 +14,9 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %)` // |> rx(45, %)`
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'UserVal', type: 'UserVal',
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY')
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
// |> rx(45, %) // |> rx(45, %)
|> extrude(2, %)` |> extrude(2, %)`
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
// @ts-ignore // @ts-ignore
const sketch001 = programMemory?.get('mySketch001') const sketch001 = execState.memory.get('mySketch001')
expect(sketch001).toEqual({ expect(sketch001).toEqual({
type: 'Solid', type: 'Solid',
id: expect.any(String), id: expect.any(String),
@ -148,9 +148,10 @@ const sk2 = startSketchOn('XY')
|> extrude(2, %) |> extrude(2, %)
` `
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const programMemory = execState.memory
// @ts-ignore // @ts-ignore
const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')] const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')]
expect(geos).toEqual([ expect(geos).toEqual([
{ {
type: 'Solid', type: 'Solid',

View File

@ -443,6 +443,6 @@ async function exe(
) { ) {
const ast = parse(code) const ast = parse(code)
const result = await enginelessExecutor(ast, programMemory) const execState = await enginelessExecutor(ast, programMemory)
return result return execState.memory
} }

View File

@ -4,11 +4,14 @@ import {
ProgramMemory, ProgramMemory,
programMemoryInit, programMemoryInit,
kclLint, kclLint,
emptyExecState,
ExecState,
} from 'lang/wasm' } from 'lang/wasm'
import { enginelessExecutor } from 'lib/testHelpers' import { enginelessExecutor } from 'lib/testHelpers'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors' import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint' import { Diagnostic } from '@codemirror/lint'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
export type ToolTip = export type ToolTip =
| 'lineTo' | 'lineTo'
@ -47,16 +50,18 @@ export async function executeAst({
engineCommandManager, engineCommandManager,
useFakeExecutor = false, useFakeExecutor = false,
programMemoryOverride, programMemoryOverride,
idGenerator,
}: { }: {
ast: Program ast: Program
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
idGenerator?: IdGenerator
isInterrupted?: boolean isInterrupted?: boolean
}): Promise<{ }): Promise<{
logs: string[] logs: string[]
errors: KCLError[] errors: KCLError[]
programMemory: ProgramMemory execState: ExecState
isInterrupted: boolean isInterrupted: boolean
}> { }> {
try { try {
@ -65,15 +70,21 @@ export async function executeAst({
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
} }
const programMemory = await (useFakeExecutor const execState = await (useFakeExecutor
? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) ? enginelessExecutor(ast, programMemoryOverride || programMemoryInit())
: _executor(ast, programMemoryInit(), engineCommandManager, false)) : _executor(
ast,
programMemoryInit(),
idGenerator,
engineCommandManager,
false
))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return { return {
logs: [], logs: [],
errors: [], errors: [],
programMemory, execState,
isInterrupted: false, isInterrupted: false,
} }
} catch (e: any) { } catch (e: any) {
@ -89,7 +100,7 @@ export async function executeAst({
return { return {
errors: [e], errors: [e],
logs: [], logs: [],
programMemory: ProgramMemory.empty(), execState: emptyExecState(),
isInterrupted, isInterrupted,
} }
} else { } else {
@ -97,7 +108,7 @@ export async function executeAst({
return { return {
logs: [e], logs: [e],
errors: [], errors: [],
programMemory: ProgramMemory.empty(), execState: emptyExecState(),
isInterrupted, isInterrupted,
} }
} }

View File

@ -220,11 +220,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression into a new variable', async () => { it('should move a binary expression into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('100 + 100') + 1 const startIndex = code.indexOf('100 + 100') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -235,11 +235,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a value into a new variable', async () => { it('should move a value into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('2.8') + 1 const startIndex = code.indexOf('2.8') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -250,11 +250,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a callExpression into a new variable', async () => { it('should move a callExpression into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('def(') const startIndex = code.indexOf('def(')
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -265,11 +265,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a binary expression with call expression into a new variable', async () => { it('should move a binary expression with call expression into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('jkl(') + 1 const startIndex = code.indexOf('jkl(') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -280,11 +280,11 @@ yo2 = hmm([identifierGuy + 5])`
it('should move a identifier into a new variable', async () => { it('should move a identifier into a new variable', async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const startIndex = code.indexOf('identifierGuy +') + 1 const startIndex = code.indexOf('identifierGuy +') + 1
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
programMemory, execState.memory,
[startIndex, startIndex], [startIndex, startIndex],
'newVar' 'newVar'
) )
@ -465,7 +465,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
|> line([306.21, 198.87], %)` |> line([306.21, 198.87], %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)' const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number] = [ const range: [number, number] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
@ -475,7 +475,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
[], [],
ast, ast,
programMemory, execState.memory,
code, code,
pathToNode pathToNode
) )
@ -543,7 +543,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const code = makeCode(line) const code = makeCode(line)
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = line const lineOfInterest = line
const range: [number, number] = [ const range: [number, number] = [
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
@ -554,7 +554,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
dependentSegments, dependentSegments,
ast, ast,
programMemory, execState.memory,
code, code,
pathToNode pathToNode
) )
@ -632,7 +632,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [ const range: [number, number] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
@ -661,7 +661,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode, pathToNode,
argPosition, argPosition,
ast, ast,
programMemory execState.memory
) )
if (!mod) return new Error('mod is undefined') if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst) const recastCode = recast(mod.modifiedAst)
@ -686,7 +686,7 @@ describe('Testing removeSingleConstraintInfo', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number] = [ const range: [number, number] = [
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
@ -711,7 +711,7 @@ describe('Testing removeSingleConstraintInfo', () => {
pathToNode, pathToNode,
argPosition, argPosition,
ast, ast,
programMemory execState.memory
) )
if (!mod) return new Error('mod is undefined') if (!mod) return new Error('mod is undefined')
const recastCode = recast(mod.modifiedAst) const recastCode = recast(mod.modifiedAst)
@ -882,7 +882,7 @@ sketch002 = startSketchOn({
// const lineOfInterest = 'line([-2.94, 2.7], %)' // const lineOfInterest = 'line([-2.94, 2.7], %)'
const ast = parse(codeBefore) const ast = parse(codeBefore)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
// deleteFromSelection // deleteFromSelection
const range: [number, number] = [ const range: [number, number] = [
@ -895,7 +895,7 @@ sketch002 = startSketchOn({
range, range,
type, type,
}, },
programMemory, execState.memory,
async () => { async () => {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
return { return {

View File

@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7 const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const { variables, bodyPath, insertIndex } = findAllPreviousVariables( const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast, ast,
programMemory, execState.memory,
[rangeStart, rangeStart] [rangeStart, rangeStart]
) )
expect(variables).toEqual([ expect(variables).toEqual([
@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [100, 101] }, selection: { type: 'default', range: [100, 101] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(true) expect(result).toEqual(true)
}) })
@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [100, 101] }, selection: { type: 'default', range: [100, 101] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(true) expect(result).toEqual(true)
}) })
@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46])
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { type: 'default', range: [10, 11] }, selection: { type: 'default', range: [10, 11] },
programMemory, programMemory: execState.memory,
}) })
expect(result).toEqual(false) expect(result).toEqual(false)
}) })

View File

@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) return ast if (err(ast)) return ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
const changeSketchArgsRetVal = changeSketchArguments( const changeSketchArgsRetVal = changeSketchArguments(
ast, ast,
programMemory, execState.memory,
{ {
type: 'sourceRange', type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length], sourceRange: [sourceStart, sourceStart + lineToChange.length],
@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY')
const ast = parse(code) const ast = parse(code)
if (err(ast)) return ast if (err(ast)) return ast
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(89) expect(sourceStart).toBe(89)
const newSketchLnRetVal = addNewSketchLn({ const newSketchLnRetVal = addNewSketchLn({
node: ast, node: ast,
programMemory, programMemory: execState.memory,
input: { input: {
type: 'straight-segment', type: 'straight-segment',
from: [0, 0], from: [0, 0],
@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY')
const modifiedAst2 = addCloseToPipe({ const modifiedAst2 = addCloseToPipe({
node: ast, node: ast,
programMemory, programMemory: execState.memory,
pathToNode: [ pathToNode: [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
{ {
// previousProgramMemory: programMemory, // redundant? // previousProgramMemory: execState.memory, // redundant?
pathToNode, pathToNode,
node: ast, node: ast,
}, },

View File

@ -34,7 +34,7 @@ async function testingSwapSketchFnCall({
const ast = parse(inputCode) const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const selections = { const selections = {
codeBasedSelections: [range], codeBasedSelections: [range],
otherSelections: [], otherSelections: [],
@ -45,7 +45,7 @@ async function testingSwapSketchFnCall({
return Promise.reject(new Error('transformInfos undefined')) return Promise.reject(new Error('transformInfos undefined'))
const ast2 = transformAstSketchLines({ const ast2 = transformAstSketchLines({
ast, ast,
programMemory, programMemory: execState.memory,
selectionRanges: selections, selectionRanges: selections,
transformInfos, transformInfos,
referenceSegName: '', referenceSegName: '',
@ -360,10 +360,10 @@ part001 = startSketchOn('XY')
|> line([2.14, 1.35], %) // normal-segment |> line([2.14, 1.35], %) // normal-segment
|> xLine(3.54, %)` |> xLine(3.54, %)`
it('normal case works', async () => { it('normal case works', async () => {
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const index = code.indexOf('// normal-segment') - 7 const index = code.indexOf('// normal-segment') - 7
const sg = sketchFromKclValue( const sg = sketchFromKclValue(
programMemory.get('part001'), execState.memory.get('part001'),
'part001' 'part001'
) as Sketch ) as Sketch
const _segment = getSketchSegmentFromSourceRange(sg, [index, index]) const _segment = getSketchSegmentFromSourceRange(sg, [index, index])
@ -377,10 +377,10 @@ part001 = startSketchOn('XY')
}) })
}) })
it('verify it works when the segment is in the `start` property', async () => { it('verify it works when the segment is in the `start` property', async () => {
const programMemory = await enginelessExecutor(parse(code)) const execState = await enginelessExecutor(parse(code))
const index = code.indexOf('// segment-in-start') - 7 const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sketchFromKclValue(programMemory.get('part001'), 'part001') as Sketch, sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
[index, index] [index, index]
) )
if (err(_segment)) throw _segment if (err(_segment)) throw _segment

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
} from './sketchcombos' } from './sketchcombos'
import { ToolTip } from 'lang/langHelpers' import { ToolTip } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
@ -96,6 +96,86 @@ function makeSelections(
} }
describe('testing transformAstForSketchLines for equal length constraint', () => { describe('testing transformAstForSketchLines for equal length constraint', () => {
describe(`should always reorder selections to have the base selection first`, () => {
const inputScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %)
|> line([-2, 5], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
const expectedModifiedScript = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $seg01)
|> angledLine([112, segLen(seg01)], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const selectLine = (script: string, lineNumber: number): Selection => {
const lines = script.split('\n')
const codeBeforeLine = lines.slice(0, lineNumber).join('\n').length
const line = lines.find((_, i) => i === lineNumber)
if (!line) {
throw new Error(
`line index ${lineNumber} not found in test sample, friend`
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number] = [start, start]
return {
type: 'default',
range,
}
}
async function applyTransformation(
inputCode: string,
selectionRanges: Selections['codeBasedSelections']
) {
const ast = parse(inputCode)
if (err(ast)) return Promise.reject(ast)
const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)),
ast,
'equalLength'
)
const transformedSelection = makeSelections(selectionRanges)
const newAst = transformSecondarySketchLinesTagFirst({
ast,
selectionRanges: transformedSelection,
transformInfos,
programMemory: execState.memory,
})
if (err(newAst)) return Promise.reject(newAst)
const newCode = recast(newAst.modifiedAst)
return newCode
}
it(`Should reorder when user selects first-to-last`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 3),
selectLine(inputScript, 4),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
it(`Should reorder when user selects last-to-first`, async () => {
const selectionRanges: Selections['codeBasedSelections'] = [
selectLine(inputScript, 4),
selectLine(inputScript, 3),
]
const newCode = await applyTransformation(inputScript, selectionRanges)
expect(newCode).toBe(expectedModifiedScript)
})
})
const inputScript = `myVar = 3 const inputScript = `myVar = 3
myVar2 = 5 myVar2 = 5
myVar3 = 6 myVar3 = 6
@ -220,7 +300,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)), makeSelections(selectionRanges.slice(1)),
ast, ast,
@ -231,7 +311,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -311,7 +391,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges), makeSelections(selectionRanges),
ast, ast,
@ -322,7 +402,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
referenceSegName: '', referenceSegName: '',
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -373,7 +453,7 @@ part001 = startSketchOn('XY')
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges), makeSelections(selectionRanges),
ast, ast,
@ -384,7 +464,7 @@ part001 = startSketchOn('XY')
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
referenceSegName: '', referenceSegName: '',
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)
@ -470,7 +550,7 @@ async function helperThing(
} }
}) })
const programMemory = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const transformInfos = getTransformInfos( const transformInfos = getTransformInfos(
makeSelections(selectionRanges.slice(1)), makeSelections(selectionRanges.slice(1)),
ast, ast,
@ -481,7 +561,7 @@ async function helperThing(
ast, ast,
selectionRanges: makeSelections(selectionRanges), selectionRanges: makeSelections(selectionRanges),
transformInfos, transformInfos,
programMemory, programMemory: execState.memory,
}) })
if (err(newAst)) return Promise.reject(newAst) if (err(newAst)) return Promise.reject(newAst)

View File

@ -1559,7 +1559,15 @@ export function transformSecondarySketchLinesTagFirst({
} }
| Error { | Error {
// let node = structuredClone(ast) // let node = structuredClone(ast)
const primarySelection = selectionRanges.codeBasedSelections[0].range
// We need to sort the selections by their start position
// so that we can process them in dependency order and not write invalid KCL.
const sortedCodeBasedSelections =
selectionRanges.codeBasedSelections.toSorted(
(a, b) => a.range[0] - b.range[0]
)
const primarySelection = sortedCodeBasedSelections[0].range
const secondarySelections = sortedCodeBasedSelections.slice(1)
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName) const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
if (err(_tag)) return _tag if (err(_tag)) return _tag
@ -1569,7 +1577,7 @@ export function transformSecondarySketchLinesTagFirst({
ast: modifiedAst, ast: modifiedAst,
selectionRanges: { selectionRanges: {
...selectionRanges, ...selectionRanges,
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), codeBasedSelections: secondarySelections,
}, },
referencedSegmentRange: primarySelection, referencedSegmentRange: primarySelection,
transformInfos, transformInfos,

View File

@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => {
offset: ${offset}, offset: ${offset},
}, %, $yo2) }, %, $yo2)
intersect = segEndX(yo2)` intersect = segEndX(yo2)`
const mem = await enginelessExecutor(parse(code('-1'))) const execState = await enginelessExecutor(parse(code('-1')))
expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2)) expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2))
const noOffset = await enginelessExecutor(parse(code('0'))) const noOffset = await enginelessExecutor(parse(code('0')))
expect(noOffset.get('intersect')?.value).toBeCloseTo(1) expect(noOffset.memory.get('intersect')?.value).toBeCloseTo(1)
}) })
}) })

View File

@ -37,6 +37,11 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types' import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch' import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -136,29 +141,46 @@ export const parse = (code: string | Error): Program | Error => {
export type PathToNode = [string | number, string][] export type PathToNode = [string | number, string][]
interface Memory { export interface ExecState {
[key: string]: KclValue memory: ProgramMemory
idGenerator: IdGenerator
} }
type EnvironmentRef = number /**
* Create an empty ExecState. This is useful on init to prevent needing an
* Option.
*/
export function emptyExecState(): ExecState {
return {
memory: ProgramMemory.empty(),
idGenerator: defaultIdGenerator(),
}
}
function execStateFromRaw(raw: RawExecState): ExecState {
return {
memory: ProgramMemory.fromRaw(raw.memory),
idGenerator: raw.idGenerator,
}
}
export function defaultIdGenerator(): IdGenerator {
return {
nextId: 0,
ids: [],
}
}
interface Memory {
[key: string]: KclValue | undefined
}
const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0 const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0
interface Environment {
bindings: Memory
parent: EnvironmentRef | null
}
function emptyEnvironment(): Environment { function emptyEnvironment(): Environment {
return { bindings: {}, parent: null } return { bindings: {}, parent: null }
} }
interface RawProgramMemory {
environments: Environment[]
currentEnv: EnvironmentRef
return: KclValue | null
}
/** /**
* This duplicates logic in Rust. The hope is to keep ProgramMemory internals * This duplicates logic in Rust. The hope is to keep ProgramMemory internals
* isolated from the rest of the TypeScript code so that we can move it to Rust * isolated from the rest of the TypeScript code so that we can move it to Rust
@ -217,7 +239,7 @@ export class ProgramMemory {
while (true) { while (true) {
const env = this.environments[envRef] const env = this.environments[envRef]
if (env.bindings.hasOwnProperty(name)) { if (env.bindings.hasOwnProperty(name)) {
return env.bindings[name] return env.bindings[name] ?? null
} }
if (!env.parent) { if (!env.parent) {
break break
@ -260,6 +282,7 @@ export class ProgramMemory {
} }
for (const [name, value] of Object.entries(env.bindings)) { for (const [name, value] of Object.entries(env.bindings)) {
if (value === undefined) continue
// Check the predicate. // Check the predicate.
if (!predicate(value)) { if (!predicate(value)) {
continue continue
@ -293,6 +316,7 @@ export class ProgramMemory {
while (true) { while (true) {
const env = this.environments[envRef] const env = this.environments[envRef]
for (const [name, value] of Object.entries(env.bindings)) { for (const [name, value] of Object.entries(env.bindings)) {
if (value === undefined) continue
// Don't include shadowed variables. // Don't include shadowed variables.
if (!map.has(name)) { if (!map.has(name)) {
map.set(name, value) map.set(name, value)
@ -356,9 +380,10 @@ export function sketchFromKclValue(
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(), programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean = false isMock: boolean = false
): Promise<ProgramMemory> => { ): Promise<ExecState> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -366,6 +391,7 @@ export const executor = async (
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
programMemory, programMemory,
idGenerator,
engineCommandManager, engineCommandManager,
isMock isMock
) )
@ -378,9 +404,10 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory | Error = ProgramMemory.empty(), programMemory: ProgramMemory | Error = ProgramMemory.empty(),
idGenerator: IdGenerator = defaultIdGenerator(),
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
isMock: boolean isMock: boolean
): Promise<ProgramMemory> => { ): Promise<ExecState> => {
if (err(programMemory)) return Promise.reject(programMemory) if (err(programMemory)) return Promise.reject(programMemory)
try { try {
@ -392,15 +419,16 @@ export const _executor = async (
baseUnit = baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' (await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
} }
const memory: RawProgramMemory = await execute_wasm( const execState: RawExecState = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory.toRaw()), JSON.stringify(programMemory.toRaw()),
JSON.stringify(idGenerator),
baseUnit, baseUnit,
engineCommandManager, engineCommandManager,
fileSystemManager, fileSystemManager,
isMock isMock
) )
return ProgramMemory.fromRaw(memory) return execStateFromRaw(execState)
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())

View File

@ -281,6 +281,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: true, multiple: true,
required: true, required: true,
skip: false, skip: false,
warningMessage:
'Fillets cannot touch other fillets yet. This is under development.',
}, },
radius: { radius: {
inputType: 'kcl', inputType: 'kcl',

View File

@ -113,6 +113,7 @@ export type CommandArgumentConfig<
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: C machineContext?: C
) => boolean) ) => boolean)
warningMessage?: string
skip?: boolean skip?: boolean
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
@ -189,6 +190,7 @@ export type CommandArgument<
) => boolean) ) => boolean)
skip?: boolean skip?: boolean
machineActor?: Actor<T> machineActor?: Actor<T>
warningMessage?: string
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
*/ */

View File

@ -102,3 +102,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json', 'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
localFallback: '/kcl-samples-manifest-fallback.json', localFallback: '/kcl-samples-manifest-fallback.json',
} as const } as const
/** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'

View File

@ -152,6 +152,7 @@ export function buildCommandArgument<
skip: arg.skip, skip: arg.skip,
machineActor, machineActor,
valueSummary: arg.valueSummary, valueSummary: arg.valueSummary,
warningMessage: arg.warningMessage ?? '',
} satisfies Omit<CommandArgument<O, T>, 'inputType'> } satisfies Omit<CommandArgument<O, T>, 'inputType'>
if (arg.inputType === 'options') { if (arg.inputType === 'options') {

View File

@ -379,7 +379,7 @@ const getAppFolderName = () => {
return window.electron.packageJson.name return window.electron.packageJson.name
} }
const getAppSettingsFilePath = async () => { export const getAppSettingsFilePath = async () => {
const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true' const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true'
const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY
const appConfig = await window.electron.getPath('appData') const appConfig = await window.electron.getPath('appData')

View File

@ -126,6 +126,8 @@ export interface components {
* *
* What "close" means is up to you! */ * What "close" means is up to you! */
max_part_volume?: components['schemas']['Volume'] | null max_part_volume?: components['schemas']['Volume'] | null
/** @description Status of the printer -- be it printing, idle, or unreachable. This may dictate if a machine is capable of taking a new job. */
state: components['schemas']['MachineState']
} }
/** @description Information regarding the make/model of a discovered endpoint. */ /** @description Information regarding the make/model of a discovered endpoint. */
MachineMakeModel: { MachineMakeModel: {
@ -136,6 +138,17 @@ export interface components {
/** @description The unique serial number of the connected Machine. */ /** @description The unique serial number of the connected Machine. */
serial?: string | null serial?: string | null
} }
/** @description Current state of the machine -- be it printing, idle or offline. This can be used to determine if a printer is in the correct state to take a new job. */
MachineState:
| 'Unknown'
| 'Idle'
| 'Running'
| 'Offline'
| 'Paused'
| 'Complete'
| {
Failed: string | null
}
/** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */ /** @description Specific technique by which this Machine takes a design, and produces a real-world 3D object. */
MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc' MachineType: 'Stereolithography' | 'FusedDeposition' | 'Cnc'
/** @description The response from the `/ping` endpoint. */ /** @description The response from the `/ping` endpoint. */

View File

@ -145,6 +145,13 @@ export const interactionMap: Record<
description: description:
'Available while modeling with either a face selected or an empty selection, when not typing in the code editor.', 'Available while modeling with either a face selected or an empty selection, when not typing in the code editor.',
}, },
{
name: 'center-on-selection',
sequence: `${PRIMARY}+Alt+C`,
title: 'Center on selection',
description:
'Centers the view on the selected geometry, or everything if nothing is selected.',
},
], ],
'Code Editor': [ 'Code Editor': [
{ {

View File

@ -177,14 +177,14 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings() let settingsNext = createSettings()
// Because getting the default directory is async, we need to set it after // Because getting the default directory is async, we need to set it after
if (onDesktop) { if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir() settings.app.projectDirectory.default = await getInitialDefaultDir()
} }
setSettingsAtLevel( settingsNext = setSettingsAtLevel(
settings, settingsNext,
'user', 'user',
configurationToSettingsPayload(appSettingsPayload) configurationToSettingsPayload(appSettingsPayload)
) )
@ -199,8 +199,8 @@ export async function loadAndValidateSettings(
return Promise.reject(new Error('Invalid project settings')) return Promise.reject(new Error('Invalid project settings'))
const projectSettingsPayload = projectSettings const projectSettingsPayload = projectSettings
setSettingsAtLevel( settingsNext = setSettingsAtLevel(
settings, settingsNext,
'project', 'project',
projectConfigurationToSettingsPayload(projectSettingsPayload) projectConfigurationToSettingsPayload(projectSettingsPayload)
) )
@ -208,7 +208,7 @@ export async function loadAndValidateSettings(
// Return the settings object // Return the settings object
return { return {
settings, settings: settingsNext,
configuration: appSettingsPayload, configuration: appSettingsPayload,
} }
} }

View File

@ -49,6 +49,7 @@ if (typeof window !== 'undefined') {
type: 'zoom_to_fit', type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects padding: 0.2, // padding around the objects
animated: false, // don't animate the zoom for now
}, },
}) })
} }

View File

@ -1,4 +1,11 @@
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm' import {
Program,
ProgramMemory,
_executor,
SourceRange,
ExecState,
defaultIdGenerator,
} from '../lang/wasm'
import { import {
EngineCommandManager, EngineCommandManager,
EngineCommandManagerEvents, EngineCommandManagerEvents,
@ -9,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { toSync } from './utils' import { toSync } from './utils'
import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator'
type WebSocketResponse = Models['WebSocketResponse_type'] type WebSocketResponse = Models['WebSocketResponse_type']
@ -77,8 +85,9 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Program | Error, ast: Program | Error,
pm: ProgramMemory | Error = ProgramMemory.empty() pm: ProgramMemory | Error = ProgramMemory.empty(),
): Promise<ProgramMemory> { idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> {
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
if (err(pm)) return Promise.reject(pm) if (err(pm)) return Promise.reject(pm)
@ -88,15 +97,22 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) const execState = await _executor(
ast,
pm,
idGenerator,
mockEngineCommandManager,
true
)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return programMemory return execState
} }
export async function executor( export async function executor(
ast: Program, ast: Program,
pm: ProgramMemory = ProgramMemory.empty() pm: ProgramMemory = ProgramMemory.empty(),
): Promise<ProgramMemory> { idGenerator: IdGenerator = defaultIdGenerator()
): Promise<ExecState> {
const engineCommandManager = new EngineCommandManager() const engineCommandManager = new EngineCommandManager()
engineCommandManager.start({ engineCommandManager.start({
setIsStreamReady: () => {}, setIsStreamReady: () => {},
@ -117,14 +133,15 @@ export async function executor(
toSync(async () => { toSync(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor( const execState = await _executor(
ast, ast,
pm, pm,
idGenerator,
engineCommandManager, engineCommandManager,
false false
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
resolve(programMemory) resolve(execState)
}, reportRejection) }, reportRejection)
) )
}) })

View File

@ -249,7 +249,7 @@ export async function submitAndAwaitTextToKcl({
export async function sendTelemetry( export async function sendTelemetry(
id: string, id: string,
feedback: Models['AiFeedback_type'], feedback: Models['MlFeedback_type'],
token?: string token?: string
): Promise<void> { ): Promise<void> {
const url = const url =

View File

@ -295,15 +295,24 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
'break', 'break',
{ {
id: 'line', id: 'line',
onClick: ({ modelingState, modelingSend }) => onClick: ({ modelingState, modelingSend }) => {
modelingSend({ if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) {
type: 'change tool', // Exit the sketch state if there are no points and they press ESC
data: { modelingSend({
tool: !modelingState.matches({ Sketch: 'Line tool' }) type: 'Cancel',
? 'line' })
: 'none', } else {
}, // Exit the tool if there are points and they press ESC
}), modelingSend({
type: 'change tool',
data: {
tool: !modelingState.matches({ Sketch: 'Line tool' })
? 'line'
: 'none',
},
})
}
},
icon: 'line', icon: 'line',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>

View File

@ -97,7 +97,7 @@ export function useCalculateKclExpression({
}) })
if (trap(error, { suppress: true })) return if (trap(error, { suppress: true })) return
} }
const { programMemory } = await executeAst({ const { execState } = await executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
@ -111,7 +111,7 @@ export function useCalculateKclExpression({
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.get('__result__')?.value const result = execState.memory?.get('__result__')?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
} }

View File

@ -252,6 +252,9 @@ export type ModelingMachineEvent =
type: 'Set Segment Overlays' type: 'Set Segment Overlays'
data: SegmentOverlayPayload data: SegmentOverlayPayload
} }
| {
type: 'Center camera on selection'
}
| { | {
type: 'Delete segment' type: 'Delete segment'
data: PathToNode data: PathToNode
@ -663,6 +666,7 @@ export const modelingMachine = setup({
const testExecute = await executeAst({ const testExecute = await executeAst({
ast: modifiedAst, ast: modifiedAst,
idGenerator: kclManager.execState.idGenerator,
useFakeExecutor: true, useFakeExecutor: true,
engineCommandManager, engineCommandManager,
}) })
@ -938,6 +942,7 @@ export const modelingMachine = setup({
'Set selection': () => {}, 'Set selection': () => {},
'Set mouse state': () => {}, 'Set mouse state': () => {},
'Set Segment Overlays': () => {}, 'Set Segment Overlays': () => {},
'Center camera on selection': () => {},
'Engine export': () => {}, 'Engine export': () => {},
'Submit to Text-to-CAD API': () => {}, 'Submit to Text-to-CAD API': () => {},
'Set sketchDetails': () => {}, 'Set sketchDetails': () => {},
@ -2105,6 +2110,10 @@ export const modelingMachine = setup({
reenter: false, reenter: false,
actions: 'Set Segment Overlays', actions: 'Set Segment Overlays',
}, },
'Center camera on selection': {
reenter: false,
actions: 'Center camera on selection',
},
}, },
}) })

View File

@ -19,7 +19,7 @@ export const settingsMachine = setup({
types: { types: {
context: {} as ReturnType<typeof createSettings>, context: {} as ReturnType<typeof createSettings>,
input: {} as ReturnType<typeof createSettings>, input: {} as ReturnType<typeof createSettings>,
events: {} as events: {} as (
| WildcardSetEvent<SettingsPaths> | WildcardSetEvent<SettingsPaths>
| SetEventTypes | SetEventTypes
| { | {
@ -34,7 +34,8 @@ export const settingsMachine = setup({
type: 'Reset settings' type: 'Reset settings'
level: SettingsLevel level: SettingsLevel
} }
| { type: 'Set all settings'; settings: typeof settings }, | { type: 'Set all settings'; settings: typeof settings }
) & { doNotPersist?: boolean },
}, },
actions: { actions: {
setEngineTheme: () => {}, setEngineTheme: () => {},

View File

@ -261,10 +261,30 @@ app.on('ready', () => {
autoUpdater.checkForUpdates().catch(reportRejection) autoUpdater.checkForUpdates().catch(reportRejection)
}, fifteenMinutes) }, fifteenMinutes)
autoUpdater.on('error', (error) => {
console.error('updater-error', error)
mainWindow?.webContents.send('updater-error', error)
})
autoUpdater.on('update-available', (info) => { autoUpdater.on('update-available', (info) => {
console.log('update-available', info) console.log('update-available', info)
}) })
autoUpdater.prependOnceListener('download-progress', (progress) => {
// For now, we'll send nothing and just start a loading spinner.
// See below for a TODO to send progress data to the renderer.
console.log('update-download-start', {
version: '',
})
mainWindow?.webContents.send('update-download-start', progress)
})
autoUpdater.on('download-progress', (progress) => {
// TODO: in a future PR (https://github.com/KittyCAD/modeling-app/issues/3994)
// send this data to mainWindow to show a progress bar for the download.
console.log('download-progress', progress)
})
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info) console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', info.version) mainWindow?.webContents.send('update-downloaded', info.version)

View File

@ -5,6 +5,7 @@ import os from 'node:os'
import fsSync from 'node:fs' import fsSync from 'node:fs'
import packageJson from '../package.json' import packageJson from '../package.json'
import { MachinesListing } from 'lib/machineManager' import { MachinesListing } from 'lib/machineManager'
import chokidar from 'chokidar'
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args) const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args) const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
@ -15,44 +16,34 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host) ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> => const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow') ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloadStart = (
callback: (value: { version: string }) => void
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
const onUpdateDownloaded = (callback: (value: string) => void) => const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value)) ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart') const appRestart = () => ipcRenderer.invoke('app.restart')
const isMac = os.platform() === 'darwin' const isMac = os.platform() === 'darwin'
const isWindows = os.platform() === 'win32' const isWindows = os.platform() === 'win32'
const isLinux = os.platform() === 'linux' const isLinux = os.platform() === 'linux'
let fsWatchListeners = new Map< let fsWatchListeners = new Map<string, ReturnType<typeof chokidar.watch>>()
string,
{
watcher: fsSync.FSWatcher
callback: (eventType: string, path: string) => void
}
>()
const watchFileOn = ( const watchFileOn = (path: string, callback: (path: string) => void) => {
path: string, const watcherMaybe = fsWatchListeners.get(path)
callback: (eventType: string, path: string) => void if (watcherMaybe) return
) => { const watcher = chokidar.watch(path)
const watcher = fsSync.watch(path) watcher.on('all', callback)
watcher.on('change', callback) fsWatchListeners.set(path, watcher)
fsWatchListeners.set(path, { watcher, callback })
} }
const watchFileOff = (path: string) => { const watchFileOff = (path: string) => {
const entry = fsWatchListeners.get(path) const watcher = fsWatchListeners.get(path)
if (!entry) return if (!watcher) return
const { watcher, callback } = entry watcher.unwatch(path)
watcher.off('change', callback)
watcher.close()
fsWatchListeners.delete(path) fsWatchListeners.delete(path)
} }
const watchFileObliterate = () => {
for (let [pathAsKey] of fsWatchListeners) {
watchFileOff(pathAsKey)
}
fsWatchListeners = new Map()
}
const readFile = (path: string) => fs.readFile(path, 'utf-8') const readFile = (path: string) => fs.readFile(path, 'utf-8')
// It seems like from the node source code this does not actually block but also // It seems like from the node source code this does not actually block but also
// don't trust me on that (jess). // don't trust me on that (jess).
@ -103,7 +94,6 @@ contextBridge.exposeInMainWorld('electron', {
// exported. // exported.
watchFileOn, watchFileOn,
watchFileOff, watchFileOff,
watchFileObliterate,
readFile, readFile,
writeFile, writeFile,
exists, exists,
@ -159,6 +149,8 @@ contextBridge.exposeInMainWorld('electron', {
kittycad, kittycad,
listMachines, listMachines,
getMachineApiIp, getMachineApiIp,
onUpdateDownloadStart,
onUpdateDownloaded, onUpdateDownloaded,
onUpdateError,
appRestart, appRestart,
}) })

View File

@ -176,7 +176,7 @@ const Home = () => {
// Re-read projects listing if the projectDir has any updates. // Re-read projects listing if the projectDir has any updates.
useFileSystemWatcher( useFileSystemWatcher(
() => { async () => {
setProjectsLoaderTrigger(projectsLoaderTrigger + 1) setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
}, },
projectsDir ? [projectsDir] : [] projectsDir ? [projectsDir] : []

View File

@ -434,9 +434,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.19" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -444,9 +444,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.19" version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -934,9 +934,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -949,9 +949,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -959,15 +959,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -976,15 +976,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -993,21 +993,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.70" version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.20" version = "0.2.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1966,12 +1966,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.1" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "oncemutex" name = "oncemutex"
@ -3910,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -3921,9 +3918,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@ -3936,9 +3933,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.43" version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-core", "futures-core",
@ -3949,9 +3946,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -3959,9 +3956,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3972,9 +3969,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.93" version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
[[package]] [[package]]
name = "wasm-lib" name = "wasm-lib"

View File

@ -20,7 +20,7 @@ tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19" toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42" wasm-bindgen-futures = "0.4.44"
[dev-dependencies] [dev-dependencies]
anyhow = "1" anyhow = "1"
@ -35,10 +35,10 @@ uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
futures = "0.3.30" futures = "0.3.31"
js-sys = "0.3.69" js-sys = "0.3.71"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen-futures = { version = "0.4.41", features = ["futures-core-03-stream"] } wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
wasm-streams = "0.4.1" wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]

View File

@ -14,7 +14,7 @@ proc-macro = true
[dependencies] [dependencies]
Inflector = "0.11.4" Inflector = "0.11.4"
convert_case = "0.6.0" convert_case = "0.6.0"
once_cell = "1.19.0" once_cell = "1.20.2"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
regex = "1.10" regex = "1.10"

View File

@ -753,6 +753,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
let tokens = crate::token::lexer(#code_block).unwrap(); let tokens = crate::token::lexer(#code_block).unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())), engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()), fs: std::sync::Arc::new(crate::fs::FileManager::new()),
@ -761,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_someFn {
let tokens = crate::token::lexer("someFn()").unwrap(); let tokens = crate::token::lexer("someFn()").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_someFn {
let tokens = crate::token::lexer("someFn()").unwrap(); let tokens = crate::token::lexer("someFn()").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap(); let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -38,6 +39,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -49,7 +51,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -6,6 +6,7 @@ mod test_examples_my_func {
crate::token::lexer("This is another code block.\nyes sirrr.\nmyFunc").unwrap(); crate::token::lexer("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -17,7 +18,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -39,6 +40,7 @@ mod test_examples_my_func {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmyFunc").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -50,7 +52,7 @@ mod test_examples_my_func {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -6,6 +6,7 @@ mod test_examples_line_to {
crate::token::lexer("This is another code block.\nyes sirrr.\nlineTo").unwrap(); crate::token::lexer("This is another code block.\nyes sirrr.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -17,7 +18,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -39,6 +40,7 @@ mod test_examples_line_to {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nlineTo").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -50,7 +52,7 @@ mod test_examples_line_to {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_min {
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nmin").unwrap(); let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -38,6 +39,7 @@ mod test_examples_min {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmin").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -49,7 +51,7 @@ mod test_examples_min {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_import {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_show {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -5,6 +5,7 @@ mod test_examples_some_function {
let tokens = crate::token::lexer("someFunction()").unwrap(); let tokens = crate::token::lexer("someFunction()").unwrap();
let parser = crate::parser::Parser::new(tokens); let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap(); let program = parser.ast().unwrap();
let id_generator = crate::executor::IdGenerator::default();
let ctx = crate::executor::ExecutorContext { let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new( engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new() crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +17,7 @@ mod test_examples_some_function {
settings: Default::default(), settings: Default::default(),
context_type: crate::executor::ContextType::Mock, context_type: crate::executor::ContextType::Mock,
}; };
ctx.run(&program, None).await.unwrap(); ctx.run(&program, None, id_generator).await.unwrap();
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 5)] #[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -166,15 +166,19 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
Err(e) => return bad_request(format!("Parse error: {e}")), Err(e) => return bad_request(format!("Parse error: {e}")),
}; };
eprintln!("Executing {test_name}"); eprintln!("Executing {test_name}");
let mut id_generator = kcl_lib::executor::IdGenerator::default();
// This is a shitty source range, I don't know what else to use for it though. // This is a shitty source range, I don't know what else to use for it though.
// There's no actual KCL associated with this reset_scene call. // There's no actual KCL associated with this reset_scene call.
if let Err(e) = state.reset_scene(kcl_lib::executor::SourceRange::default()).await { if let Err(e) = state
.reset_scene(&mut id_generator, kcl_lib::executor::SourceRange::default())
.await
{
return kcl_err(e); return kcl_err(e);
} }
// Let users know if the test is taking a long time. // Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>(); let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx); let timer = time_until(done_rx);
let snapshot = match state.execute_and_prepare_snapshot(&program).await { let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await {
Ok(sn) => sn, Ok(sn) => sn,
Err(e) => return kcl_err(e), Err(e) => return kcl_err(e),
}; };

View File

@ -1,6 +1,9 @@
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcl_lib::{errors::KclError, executor::DefaultPlanes}; use kcl_lib::{
errors::KclError,
executor::{DefaultPlanes, IdGenerator},
};
use kittycad_modeling_cmds::{ use kittycad_modeling_cmds::{
self as kcmc, self as kcmc,
id::ModelingCmdId, id::ModelingCmdId,
@ -357,7 +360,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
async fn default_planes(&self, source_range: kcl_lib::executor::SourceRange) -> Result<DefaultPlanes, KclError> { async fn default_planes(
&self,
id_generator: &mut IdGenerator,
source_range: kcl_lib::executor::SourceRange,
) -> Result<DefaultPlanes, KclError> {
if NEED_PLANES { if NEED_PLANES {
{ {
let opt = self.default_planes.read().await.as_ref().cloned(); let opt = self.default_planes.read().await.as_ref().cloned();
@ -366,7 +373,7 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
} }
} // drop the read lock } // drop the read lock
let new_planes = self.new_default_planes(source_range).await?; let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.default_planes.write().await = Some(new_planes.clone()); *self.default_planes.write().await = Some(new_planes.clone());
Ok(new_planes) Ok(new_planes)
@ -375,7 +382,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
} }
} }
async fn clear_scene_post_hook(&self, _source_range: kcl_lib::executor::SourceRange) -> Result<(), KclError> { async fn clear_scene_post_hook(
&self,
_id_generator: &mut IdGenerator,
_source_range: kcl_lib::executor::SourceRange,
) -> Result<(), KclError> {
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use kcl_lib::executor::ExecutorContext; use kcl_lib::executor::{ExecutorContext, IdGenerator};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
settings: Default::default(), settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded, context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
}; };
let _memory = ctx.run(&program, None).await?; let _memory = ctx.run(&program, None, IdGenerator::default()).await?;
let result = result.lock().expect("mutex lock").clone(); let result = result.lock().expect("mutex lock").clone();
Ok(result) Ok(result)

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.20" version = "0.2.21"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -16,13 +16,13 @@ async-recursion = "1.1.1"
async-trait = "0.1.83" async-trait = "0.1.83"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.19", default-features = false, optional = true, features = ["std", "derive"] } clap = { version = "4.5.20", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0" convert_case = "0.6.0"
dashmap = "6.1.0" dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.29", path = "../derive-docs" } derive-docs = { version = "0.1.29", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.31" }
git_rev = "0.1.0" git_rev = "0.1.0"
gltf-json = "1.4.1" gltf-json = "1.4.1"
http = { workspace = true } http = { workspace = true }
@ -53,11 +53,11 @@ winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false } zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.71" }
tokio = { version = "1.40.0", features = ["sync", "time"] } tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] } tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42" wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.69", features = ["console"] } web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@ -787,12 +787,14 @@ fn test_generate_stdlib_json_schema() {
let stdlib = StdLib::new(); let stdlib = StdLib::new();
let combined = stdlib.combined(); let combined = stdlib.combined();
let mut json_data = vec![]; let json_data: Vec<_> = combined
.keys()
for key in combined.keys().sorted() { .sorted()
let internal_fn = combined.get(key).unwrap(); .map(|key| {
json_data.push(internal_fn.to_json().unwrap()); let internal_fn = combined.get(key).unwrap();
} internal_fn.to_json().unwrap()
})
.collect();
expectorate::assert_contents( expectorate::assert_contents(
"../../../docs/kcl/std.json", "../../../docs/kcl/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(), &serde_json::to_string_pretty(&json_data).unwrap(),

View File

@ -83,6 +83,8 @@ impl StdLibFnArg {
return Ok(Some((index, format!("${{{}:{}}}", index, "myTag")))); return Ok(Some((index, format!("${{{}:{}}}", index, "myTag"))));
} else if self.type_ == "[KclValue]" && self.required { } else if self.type_ == "[KclValue]" && self.required {
return Ok(Some((index, format!("${{{}:{}}}", index, "[0..9]")))); return Ok(Some((index, format!("${{{}:{}}}", index, "[0..9]"))));
} else if self.type_ == "KclValue" && self.required {
return Ok(Some((index, format!("${{{}:{}}}", index, "3"))));
} }
get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index) get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index)
} }

View File

@ -21,7 +21,7 @@ use tokio_tungstenite::tungstenite::Message as WsMsg;
use crate::{ use crate::{
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::DefaultPlanes, executor::{DefaultPlanes, IdGenerator},
}; };
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -314,7 +314,11 @@ impl EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
async fn default_planes(&self, source_range: crate::executor::SourceRange) -> Result<DefaultPlanes, KclError> { async fn default_planes(
&self,
id_generator: &mut IdGenerator,
source_range: crate::executor::SourceRange,
) -> Result<DefaultPlanes, KclError> {
{ {
let opt = self.default_planes.read().await.as_ref().cloned(); let opt = self.default_planes.read().await.as_ref().cloned();
if let Some(planes) = opt { if let Some(planes) = opt {
@ -322,15 +326,19 @@ impl EngineManager for EngineConnection {
} }
} // drop the read lock } // drop the read lock
let new_planes = self.new_default_planes(source_range).await?; let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.default_planes.write().await = Some(new_planes.clone()); *self.default_planes.write().await = Some(new_planes.clone());
Ok(new_planes) Ok(new_planes)
} }
async fn clear_scene_post_hook(&self, source_range: crate::executor::SourceRange) -> Result<(), KclError> { async fn clear_scene_post_hook(
&self,
id_generator: &mut IdGenerator,
source_range: crate::executor::SourceRange,
) -> Result<(), KclError> {
// Remake the default planes, since they would have been removed after the scene was cleared. // Remake the default planes, since they would have been removed after the scene was cleared.
let new_planes = self.new_default_planes(source_range).await?; let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.default_planes.write().await = Some(new_planes); *self.default_planes.write().await = Some(new_planes);
Ok(()) Ok(())

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