Compare commits

...

24 Commits

Author SHA1 Message Date
e0025f7fad Release KCL 81 (#7463) 2025-06-12 16:20:41 +00:00
2a13888c54 Checkbox for variable creation in command bar flows (#7452)
* Draft: Constrain with Expression: prevent throwaway variables
Fixes #7252

* WIP tests
2025-06-12 10:58:07 -04:00
1443f3ab39 Improve error messages around PI and other numbers with unknown units (#7457)
* Improve docs around PI

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor and polish type error messages

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Add suggestion to fix unknown numbers error

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Don't warn so often about unknown units

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-06-12 10:20:04 -04:00
bf87c23ea8 [Fix]: All 404 links in the repo (#7448)
* fix: 404 fixes only

* fix: auto fmt
2025-06-12 09:18:57 -05:00
5d23b0e487 add broken tests (#7445)
* add broken tests

* spelling work around
2025-06-12 11:33:46 +00:00
df6c81b0b4 Units bug fix with modulo (#7446)
* Add degrees annotations to examples

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix a units bug with the modulo operation

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-06-12 08:44:55 +12:00
5f1f579d4b Quick follow up on share link tests (#7401)
* pierremtb/adhoc-quick-sharelink-follow-up

* Update snapshots

* Update snapshots

* Remove Linux from 2000 tests

* Revert "Remove Linux from 2000 tests"

This reverts commit ad9bc58dd7.

* Remove Linux from 2000 tests and guard linux in the app too

* Revert "Remove Linux from 2000 tests and guard linux in the app too"

This reverts commit e13983eb12.

* Break out test in two

* Not

* Add debug prints

* Try to force userAgent in pw config

* Revert "Try to force userAgent in pw config"

This reverts commit d1e6d7c7be.

* Clean up

* Add sad fix

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-11 13:24:38 -04:00
9a549ff379 Track artifact commands and operations per-module (#7426)
* Change so that operations are stored per module

* Refactor so that all modeling commands go through ExecState

* Remove unneeded PartialOrd implementations

* Remove artifact_commands from KclError since it was only for debugging

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-11 01:30:48 +00:00
851ea28bd3 Change trig functions to return number with Default units (#7425)
* Change trig functions to return number with Default units

* Update docs

* Update output
2025-06-11 00:24:48 +00:00
ff15c7b9db Extend idle threshold to 8 hours if execution takes longer than 5 min. (#7440)
Extend idle threshold to 8 hours if execution takes longer than 5 minutes
2025-06-10 16:27:54 -04:00
f304577d5d Clean up build-apps upload artifact names (#7427) 2025-06-10 10:41:08 -04:00
b03b0d3b53 #7318 Remember desktop app size, position on screen (#7322)
* desktop app should remember the last window size

* localStorage doesnt work in electron, use a settings file

* save bounds (position too)

* only restore saved bounds if its still valid for current displays

* typo

* remove logs

* cleanup

* typo

* add version to LastWindowConfig

* rename window_config.json to device_state.json
2025-06-10 11:38:44 +02:00
dd4d0f6d98 Rename nightly to staging and have it point to dev infra (#7422)
* Rename nightly to staging and have it point to dev infra
Fixes #7421

* To revert: force IS_STAGING

* chmod +x ./scripts/flip-files-to-staging.sh

* Fix mix up dev and prod

* Revert "To revert: force IS_STAGING"

This reverts commit 0178604a55.
2025-06-09 15:43:48 -04:00
1cd742df5d Ensure all E2E artifacts have unique names (#7417) 2025-06-09 11:05:56 -04:00
6460ed8ea8 bump the client API response timeout 60 -> 300 (#7406) 2025-06-06 16:02:42 -07:00
5c51b27f29 Use namespace for build-apps macos and windows (#7400)
* WIP: Use namespace windows-2-cores for build-apps

* WIP: 8-cores to see if it's different

* WIP: macos-6-cores and windows-4-cores

* To revert: test IS_RELEASE

* Change d drive to c for cert

* Maintain job naming

* Revert "To revert: test IS_RELEASE"

This reverts commit 5d4eadce6c.

* Change job names
2025-06-06 17:03:24 -04:00
77690b4419 Add touch support to camera while in modeling mode (#7384)
* Add HammerJS

* Fmt and little type cleanup

* Implement multi-touch through HammerJS

* Add velocity-decay "flick" behavior for orbit

* Update src/clientSideScene/CameraControls.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* tsc fix

* Update src/clientSideScene/CameraControls.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Release KCL 80 (#7391)

* Check for updates button in moar menus & toasts (#7369)

* Check for update button in more menus
Fixes #7368

* Add menubar item

* Another one

* Add Checking for updates... and No new update toasts

* Lint

* Trigger CI

* Update src/main.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update electron-builder.yml

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update electron-builder.yml

* Moar clean up

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Format examples in docs (#7378)

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix some typos in previous PR (#7392)

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Remove the untyped getters from std::args (#7377)

* Move last uses of untypeed arg getters

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Rename _typed functions

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* WIP #7226 Fix remove constraints (#7304)

* handle if graphSelections is empty

* fix removeConstrainingValuesInfo by using pathToNodes if available instead of selectionRanges: current selection should not be required to remove constraints

* selectionRanges not needed for removeConstrainingValuesInfo anymore

* fix remove constraint unit test: pass line's pathToNode instead of argument to remove constraint

* Change to use artifact pathToNode (#7361)

* Change to use artifact pathToNode

* Fix to do bounds checking

* move TTC capture to unit test (#7268)

* move TTC capture to unit test

* progress with artifact

* fmt

* abstract cases

* add another case

* add another test

* update snapshots with proper file names

* force to JSON

* fmt

* make jest happy

* add another example and other tweaks

* fix

* tweak

* add logs

* more logs

* strip out kcl version

* remove logs

* add comment explainer

* more comments

* more comment

* remove package-lock line

* Add support for tag on close segment when the last sketch edge is missing (#7375)

* add test

* fix

* Update snapshots

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Use namespace for windows e2e tests (#7398)

* Use namespace for windows e2e tests

* Change to the new profile

* Remove TODO

* Commit new snapshots even if some tests failed (#7399)

* Commit new snapshots even if some tests failed

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Clean up share link tests (#7372)

* pierremtb/adhoc/clean-up-share-link-tests

* Lint

* WIP labels

* Trigger CI

* Change to skips

* Remove old docs files (#7381)

* Remove old files; no longer generated.

* Update snapshots

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jace Browning <jacebrowning@gmail.com>

* #7199 Fix broken links in docs (#7397)

* update broken links

* update github discussion links, fmt

* update comment

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>

* Inline engine issue from @Irev-Dev

* Add commented-out test to be implemented later https://github.com/KittyCAD/modeling-app/issues/7403

* Update e2e/playwright/test-utils.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Andrew Varga <grizzly33@gmail.com>
Co-authored-by: max <margorskyi@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jace Browning <jacebrowning@gmail.com>
Co-authored-by: Nick McCleery <34814836+nickmccleery@users.noreply.github.com>
2025-06-06 20:04:20 +00:00
6996670020 fix edge case of sketch not being exited out of properly (#7396)
* fix edge case of sketch not being exited out of properly

* Update e2e/playwright/sketch-tests.spec.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-06-07 03:20:06 +10:00
1fd4e93091 reexecute KCL code when reloading due to external file modification (#7293)
Co-authored-by: Lucas Kent <rubickent@gmail.com>
2025-06-06 12:44:40 -04:00
a1ac029333 #7199 Fix broken links in docs (#7397)
* update broken links

* update github discussion links, fmt

* update comment

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-06-06 11:52:56 -04:00
29cf16d744 Remove old docs files (#7381)
* Remove old files; no longer generated.

* Update snapshots

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-06-06 15:47:40 +00:00
9b3afccf53 Clean up share link tests (#7372)
* pierremtb/adhoc/clean-up-share-link-tests

* Lint

* WIP labels

* Trigger CI

* Change to skips
2025-06-06 11:46:55 -04:00
231ca0fa35 Commit new snapshots even if some tests failed (#7399)
* Commit new snapshots even if some tests failed

* Update snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-06 10:01:07 -04:00
4608c02442 Use namespace for windows e2e tests (#7398)
* Use namespace for windows e2e tests

* Change to the new profile

* Remove TODO
2025-06-06 09:40:14 -04:00
616 changed files with 11579700 additions and 11566121 deletions

View File

@ -10,7 +10,7 @@ on:
env: env:
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
IS_NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} IS_STAGING: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -91,14 +91,14 @@ jobs:
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }} if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "npm run build:wasm" run: "npm run build:wasm"
- name: Set nightly version, product name, release notes, and icons - name: Set staging version, product name, release notes, and icons
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_STAGING == 'true' }}
run: | run: |
COMMIT=$(git rev-parse --short HEAD) COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +'%-y.%-m.%-d') DATE=$(date +'%-y.%-m.%-d')
export VERSION=$DATE-main.$COMMIT export VERSION=$DATE-main.$COMMIT
npm run files:set-version npm run files:set-version
npm run files:flip-to-nightly npm run files:flip-to-staging
- name: Set release version - name: Set release version
if: ${{ env.IS_RELEASE == 'true' }} if: ${{ env.IS_RELEASE == 'true' }}
@ -130,13 +130,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: macos-14 - os: namespace-profile-macos-6-cores
platform: mac platform: macos
- os: windows-2022 - os: namespace-profile-windows-4-cores
platform: win platform: windows
- os: ubuntu-22.04 - os: ubuntu-22.04
platform: linux platform: linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: build-apps (${{ matrix.platform }})
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
steps: steps:
@ -166,14 +167,14 @@ jobs:
- run: npm install - run: npm install
- name: Prepare certificate and variables (Windows only) - name: Prepare certificate and variables (Windows only)
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }} if: ${{ (env.IS_RELEASE == 'true' || env.IS_STAGING == 'true') && matrix.platform == 'windows' }}
run: | run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /c/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12 cat /c/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" echo "SM_CLIENT_CERT_FILE=C:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
@ -181,7 +182,7 @@ jobs:
shell: bash shell: bash
- name: Setup certicate with SSM KSP (Windows only) - name: Setup certicate with SSM KSP (Windows only)
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }} if: ${{ (env.IS_RELEASE == 'true' || env.IS_STAGING == 'true') && matrix.platform == 'windows' }}
run: | run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn msiexec /i smtools-windows-x64.msi /quiet /qn
@ -191,7 +192,7 @@ jobs:
smksp_cert_sync.exe smksp_cert_sync.exe
smctl windows certsync smctl windows certsync
# This last line `smctl windows certsync` was added after windows codesign failures started happening # This last line `smctl windows certsync` was added after windows codesign failures started happening
# with nightly-v25.4.10. It looks like `smksp_cert_sync.exe` used to do the sync to the local cert store, # with staging-v25.4.10. It looks like `smksp_cert_sync.exe` used to do the sync to the local cert store,
# but stopped doing it overnight. This extra call that I randomly got from this azure-related doc page # but stopped doing it overnight. This extra call that I randomly got from this azure-related doc page
# https://docs.digicert.com/en/digicert-keylocker/code-signing/sign-with-third-party-signing-tools/windows-applications/sign-azure-apps-with-signtool-using-ksp-library.html#sync-certificates--windows-only--618365 # https://docs.digicert.com/en/digicert-keylocker/code-signing/sign-with-third-party-signing-tools/windows-applications/sign-azure-apps-with-signtool-using-ksp-library.html#sync-certificates--windows-only--618365
# seems to be doing that extra sync that we need for scripts/sign-win.js to work. # seems to be doing that extra sync that we need for scripts/sign-win.js to work.
@ -199,13 +200,13 @@ jobs:
shell: cmd shell: cmd
- name: Build the app (debug) - name: Build the app (debug)
if: ${{ env.IS_RELEASE == 'false' && env.IS_NIGHTLY == 'false' }} if: ${{ env.IS_RELEASE == 'false' && env.IS_STAGING == 'false' }}
# electron-builder doesn't have a concept of release vs debug, # electron-builder doesn't have a concept of release vs debug,
# this is just not doing any codesign or release yml generation, and points to dev infra # this is just not doing any codesign or release yml generation, and points to dev infra
run: npm run tronb:package:dev run: npm run tronb:package:dev
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -215,7 +216,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: npm run tronb:package:prod run: npm run tronb:package:${{ env.IS_STAGING == 'true' && 'dev' || 'prod' }}
- name: List artifacts in out/ - name: List artifacts in out/
run: ls -R out run: ls -R out
@ -239,20 +240,20 @@ jobs:
out/*-x86_64-linux.* out/*-x86_64-linux.*
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
with: with:
name: out-yml-${{ matrix.platform }} name: out-yml-${{ matrix.platform }}
path: | path: |
out/latest*.yml out/latest*.yml
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight' stage back
upload-apps-release: upload-apps-release:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
contents: write contents: write
# Equivalent to IS_RELEASE || IS_NIGHTLY (but we can't access those env vars here) # Equivalent to IS_RELEASE || IS_STAGING (but we can't access those env vars here)
if: ${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} if: ${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
@ -263,32 +264,32 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-win name: out-arm64-windows
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-win name: out-x64-windows
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-win name: out-yml-windows
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-mac name: out-arm64-macos
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-mac name: out-x64-macos
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-mac name: out-yml-macos
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
@ -310,8 +311,8 @@ jobs:
env: env:
NOTES: ${{ needs.prepare-files.outputs.notes }} NOTES: ${{ needs.prepare-files.outputs.notes }}
PUB_DATE: ${{ github.event.repository.updated_at }} PUB_DATE: ${{ github.event.repository.updated_at }}
WEBSITE_DIR: ${{ env.IS_NIGHTLY == 'true' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} WEBSITE_DIR: ${{ env.IS_STAGING == 'true' && 'dl.zoo.dev/releases/modeling-app/staging' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ env.IS_NIGHTLY == 'true' && 'Zoo%20Design%20Studio%20%28Nightly%29' || 'Zoo%20Design%20Studio' }} URL_CODED_NAME: ${{ env.IS_STAGING == 'true' && 'Zoo%20Design%20Studio%20%28Staging%29' || 'Zoo%20Design%20Studio' }}
run: | run: |
RELEASE_DIR=https://${WEBSITE_DIR} RELEASE_DIR=https://${WEBSITE_DIR}
jq --null-input \ jq --null-input \
@ -360,26 +361,26 @@ jobs:
run: "ls -R out" run: "ls -R out"
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_STAGING == 'true' }}
uses: 'google-github-actions/auth@v2.1.8' uses: 'google-github-actions/auth@v2.1.8'
with: with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}' credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK - name: Set up Google Cloud SDK
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_STAGING == 'true' }}
uses: google-github-actions/setup-gcloud@v2.1.4 uses: google-github-actions/setup-gcloud@v2.1.4
with: with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload nightly files to public bucket - name: Upload staging files to public bucket
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_STAGING == 'true' }}
uses: google-github-actions/upload-cloud-storage@v2.2.2 uses: google-github-actions/upload-cloud-storage@v2.2.2
with: with:
path: out path: out
glob: '*' glob: '*'
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/staging'
- name: Invalidate bucket cache on latest*.yml and last_download.json files - name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_STAGING == 'true' }}
run: npm run files:invalidate-bucket:nightly run: npm run files:invalidate-bucket:staging

View File

@ -177,16 +177,16 @@ jobs:
TARGET: web TARGET: web
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() }}
with: with:
name: playwright-report-ubuntu-snapshot-${{ github.sha }} name: playwright-report-snapshot-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- name: Check diff - name: Check diff
if: ${{ github.ref != 'refs/heads/main' }} if: ${{ always() && github.ref != 'refs/heads/main' }}
shell: bash shell: bash
id: git-check id: git-check
run: | run: |
@ -197,7 +197,7 @@ jobs:
fi fi
- name: Commit changes - name: Commit changes
if: ${{ steps.git-check.outputs.modified == 'true' }} if: ${{ always() && steps.git-check.outputs.modified == 'true' }}
shell: bash shell: bash
run: | run: |
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
@ -220,7 +220,7 @@ jobs:
include: include:
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64" - os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
- os: namespace-profile-macos-8-cores - os: namespace-profile-macos-8-cores
- os: windows-latest-8-cores - os: namespace-profile-windows-8-cores
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: e2e:web (${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}) name: e2e:web (${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }})
env: env:
@ -293,6 +293,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-web-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
@ -304,7 +305,6 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# TODO: enable namespace-profile-windows-latest once available
include: include:
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64" - os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
shardIndex: 1 shardIndex: 1
@ -336,10 +336,10 @@ jobs:
- os: namespace-profile-macos-8-cores - os: namespace-profile-macos-8-cores
shardIndex: 2 shardIndex: 2
shardTotal: 2 shardTotal: 2
- os: windows-latest-8-cores - os: namespace-profile-windows-8-cores
shardIndex: 1 shardIndex: 1
shardTotal: 2 shardTotal: 2
- os: windows-latest-8-cores - os: namespace-profile-windows-8-cores
shardIndex: 2 shardIndex: 2
shardTotal: 2 shardTotal: 2
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -418,7 +418,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-desktop-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
@ -427,7 +427,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: playwright-report-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-desktop-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30

View File

@ -31,42 +31,42 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-win name: out-arm64-windows
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-win name: out-x64-windows
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-win name: out-yml-windows
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-mac name: out-arm64-macos
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-mac name: out-x64-macos
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-mac name: out-yml-macos
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

@ -46,3 +46,7 @@ KCL has no support for area, volume, or other higher dimension units. When inter
## Explicit conversions ## Explicit conversions
You might sometimes need to convert from one unit to another for some calculation. You can do this implicitly when calling a function (see above), but if you can't or don't want to, then you can use the explicit conversion functions in the [`std::units`](/docs/kcl-std/modules/std-units) module. You might sometimes need to convert from one unit to another for some calculation. You can do this implicitly when calling a function (see above), but if you can't or don't want to, then you can use the explicit conversion functions in the [`std::units`](/docs/kcl-std/modules/std-units) module.
KCL cannot know about changes to units caused by arithmetic. For example, you may intend for `10in * 25.4` to be the value `254mm` (i.e., `10in` in mm), however, the result of that computation in KCL is `254in`. It is always better to rely on automatic conversion or to use the explicit conversion functions, where possible.
Converting between degrees and radians using π ([`PI`](/docs/kcl-std/consts/std-math-PI) in KCL) is especially prone to this error and so the `PI` constant always requires specifying units of any computation it is used with. E.g., `radius = (circumference / (2 * PI)): number(mm)`.

File diff suppressed because one or more lines are too long

View File

@ -1,33 +0,0 @@
---
title: "KCL Constants"
excerpt: "Documentation for the KCL constants."
layout: manual
---
## Table of Contents
### `std`
- [`END`](/docs/kcl/consts/std-END)
- [`START`](/docs/kcl/consts/std-START)
- [`X`](/docs/kcl/consts/std-X)
- [`XY`](/docs/kcl/consts/std-XY)
- [`XZ`](/docs/kcl/consts/std-XZ)
- [`Y`](/docs/kcl/consts/std-Y)
- [`YZ`](/docs/kcl/consts/std-YZ)
- [`Z`](/docs/kcl/consts/std-Z)
### `std::math`
- [`E`](/docs/kcl/consts/std-math-E)
- [`PI`](/docs/kcl/consts/std-math-PI)
- [`TAU`](/docs/kcl/consts/std-math-TAU)
### `std::turns`
- [`HALF_TURN`](/docs/kcl/consts/std-turns-HALF_TURN)
- [`QUARTER_TURN`](/docs/kcl/consts/std-turns-QUARTER_TURN)
- [`THREE_QUARTER_TURN`](/docs/kcl/consts/std-turns-THREE_QUARTER_TURN)
- [`ZERO`](/docs/kcl/consts/std-turns-ZERO)

View File

@ -18,7 +18,7 @@ E: number = 2.71828182845904523536028747135266250_
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 30, length = 2 * E ^ 2) |> angledLine(angle = 30deg, length = 2 * E ^ 2)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -16,8 +16,8 @@ However, `PI` is nearly always used for converting between different units - usu
from radians. Therefore, `PI` is treated a bit specially by KCL and always has unknown units. This from radians. Therefore, `PI` is treated a bit specially by KCL and always has unknown units. This
means that if you use `PI`, you will need to give KCL some extra information about the units of numbers. means that if you use `PI`, you will need to give KCL some extra information about the units of numbers.
Usually you should use type ascription on the result of calculations, e.g., `(2 * PI): number(rad)`. Usually you should use type ascription on the result of calculations, e.g., `(2 * PI): number(rad)`.
You might prefer to use `units::toRadians` or `units::toDegrees` to convert between angles with It is better to use `units::toRadians` or `units::toDegrees` to convert between angles with
different units. different units where possible.
### Examples ### Examples

View File

@ -18,7 +18,7 @@ TAU: number = 6.28318530717958647692528676655900577_
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = 10 * TAU) |> angledLine(angle = 50deg, length = 10 * TAU)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
No turn, zero degrees/radians. No turn, zero degrees/radians.
```kcl ```kcl
turns::ZERO: number = 0 turns::ZERO
``` ```

View File

@ -27,7 +27,7 @@ abs(@input: number): number
### Examples ### Examples
```kcl ```kcl
myAngle = -120 myAngle = -120deg
sketch001 = startSketchOn(XZ) sketch001 = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])

View File

@ -8,7 +8,7 @@ layout: manual
Compute the cosine of a number. Compute the cosine of a number.
```kcl ```kcl
cos(@num: number(Angle)): number(_) cos(@num: number(Angle)): number
``` ```
@ -21,7 +21,7 @@ cos(@num: number(Angle)): number(_)
### Returns ### Returns
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number. [`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ cos(@num: number(Angle)): number(_)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 30, length = 3 / cos(30deg)) |> angledLine(angle = 30deg, length = 3 / cos(30deg))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ max(@input: [number; 1+]): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 70, length = max([15, 31, 4, 13, 22])) |> angledLine(angle = 70deg, length = max([15, 31, 4, 13, 22]))
|> line(end = [20, 0]) |> line(end = [20, 0])
|> close() |> close()

View File

@ -29,7 +29,7 @@ min(@input: [number; 1+]): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 70, length = min([15, 31, 4, 13, 22])) |> angledLine(angle = 70deg, length = min([15, 31, 4, 13, 22]))
|> line(end = [20, 0]) |> line(end = [20, 0])
|> close() |> close()

View File

@ -33,7 +33,7 @@ polar(
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = polar(angle = 30, length = 5), tag = $thing) |> line(end = polar(angle = 30deg, length = 5), tag = $thing)
|> line(end = [0, 5]) |> line(end = [0, 5])
|> line(end = [segEndX(thing), 0]) |> line(end = [segEndX(thing), 0])
|> line(end = [-20, 10]) |> line(end = [-20, 10])

View File

@ -33,7 +33,7 @@ pow(
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = pow(5, exp = 2)) |> angledLine(angle = 50deg, length = pow(5, exp = 2))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
Compute the sine of a number. Compute the sine of a number.
```kcl ```kcl
sin(@num: number(Angle)): number(_) sin(@num: number(Angle)): number
``` ```
@ -21,7 +21,7 @@ sin(@num: number(Angle)): number(_)
### Returns ### Returns
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number. [`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ sin(@num: number(Angle)): number(_)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = 15 / sin(135deg)) |> angledLine(angle = 50deg, length = 15 / sin(135deg))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ sqrt(@input: number): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = sqrt(2500)) |> angledLine(angle = 50deg, length = sqrt(2500))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
Compute the tangent of a number. Compute the tangent of a number.
```kcl ```kcl
tan(@num: number(Angle)): number(_) tan(@num: number(Angle)): number
``` ```
@ -21,7 +21,7 @@ tan(@num: number(Angle)): number(_)
### Returns ### Returns
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number. [`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ tan(@num: number(Angle)): number(_)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = 50 * tan((1 / 2): number(rad))) |> angledLine(angle = 50deg, length = 50 * tan((1 / 2): number(rad)))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -46,7 +46,7 @@ angledLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(endAbsolute = 15) |> yLine(endAbsolute = 15)
|> angledLine(angle = 30, length = 15) |> angledLine(angle = 30deg, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -42,7 +42,7 @@ exampleSketch = startSketchOn(XZ)
|> line(endAbsolute = [5, 10]) |> line(endAbsolute = [5, 10])
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect) |> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|> line(endAbsolute = [0, 20]) |> line(endAbsolute = [0, 20])
|> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10) |> angledLineThatIntersects(angle = 80deg, intersectTag = lineToIntersect, offset = 10)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -53,7 +53,7 @@ for to construct your shape, you're likely looking for tangentialArc.
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> arc(angleStart = 0, angleEnd = 280, radius = 16) |> arc(angleStart = 0, angleEnd = 280deg, radius = 16)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -43,7 +43,7 @@ extruded in the same direction.
example = startSketchOn(XZ) example = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> arc(angleStart = 120, angleEnd = 0, radius = 5) |> arc(angleStart = 120deg, angleEnd = 0, radius = 5)
|> line(end = [5, 0]) |> line(end = [5, 0])
|> line(end = [0, 10]) |> line(end = [0, 10])
|> bezierCurve(control1 = [-10, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-10, 0], control2 = [2, 10], end = [-5, 10])
@ -58,7 +58,7 @@ example = startSketchOn(XZ)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120, angleEnd = -60, radius = 5) |> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
@ -75,7 +75,7 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120, angleEnd = -60, radius = 5) |> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
@ -92,7 +92,7 @@ example = extrude(exampleSketch, length = 20, symmetric = true)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120, angleEnd = -60, radius = 5) |> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])

View File

@ -30,10 +30,10 @@ getNextAdjacentEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(angle = 60deg, length = 10)
|> angledLine(angle = 120, length = 10) |> angledLine(angle = 120deg, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240, length = 10, tag = $referenceEdge) |> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -30,10 +30,10 @@ getOppositeEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(angle = 60deg, length = 10)
|> angledLine(angle = 120, length = 10) |> angledLine(angle = 120deg, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240, length = 10, tag = $referenceEdge) |> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -30,10 +30,10 @@ getPreviousAdjacentEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(angle = 60deg, length = 10)
|> angledLine(angle = 120, length = 10) |> angledLine(angle = 120deg, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240, length = 10, tag = $referenceEdge) |> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -43,11 +43,11 @@ a = 10
b = 14 b = 14
startSketchOn(XZ) startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> involuteCircular(startRadius = a, endRadius = b, angle = 60) |> involuteCircular(startRadius = a, endRadius = b, angle = 60deg)
|> involuteCircular( |> involuteCircular(
startRadius = a, startRadius = a,
endRadius = b, endRadius = b,
angle = 60, angle = 60deg,
reverse = true, reverse = true,
) )

View File

@ -30,7 +30,7 @@ profileStart(@profile: Sketch): Point2d
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = 120, length = 50, tag = $seg01) |> angledLine(angle = 120, length = 50, tag = $seg01)
|> angledLine(angle = segAng(seg01) + 120, length = 50) |> angledLine(angle = segAng(seg01) + 120deg, length = 50)
|> line(end = profileStart(%)) |> line(end = profileStart(%))
|> close() |> close()
|> extrude(length = 20) |> extrude(length = 20)

View File

@ -30,8 +30,8 @@ profileStartX(@profile: Sketch): number(Length)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = -26.6, length = 50) |> angledLine(angle = -26.6, length = 50)
|> angledLine(angle = 90, length = 50) |> angledLine(angle = 90deg, length = 50)
|> angledLine(angle = 30, endAbsoluteX = profileStartX(%)) |> angledLine(angle = 30deg, endAbsoluteX = profileStartX(%))
``` ```

View File

@ -29,8 +29,8 @@ profileStartY(@profile: Sketch): number(Length)
```kcl ```kcl
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = -60, length = 14) |> angledLine(angle = -60deg, length = 14)
|> angledLine(angle = 30, endAbsoluteY = profileStartY(%)) |> angledLine(angle = 30deg, endAbsoluteY = profileStartY(%))
``` ```

View File

@ -62,7 +62,7 @@ part001 = startSketchOn(XY)
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = Y) // default angle is 360 |> revolve(axis = Y) // default angle is 360deg
``` ```
@ -72,7 +72,7 @@ part001 = startSketchOn(XY)
// A donut shape. // A donut shape.
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5) |> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = Y) |> revolve(angle = 360deg, axis = Y)
``` ```
@ -89,7 +89,7 @@ part001 = startSketchOn(XY)
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = Y, angle = 180) |> revolve(axis = Y, angle = 180deg)
``` ```
@ -106,7 +106,7 @@ part001 = startSketchOn(XY)
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = Y, angle = 180) |> revolve(axis = Y, angle = 180deg)
part002 = startSketchOn(part001, face = END) part002 = startSketchOn(part001, face = END)
|> startProfile(at = [4.5, -5]) |> startProfile(at = [4.5, -5])
@ -131,7 +131,7 @@ box = startSketchOn(XY)
sketch001 = startSketchOn(box, face = END) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> circle(center = [10, 10], radius = 4)
|> revolve(angle = -90, axis = Y) |> revolve(angle = -90deg, axis = Y)
``` ```
@ -148,7 +148,7 @@ box = startSketchOn(XY)
sketch001 = startSketchOn(box, face = END) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> circle(center = [10, 10], radius = 4)
|> revolve(angle = 90, axis = getOppositeEdge(revolveAxis)) |> revolve(angle = 90deg, axis = getOppositeEdge(revolveAxis))
``` ```
@ -165,7 +165,7 @@ box = startSketchOn(XY)
sketch001 = startSketchOn(box, face = END) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> circle(center = [10, 10], radius = 4)
|> revolve(angle = 90, axis = getOppositeEdge(revolveAxis), tolerance = 0.0001) |> revolve(angle = 90deg, axis = getOppositeEdge(revolveAxis), tolerance = 0.0001)
``` ```
@ -229,7 +229,7 @@ profile001 = startSketchOn(XY)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> circle(center = [-10, 10], radius = 4)
|> revolve(angle = 90, axis = revolveAxis) |> revolve(angle = 90deg, axis = revolveAxis)
``` ```
@ -246,7 +246,7 @@ profile001 = startSketchOn(XY)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> circle(center = [-10, 10], radius = 4)
|> revolve(angle = 90, axis = revolveAxis) |> revolve(angle = 90deg, axis = revolveAxis)
``` ```
@ -263,7 +263,7 @@ profile001 = startSketchOn(XY)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> circle(center = [-10, 10], radius = 4)
|> revolve(angle = 90, axis = revolveAxis, symmetric = true) |> revolve(angle = 90deg, axis = revolveAxis, symmetric = true)
``` ```
@ -280,7 +280,7 @@ profile001 = startSketchOn(XY)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> circle(center = [-10, 10], radius = 4)
|> revolve(angle = 90, axis = revolveAxis, bidirectionalAngle = 50) |> revolve(angle = 90deg, axis = revolveAxis, bidirectionalAngle = 50)
``` ```

View File

@ -30,8 +30,8 @@ segLen(@tag: tag): number(Length)
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10, tag = $thing) |> angledLine(angle = 60, length = 10, tag = $thing)
|> tangentialArc(angle = -120, radius = 5) |> tangentialArc(angle = -120deg, radius = 5)
|> angledLine(angle = -60, length = segLen(thing)) |> angledLine(angle = -60deg, length = segLen(thing))
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -156,7 +156,7 @@ exampleSketch = startSketchOn(XY)
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
example = revolve(exampleSketch, axis = Y, angle = 180) example = revolve(exampleSketch, axis = Y, angle = 180deg)
exampleSketch002 = startSketchOn(example, face = END) exampleSketch002 = startSketchOn(example, face = END)
|> startProfile(at = [4.5, -5]) |> startProfile(at = [4.5, -5])
@ -189,7 +189,7 @@ exampleSketch = startSketchOn(XY)
example = revolve( example = revolve(
exampleSketch, exampleSketch,
axis = Y, axis = Y,
angle = 180, angle = 180deg,
tagEnd = $end01, tagEnd = $end01,
) )

View File

@ -54,9 +54,9 @@ swept along the same path.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -101,7 +101,7 @@ springSketch = startSketchOn(XZ)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61) |> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -111,7 +111,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90) |> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93) |> xLine(length = 384.93)
sweep([rectangleSketch, circleSketch], path = sweepPath) sweep([rectangleSketch, circleSketch], path = sweepPath)
@ -130,7 +130,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90) |> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93) |> xLine(length = 384.93)
sweep(circleSketch, path = sweepPath, sectional = true) sweep(circleSketch, path = sweepPath, sectional = true)

View File

@ -47,7 +47,7 @@ for 'angle' degrees along the imaginary circle.
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 45, length = 10) |> angledLine(angle = 45deg, length = 10)
|> tangentialArc(end = [0, -10]) |> tangentialArc(end = [0, -10])
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> close() |> close()
@ -61,7 +61,7 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(angle = 60deg, length = 10)
|> tangentialArc(endAbsolute = [15, 15]) |> tangentialArc(endAbsolute = [15, 15])
|> line(end = [10, -15]) |> line(end = [10, -15])
|> close() |> close()
@ -75,9 +75,9 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10) |> angledLine(angle = 60deg, length = 10)
|> tangentialArc(radius = 10, angle = -120) |> tangentialArc(radius = 10, angle = -120deg)
|> angledLine(angle = -60, length = 10) |> angledLine(angle = -60deg, length = 10)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -38,10 +38,10 @@ xLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> xLine(length = 15) |> xLine(length = 15)
|> angledLine(angle = 80, length = 15) |> angledLine(angle = 80deg, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> xLine(length = 10) |> xLine(length = 10)
|> angledLine(angle = 120, length = 30) |> angledLine(angle = 120deg, length = 30)
|> xLine(length = -15) |> xLine(length = -15)
|> close() |> close()

View File

@ -38,7 +38,7 @@ yLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(length = 15) |> yLine(length = 15)
|> angledLine(angle = 30, length = 15) |> angledLine(angle = 30deg, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(length = -5) |> yLine(length = -5)
|> close() |> close()

View File

@ -55,7 +55,7 @@ example = extrude(exampleSketch, length = 5)
// Add color to a revolved solid. // Add color to a revolved solid.
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5) |> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = Y) |> revolve(angle = 360deg, axis = Y)
|> appearance(color = '#ff0000', metalness = 90, roughness = 90) |> appearance(color = '#ff0000', metalness = 90, roughness = 90)
``` ```
@ -196,9 +196,9 @@ example = extrude(exampleSketch, length = 1)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
pipeHole = startSketchOn(XY) pipeHole = startSketchOn(XY)

View File

@ -50,7 +50,7 @@ Its properties are:
- `rotation.axis` (a 3D point, defaults to the Z axis) - `rotation.axis` (a 3D point, defaults to the Z axis)
- `rotation.angle` (number of degrees) - `rotation.angle`
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local") - `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
@ -135,7 +135,7 @@ fn transform(@i) {
pow(0.9, exp = i) pow(0.9, exp = i)
], ],
// Turn by 15 degrees each time. // Turn by 15 degrees each time.
rotation = { angle = 15 * i, origin = "local" } rotation = { angle = 15deg * i, origin = "local" }
} }
} }
@ -171,7 +171,7 @@ fn transform(@i) {
return { return {
translate = [0, 0, -i * width], translate = [0, 0, -i * width],
rotation = { rotation = {
angle = 90 * i, angle = 90deg * i,
// Rotate around the overall scene's origin. // Rotate around the overall scene's origin.
origin = "global" origin = "global"
} }
@ -219,7 +219,7 @@ fn transform(@i) {
// Transform functions can return multiple transforms. They'll be applied in order. // Transform functions can return multiple transforms. They'll be applied in order.
return [ return [
{ translate = [30 * i, 0, 0] }, { translate = [30 * i, 0, 0] },
{ rotation = { angle = 45 * i } } { rotation = { angle = 45deg * i } }
] ]
} }
startSketchOn(XY) startSketchOn(XY)

View File

@ -72,9 +72,9 @@ rotation.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -98,9 +98,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -124,9 +124,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -137,7 +137,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2) |> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole) |> subtract2d(tool = pipeHole)
|> sweep(path = sweepPath) |> sweep(path = sweepPath)
|> rotate(axis = Z, angle = 90) |> rotate(axis = Z, angle = 90deg)
``` ```
@ -150,7 +150,7 @@ sweepSketch = startSketchOn(XY)
import "tests/inputs/cube.sldprt" as cube import "tests/inputs/cube.sldprt" as cube
cube cube
|> rotate(axis = [0, 0, 1.0], angle = 9) |> rotate(axis = [0, 0, 1.0], angle = 9deg)
``` ```
@ -163,9 +163,9 @@ cube
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -176,7 +176,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2) |> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole) |> subtract2d(tool = pipeHole)
|> sweep(path = sweepPath) |> sweep(path = sweepPath)
|> rotate(axis = [0, 0, 1.0], angle = 90) |> rotate(axis = [0, 0, 1.0], angle = 90deg)
``` ```
@ -199,13 +199,13 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90) |> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
// Rotate the sweeps. // Rotate the sweeps.
rotate(parts, axis = [0, 0, 1.0], angle = 90) rotate(parts, axis = [0, 0, 1.0], angle = 90deg)
``` ```
@ -228,7 +228,7 @@ profile001 = square()
profile002 = square() profile002 = square()
|> translate(x = 0, y = 0, z = 20) |> translate(x = 0, y = 0, z = 20)
|> rotate(axis = [0, 0, 1.0], angle = 45) |> rotate(axis = [0, 0, 1.0], angle = 45deg)
loft([profile001, profile002]) loft([profile001, profile002])

View File

@ -54,9 +54,9 @@ look like the model moves and gets bigger at the same time. Say you have a squar
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -93,7 +93,7 @@ cube
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61) |> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -103,7 +103,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90) |> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)

View File

@ -47,9 +47,9 @@ and then rotate it using the `rotate` function to create a loft.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5) |> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5) |> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -91,7 +91,7 @@ cube
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61) |> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -101,7 +101,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90) |> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
@ -162,7 +162,7 @@ profile001 = square()
profile002 = square() profile002 = square()
|> translate(z = 20) |> translate(z = 20)
|> rotate(axis = [0, 0, 1.0], angle = 45) |> rotate(axis = [0, 0, 1.0], angle = 45deg)
loft([profile001, profile002]) loft([profile001, profile002])

View File

@ -29,7 +29,7 @@ units::toDegrees(@num: number(Angle)): number(deg)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = 70 * cos(units::toDegrees((PI / 4): number(rad)))) |> angledLine(angle = 50deg, length = 70 * cos(units::toDegrees((PI / 4): number(rad))))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ units::toRadians(@num: number(Angle)): number(rad)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50, length = 70 * cos(units::toRadians(45))) |> angledLine(angle = 50deg, length = 70 * cos(units::toRadians(45deg)))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ startSketchOn(XZ)
|> startProfile(at = origin) |> startProfile(at = origin)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine( |> angledLine(
angle = segAng(rectangleSegmentA001) - 90, angle = segAng(rectangleSegmentA001) - 90deg,
length = 196.99, length = 196.99,
tag = $rectangleSegmentB001, tag = $rectangleSegmentB001,
) )
@ -80,7 +80,7 @@ fn rect(origin) {
|> startProfile(at = origin) |> startProfile(at = origin)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine( |> angledLine(
angle = segAng(rectangleSegmentA001) - 90, angle = segAng(rectangleSegmentA001) - 90deg,
length = 196.99 length = 196.99
tag = $rectangleSegmentB001, tag = $rectangleSegmentB001,
) )

View File

@ -302,7 +302,7 @@ test.describe('Command bar tests', () => {
// Assert that the an alternative variable name is chosen, // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance) // since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click() await cmdBar.variableCheckbox.click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'length001' 'length001'
) )

View File

@ -281,10 +281,7 @@ test.describe('Feature Tree pane', () => {
await test.step('Add a named constant for distance argument and submit', async () => { await test.step('Add a named constant for distance argument and submit', async () => {
await expect(cmdBar.currentArgumentInput).toBeVisible() await expect(cmdBar.currentArgumentInput).toBeVisible()
const addVariableButton = page.getByRole('button', { await cmdBar.variableCheckbox.click()
name: 'Create new variable',
})
await addVariableButton.click()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'review', stage: 'review',

View File

@ -172,6 +172,10 @@ export class CmdBarFixture {
return this.page.getByTestId('cmd-bar-arg-value') return this.page.getByTestId('cmd-bar-arg-value')
} }
get variableCheckbox() {
return this.page.getByTestId('cmd-bar-variable-checkbox')
}
get cmdOptions() { get cmdOptions() {
return this.page.getByTestId('cmd-bar-option') return this.page.getByTestId('cmd-bar-option')
} }
@ -191,7 +195,7 @@ export class CmdBarFixture {
* Clicks the Create new variable button for kcl input * Clicks the Create new variable button for kcl input
*/ */
createNewVariable = async () => { createNewVariable = async () => {
await this.page.getByRole('button', { name: 'Create new variable' }).click() await this.variableCheckbox.click()
} }
/** /**

View File

@ -3807,7 +3807,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
stage: 'arguments', stage: 'arguments',
}) })
await page.keyboard.insertText(newAngle) await page.keyboard.insertText(newAngle)
await page.getByRole('button', { name: 'Create new variable' }).click() await cmdBar.variableCheckbox.click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'angle001' 'angle001'
) )

View File

@ -170,7 +170,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = const crypticErrorText =
'tag requires a value with type `tag`, but found string' 'tag requires a value with type `tag`, but found a value with type `string`.'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -369,7 +369,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = const crypticErrorText =
'tag requires a value with type `tag`, but found string' 'tag requires a value with type `tag`, but found a value with type `string`.'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -408,7 +408,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = const crypticErrorText =
'tag requires a value with type `tag`, but found string' 'tag requires a value with type `tag`, but found a value with type `string`.'
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
} }
) )

View File

@ -1,45 +1,54 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
import type { Page } from '@playwright/test'
async function navigateAndClickOpenInDesktopApp(
page: Page,
codeLength: number
) {
const code = Array(codeLength).fill('0').join('')
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
expect(targetURL.length).toEqual(codeLength + 58)
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
const button = page.getByRole('button', { name: 'Open in desktop app' })
await button.click()
}
function getToastError(page: Page) {
return page.getByText('The URL is too long to open in the desktop app')
}
const isWindows =
navigator.platform === 'Windows' || navigator.platform === 'Win32'
test.describe('Share link tests', () => { test.describe('Share link tests', () => {
;[ test(
{ `Open in desktop app with 2000-long code works non-Windows`,
codeLength: 1000, { tag: ['@web', '@macos', '@linux'] },
showsErrorOnWindows: false, async ({ page }) => {
}, test.skip(process.platform === 'win32')
{ const codeLength = 2000
codeLength: 2000, await navigateAndClickOpenInDesktopApp(page, codeLength)
showsErrorOnWindows: true, await expect(getToastError(page)).not.toBeVisible()
}, }
].forEach(({ codeLength, showsErrorOnWindows }) => { )
test(
`Open in desktop app with ${codeLength}-long code ${isWindows && showsErrorOnWindows ? 'shows error' : "doesn't show error"}`,
{ tag: ['@web'] },
async ({ page }) => {
if (process.env.TARGET !== 'web') {
// This test is web-only
// TODO: re-enable on CI as part of a new @web test suite
return
}
const code = Array(codeLength).fill('0').join('') test(
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true` `Open in desktop app with 1000-long code works on Windows`,
expect(targetURL.length).toEqual(codeLength + 58) { tag: ['@web', '@windows'] },
await page.goto(page.url() + targetURL) async ({ page }) => {
expect(page.url()).toContain(targetURL) test.skip(process.platform !== 'win32')
const button = page.getByRole('button', { name: 'Open in desktop app' }) const codeLength = 1000
await button.click() await navigateAndClickOpenInDesktopApp(page, codeLength)
const toastError = page.getByText( await expect(getToastError(page)).not.toBeVisible()
'The URL is too long to open in the desktop app on Windows' }
) )
if (isWindows && showsErrorOnWindows) {
await expect(toastError).toBeVisible() test(
} else { `Open in desktop app with 2000-long code doesn't work on Windows`,
await expect(toastError).not.toBeVisible() { tag: ['@web', '@windows'] },
// TODO: check if we could verify the deep link dialog shows up async ({ page }) => {
} test.skip(process.platform !== 'win32')
} const codeLength = 2000
) await navigateAndClickOpenInDesktopApp(page, codeLength)
}) await expect(getToastError(page)).toBeVisible()
}
)
}) })

View File

@ -3433,6 +3433,71 @@ profile003 = startProfile(sketch002, at = [-201.08, 254.17])
).toBeVisible() ).toBeVisible()
}) })
}) })
test('Will exit out of sketch mode when all code is nuked', async ({
page,
context,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [106.68, 89.77])
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfile(sketch002, at = [83.39, 329.15])
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
const expectSketchOriginToBeDrawn = async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
}
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
await expectSketchOriginToBeDrawn()
})
await test.step('clear editor content while in sketch mode', async () => {
await editor.replaceCode('', '')
await page.waitForTimeout(100)
await expect(
page.getByText('Unable to maintain sketch mode')
).toBeVisible()
await scene.expectPixelColorNotToBe(
TEST_COLORS.WHITE,
{ x: 672, y: 193 },
15
)
})
})
test('empty draft sketch is cleaned up properly', async ({ test('empty draft sketch is cleaned up properly', async ({
scene, scene,
toolbar, toolbar,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1198,3 +1198,174 @@ export async function enableConsoleLogEverything({
console.log(`[Main] ${msg.type()}: ${msg.text()}`) console.log(`[Main] ${msg.type()}: ${msg.text()}`)
}) })
} }
/**
* Simulate a pan touch gesture from the center of an element.
*
* Adapted from Playwright docs: https://playwright.dev/docs/touch-events
*/
export async function panFromCenter(
locator: Locator,
deltaX = 0,
deltaY = 0,
steps = 5
) {
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
const bounds = target.getBoundingClientRect()
const centerX = bounds.left + bounds.width / 2
const centerY = bounds.top + bounds.height / 2
return { centerX, centerY }
})
// Providing only clientX and clientY as the app only cares about those.
const touches = [
{
identifier: 0,
clientX: centerX,
clientY: centerY,
},
]
await locator.dispatchEvent('touchstart', {
touches,
changedTouches: touches,
targetTouches: touches,
})
for (let j = 1; j <= steps; j++) {
const touches = [
{
identifier: 0,
clientX: centerX + (deltaX * j) / steps,
clientY: centerY + (deltaY * j) / steps,
},
]
await locator.dispatchEvent('touchmove', {
touches,
changedTouches: touches,
targetTouches: touches,
})
}
await locator.dispatchEvent('touchend')
}
/**
* Simulate a 2-finger pan touch gesture from the center of an element.
* with {touchSpacing} pixels between.
*
* Adapted from Playwright docs: https://playwright.dev/docs/touch-events
*/
export async function panTwoFingerFromCenter(
locator: Locator,
deltaX = 0,
deltaY = 0,
steps = 5,
spacingX = 20
) {
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
const bounds = target.getBoundingClientRect()
const centerX = bounds.left + bounds.width / 2
const centerY = bounds.top + bounds.height / 2
return { centerX, centerY }
})
// Providing only clientX and clientY as the app only cares about those.
const touches = [
{
identifier: 0,
clientX: centerX,
clientY: centerY,
},
{
identifier: 1,
clientX: centerX + spacingX,
clientY: centerY,
},
]
await locator.dispatchEvent('touchstart', {
touches,
changedTouches: touches,
targetTouches: touches,
})
for (let j = 1; j <= steps; j++) {
const touches = [
{
identifier: 0,
clientX: centerX + (deltaX * j) / steps,
clientY: centerY + (deltaY * j) / steps,
},
{
identifier: 1,
clientX: centerX + spacingX + (deltaX * j) / steps,
clientY: centerY + (deltaY * j) / steps,
},
]
await locator.dispatchEvent('touchmove', {
touches,
changedTouches: touches,
targetTouches: touches,
})
}
await locator.dispatchEvent('touchend')
}
/**
* Simulate a pinch touch gesture from the center of an element.
* Touch points are set horizontally from each other, separated by {startDistance} pixels.
*/
export async function pinchFromCenter(
locator: Locator,
startDistance = 100,
delta = 0,
steps = 5
) {
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
const bounds = target.getBoundingClientRect()
const centerX = bounds.left + bounds.width / 2
const centerY = bounds.top + bounds.height / 2
return { centerX, centerY }
})
// Providing only clientX and clientY as the app only cares about those.
const touches = [
{
identifier: 0,
clientX: centerX - startDistance / 2,
clientY: centerY,
},
{
identifier: 1,
clientX: centerX + startDistance / 2,
clientY: centerY,
},
]
await locator.dispatchEvent('touchstart', {
touches,
changedTouches: touches,
targetTouches: touches,
})
for (let i = 1; i <= steps; i++) {
const touches = [
{
identifier: 0,
clientX: centerX - startDistance / 2 + (delta * i) / steps,
clientY: centerY,
},
{
identifier: 1,
clientX: centerX + startDistance / 2 + (delta * i) / steps,
clientY: centerY,
},
]
await locator.dispatchEvent('touchmove', {
touches,
changedTouches: touches,
targetTouches: touches,
})
}
await locator.dispatchEvent('touchend')
}

View File

@ -0,0 +1,146 @@
import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
import { type Page } from '@playwright/test'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
test.use({
hasTouch: true,
})
test.describe('Testing Camera Movement (Touch Only)', () => {
/**
* DUPLICATED FROM `testing-camera-movement.spec.ts`, might need to become a util.
*
* hack that we're implemented our own retry instead of using retries built into playwright.
* however each of these camera drags can be flaky, because of udp
* and so putting them together means only one needs to fail to make this test extra flaky.
* this way we can retry within the test
* We could break them out into separate tests, but the longest past of the test is waiting
* for the stream to start, so it can be good to bundle related things together.
*/
const _bakeInRetries = async ({
mouseActions,
afterPosition,
beforePosition,
retryCount = 0,
page,
scene,
}: {
mouseActions: () => Promise<void>
beforePosition: [number, number, number]
afterPosition: [number, number, number]
retryCount?: number
page: Page
scene: SceneFixture
}) => {
const acceptableCamError = 5
const u = await getUtils(page)
await test.step('Set up initial camera position', async () =>
await scene.moveCameraTo({
x: beforePosition[0],
y: beforePosition[1],
z: beforePosition[2],
}))
await test.step('Do actions and watch for changes', async () =>
u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300))
await u.openAndClearDebugPanel()
await expect(page.getByTestId('cam-x-position')).toBeAttached()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const errors = vals.map((v, i) => Math.abs(Number(v) - afterPosition[i]))
let shouldRetry = false
if (errors.some((e) => e > acceptableCamError)) {
if (retryCount > 2) {
console.log('xVal', vals[0], 'xError', errors[0])
console.log('yVal', vals[1], 'yError', errors[1])
console.log('zVal', vals[2], 'zError', errors[2])
throw new Error('Camera position not as expected', {
cause: {
vals,
errors,
},
})
}
shouldRetry = true
}
if (shouldRetry) {
await _bakeInRetries({
mouseActions,
afterPosition: afterPosition,
beforePosition: beforePosition,
retryCount: retryCount + 1,
page,
scene,
})
}
}
// test(
// 'Touch camera controls',
// {
// tag: '@web',
// },
// async ({ page, homePage, scene, cmdBar }) => {
// const u = await getUtils(page)
// const camInitialPosition: [number, number, number] = [0, 85, 85]
//
// await homePage.goToModelingScene()
// await scene.settled(cmdBar)
// const stream = page.getByTestId('stream')
//
// await u.openAndClearDebugPanel()
// await u.closeKclCodePanel()
//
// await test.step('Orbit', async () => {
// await bakeInRetries({
// mouseActions: async () => {
// await panFromCenter(stream, 200, 200)
// await page.waitForTimeout(200)
// },
// afterPosition: [19, 85, 85],
// beforePosition: camInitialPosition,
// page,
// scene,
// })
// })
//
// await test.step('Pan', async () => {
// await bakeInRetries({
// mouseActions: async () => {
// await panTwoFingerFromCenter(stream, 200, 200)
// await page.waitForTimeout(200)
// },
// afterPosition: [19, 85, 85],
// beforePosition: camInitialPosition,
// page,
// scene,
// })
// })
//
// await test.step('Zoom', async () => {
// await bakeInRetries({
// mouseActions: async () => {
// await pinchFromCenter(stream, 300, -100, 5)
// },
// afterPosition: [0, 118, 118],
// beforePosition: camInitialPosition,
// page,
// scene,
// })
// })
// }
// )
})

18
package-lock.json generated
View File

@ -47,6 +47,7 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
@ -93,6 +94,7 @@
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
@ -7491,6 +7493,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/hammerjs": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": { "node_modules/@types/http-cache-semantics": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
@ -15174,6 +15183,15 @@
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
} }
}, },
"node_modules/hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/happy-dom": { "node_modules/happy-dom": {
"version": "17.4.4", "version": "17.4.4",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz",

View File

@ -49,6 +49,7 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
@ -115,10 +116,10 @@
"circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js", "circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-staging": "./scripts/flip-files-to-staging.sh",
"files:flip-to-nightly:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-nightly.ps1", "files:flip-to-staging:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-staging.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh", "files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly", "files:invalidate-bucket:staging": "./scripts/invalidate-files-bucket.sh --staging",
"postinstall": "electron-rebuild", "postinstall": "electron-rebuild",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "tron:start": "electron-forge start",
@ -168,6 +169,7 @@
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",

20
rust/Cargo.lock generated
View File

@ -1792,7 +1792,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1803,7 +1803,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1812,7 +1812,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
@ -1822,7 +1822,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.80" version = "0.2.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1843,7 +1843,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1863,7 +1863,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.80" version = "0.2.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1940,7 +1940,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.80" version = "0.3.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1955,7 +1955,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -1968,7 +1968,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1982,7 +1982,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.80" version = "0.1.81"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.80" version = "0.1.81"
edition = "2021" edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api" repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76" rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-derive-docs" name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.80" version = "0.1.81"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files" description = "A tool for generating tests from a directory of kcl files"
version = "0.1.80" version = "0.1.81"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.80" version = "0.1.81"
edition = "2021" edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server" name = "kcl-language-server"
description = "A language server for KCL." description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.80" version = "0.2.81"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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.80" version = "0.2.81"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -300,12 +300,6 @@ extrude001 = extrude(profile001, length = 4)
let first = &result.first().unwrap().2; let first = &result.first().unwrap().2;
let second = &result.last().unwrap().2; let second = &result.last().unwrap().2;
assert!(
first.artifact_commands.len() < second.artifact_commands.len(),
"Second should have all the artifact commands of the first, plus more. first={:?}, second={:?}",
first.artifact_commands.len(),
second.artifact_commands.len()
);
assert!( assert!(
first.artifact_graph.len() < second.artifact_graph.len(), first.artifact_graph.len() < second.artifact_graph.len(),
"Second should have all the artifacts of the first, plus more. first={:?}, second={:?}", "Second should have all the artifacts of the first, plus more. first={:?}, second={:?}",

View File

@ -1230,7 +1230,10 @@ secondSketch = startSketchOn(part001, face = '')
let result = execute_and_snapshot(code, None).await; let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err(); let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "face requires a value with type `tag`, but found string"); assert_eq!(
err.message(),
"face requires a value with type `tag`, but found a value with type `string`."
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1962,7 +1965,7 @@ someFunction('INVALID')
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(
err.message(), err.message(),
"The input argument of `startSketchOn` requires a value with type `Solid | Plane`, but found string" "The input argument of `startSketchOn` requires a value with type `Solid` or a value with type `Plane` (`Solid | Plane`), but found a value with type `string`."
); );
assert_eq!( assert_eq!(
err.source_ranges(), err.source_ranges(),
@ -2087,7 +2090,7 @@ async fn kcl_test_better_type_names() {
}, },
None => todo!(), None => todo!(),
}; };
assert_eq!(err, "This function expected the input argument to be one or more Solids or imported geometry but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`"); assert_eq!(err, "This function expected the input argument to be one or more Solids or ImportedGeometry but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -508,8 +508,9 @@ impl EngineManager for EngineConnection {
.await?; .await?;
// Wait for the response. // Wait for the response.
let response_timeout = 300;
let current_time = std::time::Instant::now(); let current_time = std::time::Instant::now();
while current_time.elapsed().as_secs() < 60 { while current_time.elapsed().as_secs() < response_timeout {
let guard = self.socket_health.read().await; let guard = self.socket_health.read().await;
if *guard == SocketHealth::Inactive { if *guard == SocketHealth::Inactive {
// Check if we have any pending errors. // Check if we have any pending errors.

View File

@ -135,8 +135,10 @@ pub struct KclErrorWithOutputs {
pub non_fatal: Vec<CompilationError>, pub non_fatal: Vec<CompilationError>,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>, pub operations: Vec<Operation>,
// TODO: Remove this field. Doing so breaks the ts-rs output for some
// reason.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub artifact_commands: Vec<ArtifactCommand>, pub _artifact_commands: Vec<ArtifactCommand>,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph, pub artifact_graph: ArtifactGraph,
pub filenames: IndexMap<ModuleId, ModulePath>, pub filenames: IndexMap<ModuleId, ModulePath>,
@ -162,7 +164,7 @@ impl KclErrorWithOutputs {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations, operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands, _artifact_commands: artifact_commands,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_graph, artifact_graph,
filenames, filenames,
@ -177,7 +179,7 @@ impl KclErrorWithOutputs {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: Default::default(), operations: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(), _artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(), artifact_graph: Default::default(),
filenames: Default::default(), filenames: Default::default(),
@ -781,6 +783,7 @@ impl Severity {
pub enum Tag { pub enum Tag {
Deprecated, Deprecated,
Unnecessary, Unnecessary,
UnknownNumericUnits,
None, None,
} }

View File

@ -45,26 +45,6 @@ pub struct ArtifactCommand {
pub command: ModelingCmd, pub command: ModelingCmd,
} }
impl PartialOrd for ArtifactCommand {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// Order by the source range.
let range = self.range.cmp(&other.range);
if range != std::cmp::Ordering::Equal {
return Some(range);
}
#[cfg(test)]
{
// If the ranges are equal, order by the serde variant.
Some(
crate::variant_name::variant_name(&self.command)
.cmp(&crate::variant_name::variant_name(&other.command)),
)
}
#[cfg(not(test))]
self.cmd_id.partial_cmp(&other.cmd_id)
}
}
pub type DummyPathToNode = Vec<()>; pub type DummyPathToNode = Vec<()>;
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error> fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>

View File

@ -111,8 +111,6 @@ impl GlobalState {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations, operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph, artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors, errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(), default_planes: ctx.engine.get_default_planes().read().await.clone(),

View File

@ -2,6 +2,8 @@ use indexmap::IndexMap;
use serde::Serialize; use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue}; use super::{types::NumericType, ArtifactId, KclValue};
#[cfg(feature = "artifact-graph")]
use crate::parsing::ast::types::{Node, Program};
use crate::{ModuleId, NodePath, SourceRange}; use crate::{ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations /// A CAD modeling operation for display in the feature tree, AKA operations
@ -37,26 +39,6 @@ pub enum Operation {
GroupEnd, GroupEnd,
} }
/// A way for sorting the operations in the timeline. This is used to sort
/// operations in the timeline and to determine the order of operations.
/// We use this for the multi-threaded snapshotting, so that we can have deterministic
/// output.
impl PartialOrd for Operation {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(Self::StdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupBegin { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupEnd, Self::StdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupBegin { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupEnd) => std::cmp::Ordering::Equal,
})
}
}
impl Operation { impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field. /// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) { pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
@ -65,6 +47,25 @@ impl Operation {
Self::GroupBegin { .. } | Self::GroupEnd => {} Self::GroupBegin { .. } | Self::GroupEnd => {}
} }
} }
#[cfg(feature = "artifact-graph")]
pub(crate) fn fill_node_paths(&mut self, program: &Node<Program>, cached_body_items: usize) {
match self {
Operation::StdLibCall {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
} }
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)] #[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]

View File

@ -11,8 +11,8 @@ use crate::{
memory, memory,
state::ModuleState, state::ModuleState,
types::{NumericType, PrimitiveType, RuntimeType}, types::{NumericType, PrimitiveType, RuntimeType},
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, StatementKind, BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, ModelingCmdMeta, ModuleArtifactState,
TagIdentifier, PlaneType, StatementKind, TagIdentifier,
}, },
fmt, fmt,
modules::{ModuleId, ModulePath, ModuleRepr}, modules::{ModuleId, ModulePath, ModuleRepr},
@ -83,7 +83,7 @@ impl ExecutorContext {
preserve_mem: bool, preserve_mem: bool,
module_id: ModuleId, module_id: ModuleId,
path: &ModulePath, path: &ModulePath,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> { ) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState), KclError> {
crate::log::log(format!("enter module {path} {}", exec_state.stack())); crate::log::log(format!("enter module {path} {}", exec_state.stack()));
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id)); let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
@ -108,13 +108,16 @@ impl ExecutorContext {
} else { } else {
exec_state.mut_stack().pop_env() exec_state.mut_stack().pop_env()
}; };
if !preserve_mem { let module_artifacts = if !preserve_mem {
std::mem::swap(&mut exec_state.mod_local, &mut local_state); std::mem::swap(&mut exec_state.mod_local, &mut local_state);
} local_state.artifacts
} else {
Default::default()
};
crate::log::log(format!("leave {path}")); crate::log::log(format!("leave {path}"));
result.map(|result| (result, env_ref, local_state.module_exports)) result.map(|result| (result, env_ref, local_state.module_exports, module_artifacts))
} }
/// Execute an AST's program. /// Execute an AST's program.
@ -450,12 +453,12 @@ impl ExecutorContext {
if matches!(body_type, BodyType::Root) { if matches!(body_type, BodyType::Root) {
// Flush the batch queue. // Flush the batch queue.
self.engine exec_state
.flush_batch( .flush_batch(
ModelingCmdMeta::new(self, SourceRange::new(program.end, program.end, program.module_id)),
// True here tells the engine to flush all the end commands as well like fillets // True here tells the engine to flush all the end commands as well like fillets
// and chamfers where the engine would otherwise eat the ID of the segments. // and chamfers where the engine would otherwise eat the ID of the segments.
true, true,
SourceRange::new(program.end, program.end, program.module_id),
) )
.await?; .await?;
} }
@ -535,12 +538,12 @@ impl ExecutorContext {
let result = match &mut repr { let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some((_, env_ref, items))) => Ok((*env_ref, items.clone())), ModuleRepr::Kcl(_, Some((_, env_ref, items, _))) => Ok((*env_ref, items.clone())),
ModuleRepr::Kcl(program, cache) => self ModuleRepr::Kcl(program, cache) => self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await .await
.map(|(val, er, items)| { .map(|(val, er, items, module_artifacts)| {
*cache = Some((val, er, items.clone())); *cache = Some((val, er, items.clone(), module_artifacts.clone()));
(er, items) (er, items)
}), }),
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new( ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
@ -566,28 +569,28 @@ impl ExecutorContext {
let result = match &mut repr { let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some((val, _, _))) => Ok(val.clone()), ModuleRepr::Kcl(_, Some((val, _, _, _))) => Ok(val.clone()),
ModuleRepr::Kcl(program, cached_items) => { ModuleRepr::Kcl(program, cached_items) => {
let result = self let result = self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await; .await;
match result { match result {
Ok((val, env, items)) => { Ok((val, env, items, module_artifacts)) => {
*cached_items = Some((val.clone(), env, items)); *cached_items = Some((val.clone(), env, items, module_artifacts));
Ok(val) Ok(val)
} }
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())), ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
ModuleRepr::Foreign(geom, cached) => { ModuleRepr::Foreign(geom, cached) => {
let result = super::import::send_to_engine(geom.clone(), self) let result = super::import::send_to_engine(geom.clone(), exec_state, self)
.await .await
.map(|geom| Some(KclValue::ImportedGeometry(geom))); .map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result { match result {
Ok(val) => { Ok(val) => {
*cached = val.clone(); *cached = Some((val.clone(), exec_state.mod_local.artifacts.clone()));
Ok(val) Ok(val)
} }
Err(e) => Err(e), Err(e) => Err(e),
@ -609,7 +612,7 @@ impl ExecutorContext {
exec_state: &mut ExecState, exec_state: &mut ExecState,
source_range: SourceRange, source_range: SourceRange,
preserve_mem: bool, preserve_mem: bool,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> { ) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState), KclError> {
exec_state.global.mod_loader.enter_module(path); exec_state.global.mod_loader.enter_module(path);
let result = self let result = self
.exec_module_body(program, exec_state, preserve_mem, module_id, path) .exec_module_body(program, exec_state, preserve_mem, module_id, path)
@ -798,6 +801,10 @@ fn apply_ascription(
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into()) let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::new_semantic(e.into()))?; .map_err(|e| KclError::new_semantic(e.into()))?;
if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
exec_state.clear_units_warnings(&source_range);
}
value.coerce(&ty, false, exec_state).map_err(|_| { value.coerce(&ty, false, exec_state).map_err(|_| {
let suggestion = if ty == RuntimeType::length() { let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `number(mm)`" ", you might try coercing to a fully specified numeric type such as `number(mm)`"
@ -806,9 +813,14 @@ fn apply_ascription(
} else { } else {
"" ""
}; };
let ty_str = if let Some(ty) = value.principal_type() {
format!("(with type `{ty}`) ")
} else {
String::new()
};
KclError::new_semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"could not coerce value of type {} to type {ty}{suggestion}", "could not coerce {} {ty_str}to type `{ty}`{suggestion}",
value.human_friendly_type() value.human_friendly_type()
), ),
vec![source_range], vec![source_range],
@ -1018,14 +1030,13 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned())))) .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(), .collect(),
}), }),
(being_indexed, _, _) => { (being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
let t = being_indexed.human_friendly_type(); format!(
let article = article_for(&t); "Only arrays can be indexed, but you're trying to index {}",
Err(KclError::new_semantic(KclErrorDetails::new( being_indexed.human_friendly_type()
format!("Only arrays can be indexed, but you're trying to index {article} {t}"), ),
vec![self.clone().into()], vec![self.clone().into()],
))) ))),
}
} }
} }
} }
@ -1153,7 +1164,7 @@ impl Node<BinaryExpression> {
KclValue::Number { value: l / r, meta, ty } KclValue::Number { value: l / r, meta, ty }
} }
BinaryOperator::Mod => { BinaryOperator::Mod => {
let (l, r, ty) = NumericType::combine_div(left, right); let (l, r, ty) = NumericType::combine_mod(left, right);
self.warn_on_unknown(&ty, "Modulo of", exec_state); self.warn_on_unknown(&ty, "Modulo of", exec_state);
KclValue::Number { value: l % r, meta, ty } KclValue::Number { value: l % r, meta, ty }
} }
@ -1200,11 +1211,14 @@ impl Node<BinaryExpression> {
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) { fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown { if ty == &NumericType::Unknown {
// TODO suggest how to fix this let sr = self.as_source_range();
exec_state.warn(CompilationError::err( exec_state.clear_units_warnings(&sr);
self.as_source_range(), let mut err = CompilationError::err(
format!("{} numbers which have unknown or incompatible units.", verb), sr,
)); format!("{} numbers which have unknown or incompatible units.\nYou can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.", verb),
);
err.tag = crate::errors::Tag::UnknownNumericUnits;
exec_state.warn(err);
} }
} }
} }
@ -1753,7 +1767,7 @@ a = 42: string
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string() err.to_string()
.contains("could not coerce value of type number(default units) to type string"), .contains("could not coerce a number (with type `number`) to type `string`"),
"Expected error but found {err:?}" "Expected error but found {err:?}"
); );
@ -1764,7 +1778,7 @@ a = 42: Plane
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string() err.to_string()
.contains("could not coerce value of type number(default units) to type Plane"), .contains("could not coerce a number (with type `number`) to type `Plane`"),
"Expected error but found {err:?}" "Expected error but found {err:?}"
); );
@ -1775,7 +1789,7 @@ arr = [0]: [string]
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string().contains( err.to_string().contains(
"could not coerce value of type array of number(default units) with 1 value to type [string]" "could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
), ),
"Expected error but found {err:?}" "Expected error but found {err:?}"
); );
@ -1786,8 +1800,9 @@ mixedArr = [0, "a"]: [number(mm)]
let result = parse_execute(program).await; let result = parse_execute(program).await;
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string() err.to_string().contains(
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"), "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
),
"Expected error but found {err:?}" "Expected error but found {err:?}"
); );
} }
@ -2092,4 +2107,19 @@ y = x: number(Length)"#;
assert_eq!(num.n, 2.0); assert_eq!(num.n, 2.0);
assert_eq!(num.ty, NumericType::mm()); assert_eq!(num.ty, NumericType::mm());
} }
#[tokio::test(flavor = "multi_thread")]
async fn one_warning_unknown() {
let ast = r#"
// Should warn once
a = PI * 2
// Should warn once
b = (PI * 2) / 3
// Should not warn
c = ((PI * 2) / 3): number(deg)
"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(result.exec_state.errors().len(), 2);
}
} }

View File

@ -532,6 +532,44 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
Ok(()) Ok(())
} }
fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
fn strip_backticks(s: &str) -> &str {
let mut result = s;
if s.starts_with('`') {
result = &result[1..]
}
if s.ends_with('`') {
result = &result[..result.len() - 1]
}
result
}
let expected_human = expected.human_friendly_type();
let expected_ty = expected.to_string();
let expected_str =
if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
format!("a value with type `{expected_ty}`")
} else {
format!("{expected_human} (`{expected_ty}`)")
};
let found_human = found.human_friendly_type();
let found_ty = found.principal_type_string();
let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
format!("a value with type {}", found_ty)
} else {
format!("{found_human} (with type {})", found_ty)
};
let mut result = format!("{expected_str}, but found {found_str}.");
if found.is_unknown_number() {
exec_state.clear_units_warnings(source_range);
result.push_str("\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.");
}
result
}
fn type_check_params_kw( fn type_check_params_kw(
fn_name: Option<&str>, fn_name: Option<&str>,
fn_def: &FunctionDefinition<'_>, fn_def: &FunctionDefinition<'_>,
@ -556,18 +594,19 @@ fn type_check_params_kw(
// For optional args, passing None should be the same as not passing an arg. // For optional args, passing None should be the same as not passing an arg.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) { if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
if let Some(ty) = ty { if let Some(ty) = ty {
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.value = arg arg.value = arg
.value .value
.coerce( .coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?, &rty,
true, true,
exec_state, exec_state,
) )
.map_err(|e| { .map_err(|e| {
let mut message = format!( let mut message = format!(
"{label} requires a value with type `{}`, but found {}", "{label} requires {}",
ty, type_err_str(ty, &arg.value, &arg.source_range, exec_state),
arg.value.human_friendly_type(),
); );
if let Some(ty) = e.explicit_coercion { if let Some(ty) = e.explicit_coercion {
// TODO if we have access to the AST for the argument we could choose which example to suggest. // TODO if we have access to the AST for the argument we could choose which example to suggest.
@ -630,28 +669,20 @@ fn type_check_params_kw(
if let Some(arg) = &mut args.unlabeled { if let Some(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg { if let Some((_, Some(ty))) = &fn_def.input_arg {
arg.1.value = arg let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.1 .map_err(|e| KclError::new_semantic(e.into()))?;
.value arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| {
.coerce( KclError::new_semantic(KclErrorDetails::new(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range) format!(
.map_err(|e| KclError::new_semantic(e.into()))?, "The input argument of {} requires {}",
true, fn_name
exec_state, .map(|n| format!("`{}`", n))
) .unwrap_or_else(|| "this function".to_owned()),
.map_err(|_| { type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state),
KclError::new_semantic(KclErrorDetails::new( ),
format!( vec![arg.1.source_range],
"The input argument of {} requires a value with type `{}`, but found {}", ))
fn_name })?;
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
ty,
arg.1.value.human_friendly_type()
),
vec![arg.1.source_range],
))
})?;
} }
} else if let Some((name, _)) = &fn_def.input_arg { } else if let Some((name, _)) = &fn_def.input_arg {
if let Some(arg) = args.labeled.get(name) { if let Some(arg) = args.labeled.get(name) {
@ -747,9 +778,8 @@ fn coerce_result_type(
let val = val.coerce(&ty, true, exec_state).map_err(|_| { let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"This function requires its result to be of type `{}`, but found {}", "This function requires its result to be {}",
ty.human_friendly_type(), type_err_str(ret_ty, &val, &(&val).into(), exec_state)
val.human_friendly_type(),
), ),
ret_ty.as_source_ranges(), ret_ty.as_source_ranges(),
)) ))
@ -928,7 +958,7 @@ msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err(); let err = parse_execute(program).await.unwrap_err();
assert_eq!( assert_eq!(
err.message(), err.message(),
"prefix requires a value with type `string`, but found number(default units)" "prefix requires a value with type `string`, but found a value with type `number`.\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`."
) )
} }
} }

View File

@ -15,7 +15,10 @@ use uuid::Uuid;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry}, execution::{
annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry,
ModelingCmdMeta,
},
fs::FileSystem, fs::FileSystem,
parsing::ast::types::{Annotation, Node}, parsing::ast::types::{Annotation, Node},
source_range::SourceRange, source_range::SourceRange,
@ -257,15 +260,22 @@ pub struct PreImportedGeometry {
pub source_range: SourceRange, pub source_range: SourceRange,
} }
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> { pub async fn send_to_engine(
pre: PreImportedGeometry,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
) -> Result<ImportedGeometry, KclError> {
let imported_geometry = ImportedGeometry::new( let imported_geometry = ImportedGeometry::new(
pre.id, pre.id,
pre.command.files.iter().map(|f| f.path.to_string()).collect(), pre.command.files.iter().map(|f| f.path.to_string()).collect(),
vec![pre.source_range.into()], vec![pre.source_range.into()],
); );
ctxt.engine exec_state
.async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone())) .async_modeling_cmd(
ModelingCmdMeta::with_id(ctxt, pre.source_range, pre.id),
&ModelingCmd::from(pre.command.clone()),
)
.await?; .await?;
Ok(imported_geometry) Ok(imported_geometry)

View File

@ -4,7 +4,6 @@ use anyhow::Result;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use super::types::UnitType;
use crate::{ use crate::{
errors::KclErrorDetails, errors::KclErrorDetails,
execution::{ execution::{
@ -281,69 +280,57 @@ impl KclValue {
/// Human readable type name used in error messages. Should not be relied /// Human readable type name used in error messages. Should not be relied
/// on for program logic. /// on for program logic.
pub(crate) fn human_friendly_type(&self) -> String { pub(crate) fn human_friendly_type(&self) -> String {
self.inner_human_friendly_type(1)
}
fn inner_human_friendly_type(&self, max_depth: usize) -> String {
if let Some(pt) = self.principal_type() {
if max_depth > 0 {
// The principal type of an array uses the array's element type,
// which is oftentimes `any`, and that's not a helpful message. So
// we show the actual elements.
if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
// If it's empty, we want to show the type of the array.
if !value.is_empty() {
// A max of 3 is good because it's common to use 3D points.
let max = 3;
let len = value.len();
let ellipsis = if len > max { ", ..." } else { "" };
let element_label = if len == 1 { "value" } else { "values" };
let element_tys = value
.iter()
.take(max)
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
.collect::<Vec<_>>()
.join(", ");
return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
}
}
}
return pt.to_string();
}
match self { match self {
KclValue::Uuid { .. } => "Unique ID (uuid)", KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(),
KclValue::TagDeclarator(_) => "TagDeclarator", KclValue::TagDeclarator(_) => "a tag declarator".to_owned(),
KclValue::TagIdentifier(_) => "TagIdentifier", KclValue::TagIdentifier(_) => "a tag identifier".to_owned(),
KclValue::Solid { .. } => "Solid", KclValue::Solid { .. } => "a solid".to_owned(),
KclValue::Sketch { .. } => "Sketch", KclValue::Sketch { .. } => "a sketch".to_owned(),
KclValue::Helix { .. } => "Helix", KclValue::Helix { .. } => "a helix".to_owned(),
KclValue::ImportedGeometry(_) => "ImportedGeometry", KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(),
KclValue::Function { .. } => "Function", KclValue::Function { .. } => "a function".to_owned(),
KclValue::Plane { .. } => "Plane", KclValue::Plane { .. } => "a plane".to_owned(),
KclValue::Face { .. } => "Face", KclValue::Face { .. } => "a face".to_owned(),
KclValue::Bool { .. } => "boolean (true/false value)", KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(),
KclValue::Number { KclValue::Number {
ty: NumericType::Unknown, ty: NumericType::Unknown,
.. ..
} => "number(unknown units)", } => "a number with unknown units".to_owned(),
KclValue::Number { KclValue::Number {
ty: NumericType::Known(UnitType::Length(_)), ty: NumericType::Known(units),
.. ..
} => "number(Length)", } => format!("a number ({units})"),
KclValue::Number { KclValue::Number { .. } => "a number".to_owned(),
ty: NumericType::Known(UnitType::Angle(_)), KclValue::String { .. } => "a string".to_owned(),
.. KclValue::Object { .. } => "an object".to_owned(),
} => "number(Angle)", KclValue::Module { .. } => "a module".to_owned(),
KclValue::Number { .. } => "number", KclValue::Type { .. } => "a type".to_owned(),
KclValue::String { .. } => "string (text)", KclValue::KclNone { .. } => "none".to_owned(),
KclValue::Tuple { .. } => "tuple (list)", KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
KclValue::HomArray { .. } => "array (list)", if value.is_empty() {
KclValue::Object { .. } => "object", "an empty array".to_owned()
KclValue::Module { .. } => "module", } else {
KclValue::Type { .. } => "type", // A max of 3 is good because it's common to use 3D points.
KclValue::KclNone { .. } => "None", const MAX: usize = 3;
let len = value.len();
let element_tys = value
.iter()
.take(MAX)
.map(|elem| elem.principal_type_string())
.collect::<Vec<_>>()
.join(", ");
let mut result = format!("an array of {element_tys}");
if len > MAX {
result.push_str(&format!(", ... with {len} values"));
}
if len == 1 {
result.push_str(" with 1 value");
}
result
}
}
} }
.to_owned()
} }
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self { pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
@ -602,6 +589,13 @@ impl KclValue {
}) })
} }
pub fn is_unknown_number(&self) -> bool {
match self {
KclValue::Number { ty, .. } => !ty.is_fully_specified(),
_ => false,
}
}
pub fn value_str(&self) -> Option<String> { pub fn value_str(&self) -> Option<String> {
match self { match self {
KclValue::Bool { value, .. } => Some(format!("{value}")), KclValue::Bool { value, .. } => Some(format!("{value}")),
@ -649,6 +643,8 @@ impl From<GeometryWithImportedGeometry> for KclValue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::exec::UnitType;
use super::*; use super::*;
#[test] #[test]
@ -658,21 +654,21 @@ mod tests {
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)), ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
meta: vec![], meta: vec![],
}; };
assert_eq!(len.human_friendly_type(), "number(Length)".to_string()); assert_eq!(len.human_friendly_type(), "a number (Length)".to_string());
let unknown = KclValue::Number { let unknown = KclValue::Number {
value: 1.0, value: 1.0,
ty: NumericType::Unknown, ty: NumericType::Unknown,
meta: vec![], meta: vec![],
}; };
assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string()); assert_eq!(unknown.human_friendly_type(), "a number with unknown units".to_string());
let mm = KclValue::Number { let mm = KclValue::Number {
value: 1.0, value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)), ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
meta: vec![], meta: vec![],
}; };
assert_eq!(mm.human_friendly_type(), "number(mm)".to_string()); assert_eq!(mm.human_friendly_type(), "a number (mm)".to_string());
let array1_mm = KclValue::HomArray { let array1_mm = KclValue::HomArray {
value: vec![mm.clone()], value: vec![mm.clone()],
@ -680,7 +676,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array1_mm.human_friendly_type(), array1_mm.human_friendly_type(),
"array of number(mm) with 1 value".to_string() "an array of `number(mm)` with 1 value".to_string()
); );
let array2_mm = KclValue::HomArray { let array2_mm = KclValue::HomArray {
@ -689,7 +685,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array2_mm.human_friendly_type(), array2_mm.human_friendly_type(),
"array of number(mm), number(mm) with 2 values".to_string() "an array of `number(mm)`, `number(mm)`".to_string()
); );
let array3_mm = KclValue::HomArray { let array3_mm = KclValue::HomArray {
@ -698,7 +694,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array3_mm.human_friendly_type(), array3_mm.human_friendly_type(),
"array of number(mm), number(mm), number(mm) with 3 values".to_string() "an array of `number(mm)`, `number(mm)`, `number(mm)`".to_string()
); );
let inches = KclValue::Number { let inches = KclValue::Number {
@ -712,14 +708,14 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array4.human_friendly_type(), array4.human_friendly_type(),
"array of number(mm), number(mm), number(in), ... with 4 values".to_string() "an array of `number(mm)`, `number(mm)`, `number(in)`, ... with 4 values".to_string()
); );
let empty_array = KclValue::HomArray { let empty_array = KclValue::HomArray {
value: vec![], value: vec![],
ty: RuntimeType::any(), ty: RuntimeType::any(),
}; };
assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string()); assert_eq!(empty_array.human_friendly_type(), "an empty array".to_string());
let array_nested = KclValue::HomArray { let array_nested = KclValue::HomArray {
value: vec![array2_mm.clone()], value: vec![array2_mm.clone()],
@ -727,7 +723,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array_nested.human_friendly_type(), array_nested.human_friendly_type(),
"array of [any; 2] with 1 value".to_string() "an array of `[any; 2]` with 1 value".to_string()
); );
} }
} }

View File

@ -22,8 +22,10 @@ use kcmc::{
}; };
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId}; use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
pub use memory::EnvironmentRef; pub use memory::EnvironmentRef;
pub(crate) use modeling::ModelingCmdMeta;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub(crate) use state::ModuleArtifactState;
pub use state::{ExecState, MetaSettings}; pub use state::{ExecState, MetaSettings};
use uuid::Uuid; use uuid::Uuid;
@ -56,6 +58,7 @@ mod import;
mod import_graph; mod import_graph;
pub(crate) mod kcl_value; pub(crate) mod kcl_value;
mod memory; mod memory;
mod modeling;
mod state; mod state;
pub mod typed_path; pub mod typed_path;
pub(crate) mod types; pub(crate) mod types;
@ -76,9 +79,6 @@ pub struct ExecOutcome {
/// the Feature Tree. /// the Feature Tree.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>, pub operations: Vec<Operation>,
/// Output commands to allow building the artifact graph by the caller.
#[cfg(feature = "artifact-graph")]
pub artifact_commands: Vec<ArtifactCommand>,
/// Output artifact graph. /// Output artifact graph.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph, pub artifact_graph: ArtifactGraph,
@ -575,7 +575,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone(); let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone(); let module_infos = exec_state.global.module_infos.clone();
let outcome = exec_state.to_mock_exec_outcome(result.0, self).await; let outcome = exec_state.into_mock_exec_outcome(result.0, self).await;
mem.squash_env(result.0); mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await; cache::write_old_memory((mem, module_infos)).await;
@ -773,15 +773,12 @@ impl ExecutorContext {
)) ))
.await; .await;
let outcome = exec_state.to_exec_outcome(result.0, self).await; let outcome = exec_state.into_exec_outcome(result.0, self).await;
Ok(outcome) Ok(outcome)
} }
/// Perform the execution of a program. /// Perform the execution of a program.
/// ///
/// You can optionally pass in some initialization memory for partial
/// execution.
///
/// To access non-fatal errors and warnings, extract them from the `ExecState`. /// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run( pub async fn run(
&self, &self,
@ -794,9 +791,6 @@ impl ExecutorContext {
/// Perform the execution of a program using a concurrent /// Perform the execution of a program using a concurrent
/// execution model. /// execution model.
/// ///
/// You can optionally pass in some initialization memory for partial
/// execution.
///
/// To access non-fatal errors and warnings, extract them from the `ExecState`. /// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run_concurrent( pub async fn run_concurrent(
&self, &self,
@ -842,6 +836,8 @@ impl ExecutorContext {
let module_id = *module_id; let module_id = *module_id;
let module_path = module_path.clone(); let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt); let source_range = SourceRange::from(import_stmt);
// Clone before mutating.
let module_exec_state = exec_state.clone();
self.add_import_module_ops( self.add_import_module_ops(
exec_state, exec_state,
@ -853,7 +849,6 @@ impl ExecutorContext {
); );
let repr = repr.clone(); let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone(); let exec_ctxt = self.clone();
let results_tx = results_tx.clone(); let results_tx = results_tx.clone();
@ -873,11 +868,13 @@ impl ExecutorContext {
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val))) result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
} }
ModuleRepr::Foreign(geom, _) => { ModuleRepr::Foreign(geom, _) => {
let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt) let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
.await .await
.map(|geom| Some(KclValue::ImportedGeometry(geom))); .map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| ModuleRepr::Foreign(geom.clone(), val)) result.map(|val| {
ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
})
} }
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new( ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {module_path} not found in universe"), format!("Module {module_path} not found in universe"),
@ -889,7 +886,7 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
let mut exec_state = exec_state; let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt; let exec_ctxt = exec_ctxt;
let result = exec_module( let result = exec_module(
@ -911,7 +908,7 @@ impl ExecutorContext {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
set.spawn(async move { set.spawn(async move {
let mut exec_state = exec_state; let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt; let exec_ctxt = exec_ctxt;
let result = exec_module( let result = exec_module(
@ -964,6 +961,15 @@ impl ExecutorContext {
} }
} }
// Since we haven't technically started executing the root module yet,
// the operations corresponding to the imports will be missing unless we
// track them here.
#[cfg(all(test, feature = "artifact-graph"))]
exec_state
.global
.root_module_artifacts
.extend(exec_state.mod_local.artifacts.clone());
self.inner_run(program, exec_state, preserve_mem).await self.inner_run(program, exec_state, preserve_mem).await
} }
@ -993,6 +999,18 @@ impl ExecutorContext {
Ok((universe, root_imports)) Ok((universe, root_imports))
} }
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
fn add_import_module_ops( fn add_import_module_ops(
&self, &self,
@ -1042,18 +1060,6 @@ impl ExecutorContext {
} }
} }
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
/// Perform the execution of a program. Accept all possible parameters and /// Perform the execution of a program. Accept all possible parameters and
/// output everything. /// output everything.
async fn inner_run( async fn inner_run(
@ -1121,26 +1127,32 @@ impl ExecutorContext {
&ModulePath::Main, &ModulePath::Main,
) )
.await; .await;
#[cfg(all(test, feature = "artifact-graph"))]
let exec_result = exec_result.map(|(_, env_ref, _, module_artifacts)| {
exec_state.global.root_module_artifacts.extend(module_artifacts);
env_ref
});
#[cfg(not(all(test, feature = "artifact-graph")))]
let exec_result = exec_result.map(|(_, env_ref, _, _)| env_ref);
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
{ {
// Fill in NodePath for operations. // Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items(); let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) { for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op { op.fill_node_paths(program, cached_body_items);
Operation::StdLibCall { }
node_path, #[cfg(test)]
source_range, {
.. for op in exec_state.global.root_module_artifacts.operations.iter_mut() {
op.fill_node_paths(program, cached_body_items);
}
for module in exec_state.global.module_infos.values_mut() {
if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
for op in &mut module_artifacts.operations {
op.fill_node_paths(program, cached_body_items);
}
} }
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
} }
} }
} }
@ -1153,7 +1165,7 @@ impl ExecutorContext {
self.engine.clear_queues().await; self.engine.clear_queues().await;
match exec_state.build_artifact_graph(&self.engine, program).await { match exec_state.build_artifact_graph(&self.engine, program).await {
Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref), Ok(_) => exec_result,
Err(err) => exec_result.and(Err(err)), Err(err) => exec_result.and(Err(err)),
} }
} }
@ -1919,13 +1931,13 @@ notNull = !myNull
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code1).await.unwrap_err().message(), parse_execute(code1).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)", "Cannot apply unary operator ! to non-boolean value: a number",
); );
let code2 = "notZero = !0"; let code2 = "notZero = !0";
assert_eq!( assert_eq!(
parse_execute(code2).await.unwrap_err().message(), parse_execute(code2).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)", "Cannot apply unary operator ! to non-boolean value: a number",
); );
let code3 = r#" let code3 = r#"
@ -1933,7 +1945,7 @@ notEmptyString = !""
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code3).await.unwrap_err().message(), parse_execute(code3).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: string", "Cannot apply unary operator ! to non-boolean value: a string",
); );
let code4 = r#" let code4 = r#"
@ -1942,7 +1954,7 @@ notMember = !obj.a
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code4).await.unwrap_err().message(), parse_execute(code4).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)", "Cannot apply unary operator ! to non-boolean value: a number",
); );
let code5 = " let code5 = "
@ -1950,7 +1962,7 @@ a = []
notArray = !a"; notArray = !a";
assert_eq!( assert_eq!(
parse_execute(code5).await.unwrap_err().message(), parse_execute(code5).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: [any; 0]", "Cannot apply unary operator ! to non-boolean value: an empty array",
); );
let code6 = " let code6 = "
@ -1958,7 +1970,7 @@ x = {}
notObject = !x"; notObject = !x";
assert_eq!( assert_eq!(
parse_execute(code6).await.unwrap_err().message(), parse_execute(code6).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: { }", "Cannot apply unary operator ! to non-boolean value: an object",
); );
let code7 = " let code7 = "
@ -1984,7 +1996,7 @@ notTagDeclarator = !myTagDeclarator";
assert!( assert!(
tag_declarator_err tag_declarator_err
.message() .message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"), .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
"Actual error: {:?}", "Actual error: {:?}",
tag_declarator_err tag_declarator_err
); );
@ -1998,7 +2010,7 @@ notTagIdentifier = !myTag";
assert!( assert!(
tag_identifier_err tag_identifier_err
.message() .message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"), .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
"Actual error: {:?}", "Actual error: {:?}",
tag_identifier_err tag_identifier_err
); );

View File

@ -0,0 +1,224 @@
use kcmc::ModelingCmd;
use kittycad_modeling_cmds::{
self as kcmc,
websocket::{ModelingCmdReq, OkWebSocketResponseData},
};
use uuid::Uuid;
#[cfg(feature = "artifact-graph")]
use crate::exec::ArtifactCommand;
use crate::{
exec::{IdGenerator, KclValue},
execution::Solid,
std::Args,
ExecState, ExecutorContext, KclError, SourceRange,
};
/// Context and metadata needed to send a single modeling command.
///
/// Many functions consume Self so that the command ID isn't accidentally reused
/// among multiple modeling commands.
pub(crate) struct ModelingCmdMeta<'a> {
/// The executor context, which contains the engine.
pub ctx: &'a ExecutorContext,
/// The source range of the command, used for error reporting.
pub source_range: SourceRange,
/// The id of the command, if it has been set by the caller or generated.
id: Option<Uuid>,
}
impl<'a> ModelingCmdMeta<'a> {
pub fn new(ctx: &'a ExecutorContext, source_range: SourceRange) -> Self {
ModelingCmdMeta {
ctx,
source_range,
id: None,
}
}
pub fn with_id(ctx: &'a ExecutorContext, source_range: SourceRange, id: Uuid) -> Self {
ModelingCmdMeta {
ctx,
source_range,
id: Some(id),
}
}
pub fn from_args_id(args: &'a Args, id: Uuid) -> Self {
ModelingCmdMeta {
ctx: &args.ctx,
source_range: args.source_range,
id: Some(id),
}
}
pub fn id(&mut self, id_generator: &mut IdGenerator) -> Uuid {
if let Some(id) = self.id {
return id;
}
let id = id_generator.next_uuid();
self.id = Some(id);
id
}
}
impl<'a> From<&'a Args> for ModelingCmdMeta<'a> {
fn from(args: &'a Args) -> Self {
ModelingCmdMeta::new(&args.ctx, args.source_range)
}
}
impl ExecState {
/// Add a modeling command to the batch but don't fire it right away.
pub(crate) async fn batch_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.batch_modeling_cmd(id, meta.source_range, &cmd).await
}
/// Add multiple modeling commands to the batch but don't fire them right
/// away.
pub(crate) async fn batch_modeling_cmds(
&mut self,
meta: ModelingCmdMeta<'_>,
cmds: &[ModelingCmdReq],
) -> Result<(), crate::errors::KclError> {
#[cfg(feature = "artifact-graph")]
for cmd_req in cmds {
self.push_command(ArtifactCommand {
cmd_id: *cmd_req.cmd_id.as_ref(),
range: meta.source_range,
command: cmd_req.cmd.clone(),
});
}
meta.ctx.engine.batch_modeling_cmds(meta.source_range, cmds).await
}
/// Add a modeling command to the batch that gets executed at the end of the
/// file. This is good for something like fillet or chamfer where the engine
/// would eat the path id if we executed it right away.
pub(crate) async fn batch_end_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
// TODO: The order of the tracking of these doesn't match the order that
// they're sent to the engine.
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.batch_end_cmd(id, meta.source_range, &cmd).await
}
/// Send the modeling cmd and wait for the response.
pub(crate) async fn send_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.send_modeling_cmd(id, meta.source_range, &cmd).await
}
/// Send the modeling cmd async and don't wait for the response.
/// Add it to our list of async commands.
pub(crate) async fn async_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.async_modeling_cmd(id, meta.source_range, cmd).await
}
/// Force flush the batch queue.
pub(crate) async fn flush_batch(
&mut self,
meta: ModelingCmdMeta<'_>,
// Whether or not to flush the end commands as well.
// We only do this at the very end of the file.
batch_end: bool,
) -> Result<OkWebSocketResponseData, KclError> {
meta.ctx.engine.flush_batch(batch_end, meta.source_range).await
}
/// Flush just the fillets and chamfers for this specific SolidSet.
pub(crate) async fn flush_batch_for_solids(
&mut self,
meta: ModelingCmdMeta<'_>,
solids: &[Solid],
) -> Result<(), KclError> {
// Make sure we don't traverse sketches more than once.
let mut traversed_sketches = Vec::new();
// Collect all the fillet/chamfer ids for the solids.
let mut ids = Vec::new();
for solid in solids {
// We need to traverse the solids that share the same sketch.
let sketch_id = solid.sketch.id;
if !traversed_sketches.contains(&sketch_id) {
// Find all the solids on the same shared sketch.
ids.extend(
self.stack()
.walk_call_stack()
.filter(|v| matches!(v, KclValue::Solid { value } if value.sketch.id == sketch_id))
.flat_map(|v| match v {
KclValue::Solid { value } => value.get_all_edge_cut_ids(),
_ => unreachable!(),
}),
);
traversed_sketches.push(sketch_id);
}
ids.extend(solid.get_all_edge_cut_ids());
}
// We can return early if there are no fillets or chamfers.
if ids.is_empty() {
return Ok(());
}
// We want to move these fillets and chamfers from batch_end to batch so they get executed
// before what ever we call next.
for id in ids {
// Pop it off the batch_end and add it to the batch.
let Some(item) = meta.ctx.engine.batch_end().write().await.shift_remove(&id) else {
// It might be in the batch already.
continue;
};
// Add it to the batch.
meta.ctx.engine.batch().write().await.push(item);
}
// Run flush.
// Yes, we do need to actually flush the batch here, or references will fail later.
self.flush_batch(meta, false).await?;
Ok(())
}
}

View File

@ -3,7 +3,9 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
use kittycad_modeling_cmds::websocket::WebSocketResponse; use kcmc::websocket::WebSocketResponse;
#[cfg(feature = "artifact-graph")]
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -50,6 +52,8 @@ pub(super) struct GlobalState {
pub errors: Vec<CompilationError>, pub errors: Vec<CompilationError>,
#[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))] #[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))]
pub artifacts: ArtifactState, pub artifacts: ArtifactState,
#[cfg_attr(not(all(test, feature = "artifact-graph")), expect(dead_code))]
pub root_module_artifacts: ModuleArtifactState,
} }
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
@ -77,6 +81,20 @@ pub(super) struct ArtifactState {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {} pub(super) struct ArtifactState {}
/// Artifact state for a single module.
#[cfg(all(test, feature = "artifact-graph"))]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ModuleArtifactState {
/// Outgoing engine commands.
pub commands: Vec<ArtifactCommand>,
/// Operations that have been performed in execution order.
pub operations: Vec<Operation>,
}
#[cfg(not(all(test, feature = "artifact-graph")))]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ModuleArtifactState {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct ModuleState { pub(super) struct ModuleState {
/// The id generator for this module. /// The id generator for this module.
@ -96,6 +114,7 @@ pub(super) struct ModuleState {
pub settings: MetaSettings, pub settings: MetaSettings,
pub(super) explicit_length_units: bool, pub(super) explicit_length_units: bool,
pub(super) path: ModulePath, pub(super) path: ModulePath,
pub artifacts: ModuleArtifactState,
} }
impl ExecState { impl ExecState {
@ -126,6 +145,17 @@ impl ExecState {
self.global.errors.push(e); self.global.errors.push(e);
} }
pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
self.global.errors = std::mem::take(&mut self.global.errors)
.into_iter()
.filter(|e| {
e.severity != Severity::Warning
|| !source_range.contains_range(&e.source_range)
|| e.tag != crate::errors::Tag::UnknownNumericUnits
})
.collect();
}
pub fn errors(&self) -> &[CompilationError] { pub fn errors(&self) -> &[CompilationError] {
&self.global.errors &self.global.errors
} }
@ -133,7 +163,7 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to /// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as /// reduce the amount of data that crosses the WASM boundary as much as
/// possible. /// possible.
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome { pub async fn into_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal // Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState. // state when we add more to ExecState.
ExecOutcome { ExecOutcome {
@ -142,22 +172,18 @@ impl ExecState {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: self.global.artifacts.operations, operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifacts.graph, artifact_graph: self.global.artifacts.graph,
errors: self.global.errors, errors: self.global.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(), default_planes: ctx.engine.get_default_planes().read().await.clone(),
} }
} }
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome { pub async fn into_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
ExecOutcome { ExecOutcome {
variables: self.mod_local.variables(main_ref), variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: Default::default(), operations: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(), artifact_graph: Default::default(),
errors: self.global.errors, errors: self.global.errors,
filenames: Default::default(), filenames: Default::default(),
@ -188,12 +214,22 @@ impl ExecState {
} }
pub(crate) fn push_op(&mut self, op: Operation) { pub(crate) fn push_op(&mut self, op: Operation) {
#[cfg(all(test, feature = "artifact-graph"))]
self.mod_local.artifacts.operations.push(op.clone());
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.push(op); self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))] #[cfg(not(feature = "artifact-graph"))]
drop(op); drop(op);
} }
#[cfg(feature = "artifact-graph")]
pub(crate) fn push_command(&mut self, command: ArtifactCommand) {
#[cfg(all(test, feature = "artifact-graph"))]
self.mod_local.artifacts.commands.push(command);
#[cfg(not(all(test, feature = "artifact-graph")))]
drop(command);
}
pub(super) fn next_module_id(&self) -> ModuleId { pub(super) fn next_module_id(&self) -> ModuleId {
ModuleId::from_usize(self.global.path_to_source_id.len()) ModuleId::from_usize(self.global.path_to_source_id.len())
} }
@ -241,6 +277,21 @@ impl ExecState {
self.global.module_infos.get(&id) self.global.module_infos.get(&id)
} }
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn modules(&self) -> &ModuleInfoMap {
&self.global.module_infos
}
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn operations(&self) -> &[Operation] {
&self.global.artifacts.operations
}
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn root_module_artifact_state(&self) -> &ModuleArtifactState {
&self.global.root_module_artifacts
}
pub fn current_default_units(&self) -> NumericType { pub fn current_default_units(&self) -> NumericType {
NumericType::Default { NumericType::Default {
len: self.length_unit(), len: self.length_unit(),
@ -349,6 +400,7 @@ impl GlobalState {
path_to_source_id: Default::default(), path_to_source_id: Default::default(),
module_infos: Default::default(), module_infos: Default::default(),
artifacts: Default::default(), artifacts: Default::default(),
root_module_artifacts: Default::default(),
mod_loader: Default::default(), mod_loader: Default::default(),
errors: Default::default(), errors: Default::default(),
id_to_source: Default::default(), id_to_source: Default::default(),
@ -388,6 +440,15 @@ impl ArtifactState {
} }
} }
impl ModuleArtifactState {
/// When self is a cached state, extend it with new state.
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn extend(&mut self, other: ModuleArtifactState) {
self.commands.extend(other.commands);
self.operations.extend(other.operations);
}
}
impl ModuleState { impl ModuleState {
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self { pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
ModuleState { ModuleState {
@ -403,6 +464,7 @@ impl ModuleState {
default_angle_units: Default::default(), default_angle_units: Default::default(),
kcl_version: "0.1".to_owned(), kcl_version: "0.1".to_owned(),
}, },
artifacts: Default::default(),
} }
} }

View File

@ -104,6 +104,16 @@ impl TypedPath {
} }
} }
#[cfg(not(target_arch = "wasm32"))]
pub fn strip_prefix(&self, base: impl AsRef<std::path::Path>) -> Result<Self, std::path::StripPrefixError> {
self.0.strip_prefix(base).map(|p| TypedPath(p.to_path_buf()))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn canonicalize(&self) -> Result<Self, std::io::Error> {
self.0.canonicalize().map(|p| TypedPath(p.to_path_buf()))
}
pub fn to_string_lossy(&self) -> String { pub fn to_string_lossy(&self) -> String {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {

View File

@ -438,7 +438,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Any => write!(f, "any"), PrimitiveType::Any => write!(f, "any"),
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"), PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"), PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"),
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number(default units)"), PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number"),
PrimitiveType::Number(NumericType::Any) => write!(f, "number(any units)"), PrimitiveType::Number(NumericType::Any) => write!(f, "number(any units)"),
PrimitiveType::String => write!(f, "string"), PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"), PrimitiveType::Boolean => write!(f, "bool"),
@ -453,8 +453,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis2d => write!(f, "Axis2d"), PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"), PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"), PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"), PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "function"), PrimitiveType::Function => write!(f, "fn"),
} }
} }
} }
@ -499,20 +499,6 @@ impl NumericType {
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)) NumericType::Known(UnitType::Angle(UnitAngle::Degrees))
} }
pub fn expect_default_length(&self) -> Self {
match self {
NumericType::Default { len, .. } => NumericType::Known(UnitType::Length(*len)),
_ => unreachable!(),
}
}
pub fn expect_default_angle(&self) -> Self {
match self {
NumericType::Default { angle, .. } => NumericType::Known(UnitType::Angle(*angle)),
_ => unreachable!(),
}
}
/// Combine two types when we expect them to be equal, erring on the side of less coercion. To be /// Combine two types when we expect them to be equal, erring on the side of less coercion. To be
/// precise, only adjusting one number or the other when they are of known types. /// precise, only adjusting one number or the other when they are of known types.
/// ///
@ -554,15 +540,10 @@ impl NumericType {
(at, Any) => (a.n, b.n, at), (at, Any) => (a.n, b.n, at),
(Any, bt) => (a.n, b.n, bt), (Any, bt) => (a.n, b.n, bt),
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
// Known types and compatible, but needs adjustment. // Known types and compatible, but needs adjustment.
(t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1).0, t), (t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1).0, t),
(t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1).0, t), (t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1).0, t),
// Known but incompatible.
(Known(_), Known(_)) => (a.n, b.n, Unknown),
// Known and unknown => we assume the known one, possibly with adjustment // Known and unknown => we assume the known one, possibly with adjustment
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => { (Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
(a.n, b.n, Known(UnitType::Count)) (a.n, b.n, Known(UnitType::Count))
@ -570,9 +551,12 @@ impl NumericType {
(t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) => (a.n, l2.adjust_to(b.n, l1).0, t), (t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) => (a.n, l2.adjust_to(b.n, l1).0, t),
(Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) => (l1.adjust_to(a.n, l2).0, b.n, t), (Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) => (l1.adjust_to(a.n, l2).0, b.n, t),
(t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => (a.n, a2.adjust_to(b.n, a1).0, t), (t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => (a.n, a2.adjust_to(b.n, a1).0, t),
(Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) => (a1.adjust_to(a.n, a2).0, b.n, t), (Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) => (a1.adjust_to(a.n, a2).0, b.n, t),
(Known(_), Known(_)) | (Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => {
(a.n, b.n, Unknown)
}
} }
} }
@ -647,6 +631,20 @@ impl NumericType {
} }
} }
/// Combine two types for modulo-like operations.
pub fn combine_mod(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
use NumericType::*;
match (a.ty, b.ty) {
(at @ Default { .. }, bt @ Default { .. }) if at == bt => (a.n, b.n, at),
(at, bt) if at == bt => (a.n, b.n, at),
(Default { .. }, Default { .. }) => (a.n, b.n, Unknown),
(at, Known(UnitType::Count) | Any) => (a.n, b.n, at),
(at @ Known(_), Default { .. }) => (a.n, b.n, at),
(Known(UnitType::Count), _) => (a.n, b.n, Known(UnitType::Count)),
_ => (a.n, b.n, Unknown),
}
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self { pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix { match suffix {
NumericSuffix::None => NumericType::Default { NumericSuffix::None => NumericType::Default {
@ -851,7 +849,7 @@ impl std::fmt::Display for UnitType {
} }
} }
// TODO called UnitLen so as not to clash with UnitLength in settings) // TODO called UnitLen so as not to clash with UnitLength in settings.
/// A unit of length. /// A unit of length.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
@ -1510,6 +1508,23 @@ impl KclValue {
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None, KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None,
} }
} }
pub fn principal_type_string(&self) -> String {
if let Some(ty) = self.principal_type() {
return format!("`{ty}`");
}
match self {
KclValue::Module { .. } => "module",
KclValue::KclNone { .. } => "none",
KclValue::Type { .. } => "type",
_ => {
debug_assert!(false);
"<unexpected type>"
}
}
.to_owned()
}
} }
#[cfg(test)] #[cfg(test)]
@ -2342,10 +2357,10 @@ d = cos(30)
let result = parse_execute(program).await.unwrap(); let result = parse_execute(program).await.unwrap();
assert!(result.exec_state.errors().is_empty()); assert!(result.exec_state.errors().is_empty());
assert_value_and_type("a", &result, 1.0, NumericType::count()); assert_value_and_type("a", &result, 1.0, NumericType::default());
assert_value_and_type("b", &result, 3.0, NumericType::default()); assert_value_and_type("b", &result, 3.0, NumericType::default());
assert_value_and_type("c", &result, 1.0, NumericType::count()); assert_value_and_type("c", &result, 1.0, NumericType::default());
assert_value_and_type("d", &result, 1.0, NumericType::count()); assert_value_and_type("d", &result, 1.0, NumericType::default());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -47,7 +47,7 @@ impl Tag {
match self { match self {
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]), Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]), Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
Tag::None => None, Tag::UnknownNumericUnits | Tag::None => None,
} }
} }
} }

View File

@ -950,7 +950,7 @@ startSketchOn(XY)
match hover.unwrap().contents { match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("foo: number(default units) = 42")); assert!(value.contains("foo: number = 42"));
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -3900,7 +3900,7 @@ startSketchOn(XY)
match hover.unwrap().contents { match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("foo: number(default units) = 42")); assert!(value.contains("foo: number = 42"));
} }
_ => unreachable!(), _ => unreachable!(),
} }

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
exec::KclValue, exec::KclValue,
execution::{typed_path::TypedPath, EnvironmentRef, PreImportedGeometry}, execution::{typed_path::TypedPath, EnvironmentRef, ModuleArtifactState, PreImportedGeometry},
fs::{FileManager, FileSystem}, fs::{FileManager, FileSystem},
parsing::ast::types::{ImportPath, Node, Program}, parsing::ast::types::{ImportPath, Node, Program},
source_range::SourceRange, source_range::SourceRange,
@ -131,8 +131,11 @@ impl ModuleInfo {
pub enum ModuleRepr { pub enum ModuleRepr {
Root, Root,
// AST, memory, exported names // AST, memory, exported names
Kcl(Node<Program>, Option<(Option<KclValue>, EnvironmentRef, Vec<String>)>), Kcl(
Foreign(PreImportedGeometry, Option<KclValue>), Node<Program>,
Option<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState)>,
),
Foreign(PreImportedGeometry, Option<(Option<KclValue>, ModuleArtifactState)>),
Dummy, Dummy,
} }

View File

@ -3174,6 +3174,19 @@ impl PrimitiveType {
_ => None, _ => None,
} }
} }
fn display_multiple(&self) -> String {
match self {
PrimitiveType::Any => "values".to_owned(),
PrimitiveType::Number(_) => "numbers".to_owned(),
PrimitiveType::String => "strings".to_owned(),
PrimitiveType::Boolean => "bools".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Function(_) => "functions".to_owned(),
PrimitiveType::Named { id } => format!("`{}`s", id.name),
PrimitiveType::Tag => "tags".to_owned(),
}
}
} }
impl fmt::Display for PrimitiveType { impl fmt::Display for PrimitiveType {
@ -3264,6 +3277,53 @@ pub enum Type {
}, },
} }
impl Type {
pub fn human_friendly_type(&self) -> String {
match self {
Type::Primitive(ty) => format!("a value with type `{ty}`"),
Type::Array {
ty,
len: ArrayLen::None | ArrayLen::Minimum(0),
} => {
format!("an array of {}", ty.display_multiple())
}
Type::Array {
ty,
len: ArrayLen::Minimum(1),
} => format!("one or more {}", ty.display_multiple()),
Type::Array {
ty,
len: ArrayLen::Minimum(n),
} => {
format!("an array of {n} or more {}", ty.display_multiple())
}
Type::Array {
ty,
len: ArrayLen::Known(n),
} => format!("an array of {n} {}", ty.display_multiple()),
Type::Union { tys } => tys
.iter()
.map(|t| t.human_friendly_type())
.collect::<Vec<_>>()
.join(" or "),
Type::Object { .. } => format!("an object with fields `{}`", self),
}
}
fn display_multiple(&self) -> String {
match self {
Type::Primitive(ty) => ty.display_multiple(),
Type::Array { .. } => "arrays".to_owned(),
Type::Union { tys } => tys
.iter()
.map(|t| t.display_multiple())
.collect::<Vec<_>>()
.join(" or "),
Type::Object { .. } => format!("objects with fields `{self}`"),
}
}
}
impl fmt::Display for Type { impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {

View File

@ -3,13 +3,18 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use indexmap::IndexMap;
use insta::rounded_redaction; use insta::rounded_redaction;
use crate::{errors::KclError, ModuleId}; use crate::{
errors::KclError,
execution::{EnvironmentRef, ModuleArtifactState},
ExecOutcome, ExecState, ExecutorContext, ModuleId,
};
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
use crate::{ use crate::{
exec::ArtifactCommand,
execution::{ArtifactGraph, Operation}, execution::{ArtifactGraph, Operation},
modules::{ModulePath, ModuleRepr},
}; };
mod kcl_samples; mod kcl_samples;
@ -19,8 +24,7 @@ mod kcl_samples;
struct Test { struct Test {
/// The name of the test. /// The name of the test.
name: String, name: String,
/// The name of the KCL file that's the entry point, e.g. "main.kcl", in the /// The KCL file that's the entry point, e.g. "main.kcl", in the `input_dir`.
/// `input_dir`.
entry_point: PathBuf, entry_point: PathBuf,
/// Input KCL files are in this directory. /// Input KCL files are in this directory.
input_dir: PathBuf, input_dir: PathBuf,
@ -34,6 +38,9 @@ struct Test {
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png"; pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
#[cfg(feature = "artifact-graph")]
const REPO_ROOT: &str = "../..";
impl Test { impl Test {
fn new(name: &str) -> Self { fn new(name: &str) -> Self {
Self { Self {
@ -52,6 +59,75 @@ impl Test {
} }
} }
impl ExecState {
/// Same as [`Self::into_exec_outcome`], but also returns the module state.
async fn into_test_exec_outcome(
self,
main_ref: EnvironmentRef,
ctx: &ExecutorContext,
project_directory: &Path,
) -> (ExecOutcome, IndexMap<String, ModuleArtifactState>) {
let module_state = self.to_module_state(project_directory);
let outcome = self.into_exec_outcome(main_ref, ctx).await;
(outcome, module_state)
}
#[cfg(not(feature = "artifact-graph"))]
fn to_module_state(&self, _project_directory: &Path) -> IndexMap<String, ModuleArtifactState> {
Default::default()
}
/// The keys of the map are the module paths. Can't use `ModulePath` since
/// it needs to be converted to a string to be a JSON object key. The paths
/// need to be relative so that generating locally works in CI.
#[cfg(feature = "artifact-graph")]
fn to_module_state(&self, _project_directory: &Path) -> IndexMap<String, ModuleArtifactState> {
let project_directory = std::path::Path::new(REPO_ROOT)
.canonicalize()
.unwrap_or_else(|_| panic!("Failed to canonicalize project directory: {REPO_ROOT}"));
let mut module_state = IndexMap::new();
for info in self.modules().values() {
let relative_path = relative_module_path(&info.path, &project_directory).unwrap_or_else(|err| {
panic!(
"Failed to get relative module path for {:?} in {:?}; caused by {err:?}",
&info.path, project_directory
)
});
match &info.repr {
ModuleRepr::Root => {
module_state.insert(relative_path, self.root_module_artifact_state().clone());
}
ModuleRepr::Kcl(_, None) => {
module_state.insert(relative_path, Default::default());
}
ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) => {
module_state.insert(relative_path, module_artifacts.clone());
}
ModuleRepr::Foreign(_, Some((_, module_artifacts))) => {
module_state.insert(relative_path, module_artifacts.clone());
}
ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
}
}
module_state
}
}
#[cfg(feature = "artifact-graph")]
fn relative_module_path(module_path: &ModulePath, abs_project_directory: &Path) -> Result<String, std::io::Error> {
match module_path {
ModulePath::Main => Ok("main".to_owned()),
ModulePath::Local { value: path } => {
let abs_path = path.canonicalize()?;
abs_path
.strip_prefix(abs_project_directory)
.map(|p| p.to_string_lossy())
.map_err(|_| std::io::Error::other(format!("Failed to strip prefix from module path {abs_path:?}")))
}
ModulePath::Std { value } => Ok(format!("std::{value}")),
}
}
fn assert_snapshot<F, R>(test: &Test, operation: &str, f: F) fn assert_snapshot<F, R>(test: &Test, operation: &str, f: F)
where where
F: FnOnce() -> R, F: FnOnce() -> R,
@ -181,7 +257,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
panic!("Step data was empty"); panic!("Step data was empty");
} }
} }
let outcome = exec_state.to_exec_outcome(env_ref, &ctx).await; let (outcome, module_state) = exec_state.into_test_exec_outcome(env_ref, &ctx, &test.input_dir).await;
let mem_result = catch_unwind(AssertUnwindSafe(|| { let mem_result = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Variables in memory after executing", || { assert_snapshot(test, "Variables in memory after executing", || {
@ -202,13 +278,10 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
}) })
})); }));
#[cfg(not(feature = "artifact-graph"))]
drop(module_state);
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
assert_common_snapshots( assert_artifact_snapshots(test, module_state, outcome.operations, outcome.artifact_graph);
test,
outcome.operations,
outcome.artifact_commands,
outcome.artifact_graph,
);
mem_result.unwrap(); mem_result.unwrap();
} }
Err(e) => { Err(e) => {
@ -238,7 +311,23 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
})); }));
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
assert_common_snapshots(test, error.operations, error.artifact_commands, error.artifact_graph); {
let global_operations = if !error.operations.is_empty() {
error.operations
} else if let Some(exec_state) = &e.exec_state {
// Non-fatal compilation errors don't have artifact
// output attached, so we need to get it from
// ExecState.
exec_state.operations().to_vec()
} else {
Vec::new()
};
let module_state = e
.exec_state
.map(|e| e.to_module_state(&test.input_dir))
.unwrap_or_default();
assert_artifact_snapshots(test, module_state, global_operations, error.artifact_graph);
}
err_result.unwrap(); err_result.unwrap();
} }
e => { e => {
@ -252,56 +341,44 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
} }
} }
/// Assert snapshots that should happen both when KCL execution succeeds and /// Assert snapshots for artifacts that should happen both when KCL execution
/// when it results in an error. /// succeeds and when it results in an error.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
fn assert_common_snapshots( fn assert_artifact_snapshots(
test: &Test, test: &Test,
operations: Vec<Operation>, module_state: IndexMap<String, ModuleArtifactState>,
artifact_commands: Vec<ArtifactCommand>, global_operations: Vec<Operation>,
artifact_graph: ArtifactGraph, artifact_graph: ArtifactGraph,
) { ) {
let operations = { let module_operations = module_state
// Make the operations deterministic by sorting them by their module ID, .iter()
// then by their range. .map(|(path, s)| (path, &s.operations))
let mut operations = operations.clone(); .collect::<IndexMap<_, _>>();
operations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
operations
};
let artifact_commands = {
// Due to our newfound concurrency, we're going to mess with the
// artifact_commands a bit -- we're going to maintain the order,
// but only for a given module ID. This means the artifact_commands
// is no longer meaningful, but it is deterministic and will hopefully
// catch meaningful changes in behavior.
// We sort by the source range, like we do for the operations.
let mut artifact_commands = artifact_commands.clone();
artifact_commands.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
artifact_commands
};
let result1 = catch_unwind(AssertUnwindSafe(|| { let result1 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Operations executed", || { assert_snapshot(test, "Operations executed", || {
insta::assert_json_snapshot!("ops", operations, { insta::assert_json_snapshot!("ops", module_operations, {
"[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3), ".*[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
"[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3), ".*[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
"[].**.value.value" => rounded_redaction(3), ".*[].**.value.value" => rounded_redaction(3),
"[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3), ".*[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
"[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3), ".*[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
".**.sourceRange" => Vec::new(), ".**.sourceRange" => Vec::new(),
".**.functionSourceRange" => Vec::new(), ".**.functionSourceRange" => Vec::new(),
".**.moduleId" => 0, ".**.moduleId" => 0,
}); });
}) })
})); }));
let module_commands = module_state
.iter()
.map(|(path, s)| (path, &s.commands))
.collect::<IndexMap<_, _>>();
let result2 = catch_unwind(AssertUnwindSafe(|| { let result2 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Artifact commands", || { assert_snapshot(test, "Artifact commands", || {
insta::assert_json_snapshot!("artifact_commands", artifact_commands, { insta::assert_json_snapshot!("artifact_commands", module_commands, {
"[].command.**.value" => rounded_redaction(3), ".*[].command.**.value" => rounded_redaction(3),
"[].command.**.x" => rounded_redaction(3), ".*[].command.**.x" => rounded_redaction(3),
"[].command.**.y" => rounded_redaction(3), ".*[].command.**.y" => rounded_redaction(3),
"[].command.**.z" => rounded_redaction(3), ".*[].command.**.z" => rounded_redaction(3),
".**.range" => Vec::new(), ".**.range" => Vec::new(),
}); });
}) })
@ -337,6 +414,25 @@ fn assert_common_snapshots(
result1.unwrap(); result1.unwrap();
result2.unwrap(); result2.unwrap();
result3.unwrap(); result3.unwrap();
// The global operations should be a superset of the main module. But it
// won't always be a superset of the operations of all modules.
let repo_root = std::path::Path::new(REPO_ROOT).canonicalize().unwrap();
let root_string: String = test
.entry_point
.canonicalize()
.unwrap_or_else(|_| panic!("Should be able to canonicalize the entry point {:?}", &test.entry_point))
.strip_prefix(&repo_root)
.expect("Repo root dir should be a prefix of the entry point")
.to_string_lossy()
.into_owned();
let main_operations = module_operations
.get(&root_string)
.expect("Main module state not found");
assert!(
global_operations.len() >= main_operations.len(),
"global_operations={global_operations:#?}, main_operations={main_operations:#?}"
);
} }
mod cube { mod cube {
@ -402,8 +498,8 @@ mod any_type {
super::execute(TEST_NAME, false).await super::execute(TEST_NAME, false).await
} }
} }
mod error_with_point_shows_numeric_units { mod coerce_from_trig_to_point {
const TEST_NAME: &str = "error_with_point_shows_numeric_units"; const TEST_NAME: &str = "coerce_from_trig_to_point";
/// Test parsing KCL. /// Test parsing KCL.
#[test] #[test]
@ -3546,3 +3642,27 @@ mod var_ref_in_own_def_decl {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod user_reported_union_2_bug {
// TODO IF THIS TEST START PASSING, CLOSE THE FOLLOWING ISSUE
// https://github.com/KittyCAD/modeling-app/issues/7310
// and https://github.com/KittyCAD/engine/issues/3539
const TEST_NAME: &str = "user_reported_union_2_bug";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}

View File

@ -106,17 +106,18 @@ async fn inner_appearance(
a: 100.0, a: 100.0,
}; };
args.batch_modeling_cmd( exec_state
exec_state.next_uuid(), .batch_modeling_cmd(
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr { (&args).into(),
object_id: solid_id, ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
color, object_id: solid_id,
metalness: metalness.unwrap_or_default() as f32 / 100.0, color,
roughness: roughness.unwrap_or_default() as f32 / 100.0, metalness: metalness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0, roughness: roughness.unwrap_or_default() as f32 / 100.0,
}), ambient_occlusion: 0.0,
) }),
.await?; )
.await?;
// Idk if we want to actually modify the memory for the colors, but I'm not right now since // Idk if we want to actually modify the memory for the colors, but I'm not right now since
// I can't think of a use case for it. // I can't think of a use case for it.

View File

@ -1,14 +1,10 @@
use std::num::NonZeroU32; use std::num::NonZeroU32;
use anyhow::Result; use anyhow::Result;
use kcmc::{
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use super::fillet::EdgeReference;
pub use crate::execution::fn_call::Args; pub use crate::execution::fn_call::Args;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -28,8 +24,6 @@ use crate::{
ModuleId, ModuleId,
}; };
use super::fillet::EdgeReference;
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str = const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str =
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`"; "You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`";
@ -166,7 +160,7 @@ impl Args {
None => msg_base, None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"), Some(sugg) => format!("{msg_base}. {sugg}"),
}; };
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") { if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}"); message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
} }
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges())) KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -263,7 +257,7 @@ impl Args {
Some(sugg) => format!("{msg_base}. {sugg}"), Some(sugg) => format!("{msg_base}. {sugg}"),
}; };
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") { if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}"); message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
} }
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges())) KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -277,36 +271,7 @@ impl Args {
}) })
} }
// Add a modeling command to the batch but don't fire it right away. // TODO: Move this to the modeling module.
pub(crate) async fn batch_modeling_cmd(
&self,
id: uuid::Uuid,
cmd: ModelingCmd,
) -> Result<(), crate::errors::KclError> {
self.ctx.engine.batch_modeling_cmd(id, self.source_range, &cmd).await
}
// Add multiple modeling commands to the batch but don't fire them right away.
pub(crate) async fn batch_modeling_cmds(&self, cmds: &[ModelingCmdReq]) -> Result<(), crate::errors::KclError> {
self.ctx.engine.batch_modeling_cmds(self.source_range, cmds).await
}
// Add a modeling commandSolid> to the batch that gets executed at the end of the file.
// This is good for something like fillet or chamfer where the engine would
// eat the path id if we executed it right away.
pub(crate) async fn batch_end_cmd(&self, id: uuid::Uuid, cmd: ModelingCmd) -> Result<(), crate::errors::KclError> {
self.ctx.engine.batch_end_cmd(id, self.source_range, &cmd).await
}
/// Send the modeling cmd and wait for the response.
pub(crate) async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
cmd: ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
self.ctx.engine.send_modeling_cmd(id, self.source_range, &cmd).await
}
fn get_tag_info_from_memory<'a, 'e>( fn get_tag_info_from_memory<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, exec_state: &'e mut ExecState,
@ -330,6 +295,7 @@ impl Args {
} }
} }
// TODO: Move this to the modeling module.
pub(crate) fn get_tag_engine_info<'a, 'e>( pub(crate) fn get_tag_engine_info<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, exec_state: &'e mut ExecState,
@ -345,6 +311,7 @@ impl Args {
self.get_tag_info_from_memory(exec_state, tag) self.get_tag_info_from_memory(exec_state, tag)
} }
// TODO: Move this to the modeling module.
fn get_tag_engine_info_check_surface<'a, 'e>( fn get_tag_engine_info_check_surface<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, exec_state: &'e mut ExecState,
@ -362,63 +329,6 @@ impl Args {
self.get_tag_info_from_memory(exec_state, tag) self.get_tag_info_from_memory(exec_state, tag)
} }
/// Flush just the fillets and chamfers for this specific SolidSet.
#[allow(clippy::vec_box)]
pub(crate) async fn flush_batch_for_solids(
&self,
exec_state: &mut ExecState,
solids: &[Solid],
) -> Result<(), KclError> {
// Make sure we don't traverse sketches more than once.
let mut traversed_sketches = Vec::new();
// Collect all the fillet/chamfer ids for the solids.
let mut ids = Vec::new();
for solid in solids {
// We need to traverse the solids that share the same sketch.
let sketch_id = solid.sketch.id;
if !traversed_sketches.contains(&sketch_id) {
// Find all the solids on the same shared sketch.
ids.extend(
exec_state
.stack()
.walk_call_stack()
.filter(|v| matches!(v, KclValue::Solid { value } if value.sketch.id == sketch_id))
.flat_map(|v| match v {
KclValue::Solid { value } => value.get_all_edge_cut_ids(),
_ => unreachable!(),
}),
);
traversed_sketches.push(sketch_id);
}
ids.extend(solid.get_all_edge_cut_ids());
}
// We can return early if there are no fillets or chamfers.
if ids.is_empty() {
return Ok(());
}
// We want to move these fillets and chamfers from batch_end to batch so they get executed
// before what ever we call next.
for id in ids {
// Pop it off the batch_end and add it to the batch.
let Some(item) = self.ctx.engine.batch_end().write().await.shift_remove(&id) else {
// It might be in the batch already.
continue;
};
// Add it to the batch.
self.ctx.engine.batch().write().await.push(item);
}
// Run flush.
// Yes, we do need to actually flush the batch here, or references will fail later.
self.ctx.engine.flush_batch(false, self.source_range).await?;
Ok(())
}
pub(crate) fn make_kcl_val_from_point(&self, p: [f64; 2], ty: NumericType) -> Result<KclValue, KclError> { pub(crate) fn make_kcl_val_from_point(&self, p: [f64; 2], ty: NumericType) -> Result<KclValue, KclError> {
let meta = Metadata { let meta = Metadata {
source_range: self.source_range, source_range: self.source_range,
@ -448,6 +358,7 @@ impl Args {
) )
} }
// TODO: Move this to the modeling module.
pub(crate) async fn get_adjacent_face_to_tag( pub(crate) async fn get_adjacent_face_to_tag(
&self, &self,
exec_state: &mut ExecState, exec_state: &mut ExecState,
@ -537,107 +448,12 @@ impl Args {
} }
} }
/// Types which impl this trait can be read out of the `Args` passed into a KCL function.
pub trait FromArgs<'a>: Sized {
/// Get this type from the args passed into a KCL function, at the given index in the argument list.
fn from_args(args: &'a Args, index: usize) -> Result<Self, KclError>;
}
/// Types which impl this trait can be extracted from a `KclValue`. /// Types which impl this trait can be extracted from a `KclValue`.
pub trait FromKclValue<'a>: Sized { pub trait FromKclValue<'a>: Sized {
/// Try to convert a KclValue into this type. /// Try to convert a KclValue into this type.
fn from_kcl_val(arg: &'a KclValue) -> Option<Self>; fn from_kcl_val(arg: &'a KclValue) -> Option<Self>;
} }
impl<'a, T> FromArgs<'a> for T
where
T: FromKclValue<'a> + Sized,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(arg) = args.args.get(i) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Expected an argument at index {i}"),
vec![args.source_range],
)));
};
let Some(val) = T::from_kcl_val(&arg.value) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Argument at index {i} was supposed to be type {} but found {}",
tynm::type_name::<T>(),
arg.value.human_friendly_type(),
),
arg.source_ranges(),
)));
};
Ok(val)
}
}
impl<'a, T> FromArgs<'a> for Option<T>
where
T: FromKclValue<'a> + Sized,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(arg) = args.args.get(i) else { return Ok(None) };
if crate::parsing::ast::types::KclNone::from_kcl_val(&arg.value).is_some() {
return Ok(None);
}
let Some(val) = T::from_kcl_val(&arg.value) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Argument at index {i} was supposed to be type Option<{}> but found {}",
tynm::type_name::<T>(),
arg.value.human_friendly_type()
),
arg.source_ranges(),
)));
};
Ok(Some(val))
}
}
impl<'a, A, B> FromArgs<'a> for (A, B)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
Ok((a, b))
}
}
impl<'a, A, B, C> FromArgs<'a> for (A, B, C)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
C: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
let c = C::from_args(args, i + 2)?;
Ok((a, b, c))
}
}
impl<'a, A, B, C, D> FromArgs<'a> for (A, B, C, D)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
C: FromArgs<'a>,
D: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
let c = C::from_args(args, i + 2)?;
let d = D::from_args(args, i + 3)?;
Ok((a, b, c, d))
}
}
impl<'a> FromKclValue<'a> for TagNode { impl<'a> FromKclValue<'a> for TagNode {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok() arg.get_tag_declarator().ok()

View File

@ -7,7 +7,10 @@ use kittycad_modeling_cmds as kcmc;
use super::args::TyF64; use super::args::TyF64;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid}, execution::{
types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta,
Solid,
},
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::{fillet::EdgeReference, Args}, std::{fillet::EdgeReference, Args},
}; };
@ -52,20 +55,21 @@ async fn inner_chamfer(
}; };
let id = exec_state.next_uuid(); let id = exec_state.next_uuid();
args.batch_end_cmd( exec_state
id, .batch_end_cmd(
ModelingCmd::from(mcmd::Solid3dFilletEdge { ModelingCmdMeta::from_args_id(&args, id),
edge_id: None, ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_ids: vec![edge_id], edge_id: None,
extra_face_ids: vec![], edge_ids: vec![edge_id],
strategy: Default::default(), extra_face_ids: vec![],
object_id: solid.id, strategy: Default::default(),
radius: LengthUnit(length.to_mm()), object_id: solid.id,
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future. radius: LengthUnit(length.to_mm()),
cut_type: CutType::Chamfer, tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
}), cut_type: CutType::Chamfer,
) }),
.await?; )
.await?;
solid.edge_cuts.push(EdgeCut::Chamfer { solid.edge_cuts.push(EdgeCut::Chamfer {
id, id,

View File

@ -16,7 +16,7 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::{NumericType, PrimitiveType, RuntimeType}, types::{NumericType, PrimitiveType, RuntimeType},
ExecState, GeometryWithImportedGeometry, KclValue, Sketch, Solid, ExecState, GeometryWithImportedGeometry, KclValue, ModelingCmdMeta, Sketch, Solid,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::{extrude::NamedCapTags, Args}, std::{extrude::NamedCapTags, Args},
@ -64,7 +64,9 @@ async fn inner_clone(
} }
GeometryWithImportedGeometry::Solid(solid) => { GeometryWithImportedGeometry::Solid(solid) => {
// We flush before the clone so all the shit exists. // We flush before the clone so all the shit exists.
args.flush_batch_for_solids(exec_state, &[solid.clone()]).await?; exec_state
.flush_batch_for_solids((&args).into(), &[solid.clone()])
.await?;
let mut new_solid = solid.clone(); let mut new_solid = solid.clone();
new_solid.id = new_id; new_solid.id = new_id;
@ -78,7 +80,11 @@ async fn inner_clone(
return Ok(new_geometry); return Ok(new_geometry);
} }
args.batch_modeling_cmd(new_id, ModelingCmd::from(mcmd::EntityClone { entity_id: old_id })) exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(&args, new_id),
ModelingCmd::from(mcmd::EntityClone { entity_id: old_id }),
)
.await?; .await?;
fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args) fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
@ -169,9 +175,9 @@ async fn get_old_new_child_map(
args: &Args, args: &Args,
) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> { ) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
// Get the old geometries entity ids. // Get the old geometries entity ids.
let response = args let response = exec_state
.send_modeling_cmd( .send_modeling_cmd(
exec_state.next_uuid(), args.into(),
ModelingCmd::from(mcmd::EntityGetAllChildUuids { ModelingCmd::from(mcmd::EntityGetAllChildUuids {
entity_id: old_geometry_id, entity_id: old_geometry_id,
}), }),
@ -188,9 +194,9 @@ async fn get_old_new_child_map(
}; };
// Get the new geometries entity ids. // Get the new geometries entity ids.
let response = args let response = exec_state
.send_modeling_cmd( .send_modeling_cmd(
exec_state.next_uuid(), args.into(),
ModelingCmd::from(mcmd::EntityGetAllChildUuids { ModelingCmd::from(mcmd::EntityGetAllChildUuids {
entity_id: new_geometry_id, entity_id: new_geometry_id,
}), }),

View File

@ -12,7 +12,7 @@ use kittycad_modeling_cmds::{
use super::{args::TyF64, DEFAULT_TOLERANCE}; use super::{args::TyF64, DEFAULT_TOLERANCE};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{types::RuntimeType, ExecState, KclValue, Solid}, execution::{types::RuntimeType, ExecState, KclValue, ModelingCmdMeta, Solid},
std::{patterns::GeometryTrait, Args}, std::{patterns::GeometryTrait, Args},
}; };
@ -50,11 +50,11 @@ pub(crate) async fn inner_union(
} }
// Flush the fillets for the solids. // Flush the fillets for the solids.
args.flush_batch_for_solids(exec_state, &solids).await?; exec_state.flush_batch_for_solids((&args).into(), &solids).await?;
let result = args let result = exec_state
.send_modeling_cmd( .send_modeling_cmd(
solid_out_id, ModelingCmdMeta::from_args_id(&args, solid_out_id),
ModelingCmd::from(mcmd::BooleanUnion { ModelingCmd::from(mcmd::BooleanUnion {
solid_ids: solids.iter().map(|s| s.id).collect(), solid_ids: solids.iter().map(|s| s.id).collect(),
tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)), tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
@ -115,11 +115,11 @@ pub(crate) async fn inner_intersect(
} }
// Flush the fillets for the solids. // Flush the fillets for the solids.
args.flush_batch_for_solids(exec_state, &solids).await?; exec_state.flush_batch_for_solids((&args).into(), &solids).await?;
let result = args let result = exec_state
.send_modeling_cmd( .send_modeling_cmd(
solid_out_id, ModelingCmdMeta::from_args_id(&args, solid_out_id),
ModelingCmd::from(mcmd::BooleanIntersection { ModelingCmd::from(mcmd::BooleanIntersection {
solid_ids: solids.iter().map(|s| s.id).collect(), solid_ids: solids.iter().map(|s| s.id).collect(),
tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)), tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
@ -176,11 +176,13 @@ pub(crate) async fn inner_subtract(
// Flush the fillets for the solids and the tools. // Flush the fillets for the solids and the tools.
let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>(); let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
args.flush_batch_for_solids(exec_state, &combined_solids).await?; exec_state
.flush_batch_for_solids((&args).into(), &combined_solids)
.await?;
let result = args let result = exec_state
.send_modeling_cmd( .send_modeling_cmd(
solid_out_id, ModelingCmdMeta::from_args_id(&args, solid_out_id),
ModelingCmd::from(mcmd::BooleanSubtract { ModelingCmd::from(mcmd::BooleanSubtract {
target_ids: solids.iter().map(|s| s.id).collect(), target_ids: solids.iter().map(|s| s.id).collect(),
tool_ids: tools.iter().map(|s| s.id).collect(), tool_ids: tools.iter().map(|s| s.id).collect(),

View File

@ -9,7 +9,7 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::{ArrayLen, RuntimeType}, types::{ArrayLen, RuntimeType},
ExecState, ExtrudeSurface, KclValue, TagIdentifier, ExecState, ExtrudeSurface, KclValue, ModelingCmdMeta, TagIdentifier,
}, },
std::Args, std::Args,
}; };
@ -35,15 +35,16 @@ async fn inner_get_opposite_edge(
} }
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?; let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?; let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let tagged_path_id = tagged_path.id;
let sketch_id = tagged_path.sketch;
let resp = args let resp = exec_state
.send_modeling_cmd( .send_modeling_cmd(
id, (&args).into(),
ModelingCmd::from(mcmd::Solid3dGetOppositeEdge { ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
edge_id: tagged_path.id, edge_id: tagged_path_id,
object_id: tagged_path.sketch, object_id: sketch_id,
face_id, face_id,
}), }),
) )
@ -82,15 +83,16 @@ async fn inner_get_next_adjacent_edge(
} }
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?; let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?; let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let tagged_path_id = tagged_path.id;
let sketch_id = tagged_path.sketch;
let resp = args let resp = exec_state
.send_modeling_cmd( .send_modeling_cmd(
id, (&args).into(),
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge { ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: tagged_path.id, edge_id: tagged_path_id,
object_id: tagged_path.sketch, object_id: sketch_id,
face_id, face_id,
}), }),
) )
@ -138,15 +140,16 @@ async fn inner_get_previous_adjacent_edge(
} }
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?; let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?; let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let tagged_path_id = tagged_path.id;
let sketch_id = tagged_path.sketch;
let resp = args let resp = exec_state
.send_modeling_cmd( .send_modeling_cmd(
id, (&args).into(),
ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge { ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
edge_id: tagged_path.id, edge_id: tagged_path_id,
object_id: tagged_path.sketch, object_id: sketch_id,
face_id, face_id,
}), }),
) )
@ -221,14 +224,14 @@ async fn inner_get_common_edge(
// TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
// but for now, we'll just flush everything. // but for now, we'll just flush everything.
if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface { if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
args.ctx.engine.flush_batch(true, args.source_range).await?; exec_state.flush_batch((&args).into(), true).await?;
} else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface { } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
args.ctx.engine.flush_batch(true, args.source_range).await?; exec_state.flush_batch((&args).into(), true).await?;
} }
let resp = args let resp = exec_state
.send_modeling_cmd( .send_modeling_cmd(
id, ModelingCmdMeta::from_args_id(&args, id),
ModelingCmd::from(mcmd::Solid3dGetCommonEdge { ModelingCmd::from(mcmd::Solid3dGetCommonEdge {
object_id: first_tagged_path.sketch, object_id: first_tagged_path.sketch,
face_ids: [first_face_id, second_face_id], face_ids: [first_face_id, second_face_id],

View File

@ -19,8 +19,8 @@ use super::args::TyF64;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::RuntimeType, ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface, types::RuntimeType, ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch,
Solid, SketchSurface, Solid,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::Args, std::Args,
@ -85,7 +85,7 @@ async fn inner_extrude(
for sketch in &sketches { for sketch in &sketches {
let id = exec_state.next_uuid(); let id = exec_state.next_uuid();
args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds( let cmds = sketch.build_sketch_mode_cmds(
exec_state, exec_state,
ModelingCmdReq { ModelingCmdReq {
cmd_id: id.into(), cmd_id: id.into(),
@ -96,8 +96,10 @@ async fn inner_extrude(
opposite: opposite.clone(), opposite: opposite.clone(),
}), }),
}, },
)) );
.await?; exec_state
.batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
.await?;
solids.push( solids.push(
do_post_extrude( do_post_extrude(
@ -139,11 +141,12 @@ pub(crate) async fn do_post_extrude<'a>(
) -> Result<Solid, KclError> { ) -> Result<Solid, KclError> {
// Bring the object to the front of the scene. // Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806 // See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd( exec_state
exec_state.next_uuid(), .batch_modeling_cmd(
ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }), args.into(),
) ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
.await?; )
.await?;
let any_edge_id = if let Some(id) = edge_id { let any_edge_id = if let Some(id) = edge_id {
id id
@ -168,9 +171,9 @@ pub(crate) async fn do_post_extrude<'a>(
sketch.id = face.solid.sketch.id; sketch.id = face.solid.sketch.id;
} }
let solid3d_info = args let solid3d_info = exec_state
.send_modeling_cmd( .send_modeling_cmd(
exec_state.next_uuid(), args.into(),
ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo { ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
edge_id: any_edge_id, edge_id: any_edge_id,
object_id: sketch.id, object_id: sketch.id,
@ -193,14 +196,15 @@ pub(crate) async fn do_post_extrude<'a>(
// Getting the ids of a sectional sweep does not work well and we cannot guarantee that // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
// any of these call will not just fail. // any of these call will not just fail.
if !sectional { if !sectional {
args.batch_modeling_cmd( exec_state
exec_state.next_uuid(), .batch_modeling_cmd(
ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo { args.into(),
object_id: sketch.id, ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
edge_id: any_edge_id, object_id: sketch.id,
}), edge_id: any_edge_id,
) }),
.await?; )
.await?;
} }
} }

View File

@ -10,7 +10,8 @@ use super::{args::TyF64, DEFAULT_TOLERANCE};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier, types::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, ModelingCmdMeta,
Solid, TagIdentifier,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::Args, std::Args,
@ -111,20 +112,21 @@ async fn inner_fillet(
for _ in 0..num_extra_ids { for _ in 0..num_extra_ids {
extra_face_ids.push(exec_state.next_uuid()); extra_face_ids.push(exec_state.next_uuid());
} }
args.batch_end_cmd( exec_state
id, .batch_end_cmd(
ModelingCmd::from(mcmd::Solid3dFilletEdge { ModelingCmdMeta::from_args_id(&args, id),
edge_id: None, ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_ids: edge_ids.clone(), edge_id: None,
extra_face_ids, edge_ids: edge_ids.clone(),
strategy: Default::default(), extra_face_ids,
object_id: solid.id, strategy: Default::default(),
radius: LengthUnit(radius.to_mm()), object_id: solid.id,
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)), radius: LengthUnit(radius.to_mm()),
cut_type: CutType::Fillet, tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
}), cut_type: CutType::Fillet,
) }),
.await?; )
.await?;
let new_edge_cuts = edge_ids.into_iter().map(|edge_id| EdgeCut::Fillet { let new_edge_cuts = edge_ids.into_iter().map(|edge_id| EdgeCut::Fillet {
id, id,

View File

@ -9,7 +9,7 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
types::{PrimitiveType, RuntimeType}, types::{PrimitiveType, RuntimeType},
ExecState, Helix as HelixValue, KclValue, Solid, ExecState, Helix as HelixValue, KclValue, ModelingCmdMeta, Solid,
}, },
std::{axis_or_reference::Axis3dOrEdgeReference, Args}, std::{axis_or_reference::Axis3dOrEdgeReference, Args},
}; };
@ -124,17 +124,18 @@ async fn inner_helix(
} }
if let Some(cylinder) = cylinder { if let Some(cylinder) = cylinder {
args.batch_modeling_cmd( exec_state
id, .batch_modeling_cmd(
ModelingCmd::from(mcmd::EntityMakeHelix { ModelingCmdMeta::from_args_id(&args, id),
cylinder_id: cylinder.id, ModelingCmd::from(mcmd::EntityMakeHelix {
is_clockwise: !helix_result.ccw, cylinder_id: cylinder.id,
length: LengthUnit(length.as_ref().map(|t| t.to_mm()).unwrap_or(cylinder.height_in_mm())), is_clockwise: !helix_result.ccw,
revolutions, length: LengthUnit(length.as_ref().map(|t| t.to_mm()).unwrap_or(cylinder.height_in_mm())),
start_angle: Angle::from_degrees(angle_start), revolutions,
}), start_angle: Angle::from_degrees(angle_start),
) }),
.await?; )
.await?;
} else if let (Some(axis), Some(radius)) = (axis, radius) { } else if let (Some(axis), Some(radius)) = (axis, radius) {
match axis { match axis {
Axis3dOrEdgeReference::Axis { direction, origin } => { Axis3dOrEdgeReference::Axis { direction, origin } => {
@ -146,43 +147,45 @@ async fn inner_helix(
))); )));
}; };
args.batch_modeling_cmd( exec_state
id, .batch_modeling_cmd(
ModelingCmd::from(mcmd::EntityMakeHelixFromParams { ModelingCmdMeta::from_args_id(&args, id),
radius: LengthUnit(radius.to_mm()), ModelingCmd::from(mcmd::EntityMakeHelixFromParams {
is_clockwise: !helix_result.ccw, radius: LengthUnit(radius.to_mm()),
length: LengthUnit(length.to_mm()), is_clockwise: !helix_result.ccw,
revolutions, length: LengthUnit(length.to_mm()),
start_angle: Angle::from_degrees(angle_start), revolutions,
axis: Point3d { start_angle: Angle::from_degrees(angle_start),
x: direction[0].to_mm(), axis: Point3d {
y: direction[1].to_mm(), x: direction[0].to_mm(),
z: direction[2].to_mm(), y: direction[1].to_mm(),
}, z: direction[2].to_mm(),
center: Point3d { },
x: LengthUnit(origin[0].to_mm()), center: Point3d {
y: LengthUnit(origin[1].to_mm()), x: LengthUnit(origin[0].to_mm()),
z: LengthUnit(origin[2].to_mm()), y: LengthUnit(origin[1].to_mm()),
}, z: LengthUnit(origin[2].to_mm()),
}), },
) }),
.await?; )
.await?;
} }
Axis3dOrEdgeReference::Edge(edge) => { Axis3dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?; let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd( exec_state
id, .batch_modeling_cmd(
ModelingCmd::from(mcmd::EntityMakeHelixFromEdge { ModelingCmdMeta::from_args_id(&args, id),
radius: LengthUnit(radius.to_mm()), ModelingCmd::from(mcmd::EntityMakeHelixFromEdge {
is_clockwise: !helix_result.ccw, radius: LengthUnit(radius.to_mm()),
length: length.map(|t| LengthUnit(t.to_mm())), is_clockwise: !helix_result.ccw,
revolutions, length: length.map(|t| LengthUnit(t.to_mm())),
start_angle: Angle::from_degrees(angle_start), revolutions,
edge_id, start_angle: Angle::from_degrees(angle_start),
}), edge_id,
) }),
.await?; )
.await?;
} }
}; };
} }

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