Compare commits

..

30 Commits

Author SHA1 Message Date
d3d837c54d hide closed panels via css instead of unmounting them from dome (test) 2025-06-13 17:17:53 +02:00
af8d1330fd move useHotkey for undo/redo into App 2025-06-13 15:56:18 +02:00
939c2c77b0 the settings docs have a stutter with 2 h1s this fixes it (#7472)
* the settings docs have a stutter with 2 h1s this fixes it

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

* regenerate

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

---------

Signed-off-by: Jessie Frazelle <github@jessfraz.com>
2025-06-13 16:33:28 +10:00
31ec0184a1 Switch to plain Markdown template for releases (#7471) 2025-06-12 22:36:04 +00:00
62c4546658 Create an issue template for releases (#7467)
* Create an issue template for releases

* Remove invalid syntax

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>
2025-06-12 19:28:26 +00:00
383b38c2d2 Add operations for variable declarations (#7451)
* Add operations for variable declarations

* Update output

* Change to use OpKclValue

* Update output after merge
2025-06-12 16:38:12 +00:00
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
627 changed files with 11650403 additions and 11565724 deletions

38
.github/ISSUE_TEMPLATE/release.md vendored Normal file
View File

@ -0,0 +1,38 @@
---
name: Release
about: Create a new release for the Zoo Design Studio
title: "Cut release v1.?.?"
labels: [release]
---
> Instructions: https://github.com/KittyCAD/modeling-app/blob/main/CONTRIBUTING.md#shipping-releases
---
# Manual Checklist
Release builds URL: ???
## Windows via ???
* [ ] Download the release build for this platform
* [ ] Confirm the application opens (dismiss the updater)
* [ ] Create a project with a basic Text-to-CAD prompt
* [ ] Confirm the result is viewable in an engine stream
* [ ] Open the application again and confirm the updater can downgrade
## macOS via ???
* [ ] Download the release build for this platform
* [ ] Confirm the application opens (dismiss the updater)
* [ ] Create a project with a basic Text-to-CAD prompt
* [ ] Confirm the result is viewable in an engine stream
* [ ] Open the application again and confirm the updater can downgrade
## Linux via ???
* [ ] Download the release build for this platform
* [ ] Confirm the application opens (dismiss the updater)
* [ ] Create a project with a basic Text-to-CAD prompt
* [ ] Confirm the result is viewable in an engine stream
* [ ] Open the application again and confirm the updater can downgrade

View File

@ -10,7 +10,7 @@ on:
env:
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:
group: ${{ github.workflow }}-${{ github.ref }}
@ -91,14 +91,14 @@ jobs:
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "npm run build:wasm"
- name: Set nightly version, product name, release notes, and icons
if: ${{ env.IS_NIGHTLY == 'true' }}
- name: Set staging version, product name, release notes, and icons
if: ${{ env.IS_STAGING == 'true' }}
run: |
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +'%-y.%-m.%-d')
export VERSION=$DATE-main.$COMMIT
npm run files:set-version
npm run files:flip-to-nightly
npm run files:flip-to-staging
- name: Set release version
if: ${{ env.IS_RELEASE == 'true' }}
@ -130,13 +130,14 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-14
platform: mac
- os: windows-2022
platform: win
- os: namespace-profile-macos-6-cores
platform: macos
- os: namespace-profile-windows-4-cores
platform: windows
- os: ubuntu-22.04
platform: linux
runs-on: ${{ matrix.os }}
name: build-apps (${{ matrix.platform }})
env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
steps:
@ -166,14 +167,14 @@ jobs:
- run: npm install
- 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: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /c/Certificate_pkcs12.p12
cat /c/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=C:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
@ -181,7 +182,7 @@ jobs:
shell: bash
- 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: |
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
@ -191,7 +192,7 @@ jobs:
smksp_cert_sync.exe
smctl windows certsync
# 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
# 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.
@ -199,13 +200,13 @@ jobs:
shell: cmd
- 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,
# this is just not doing any codesign or release yml generation, and points to dev infra
run: npm run tronb:package:dev
- name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -215,7 +216,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
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/
run: ls -R out
@ -239,20 +240,20 @@ jobs:
out/*-x86_64-linux.*
- uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
with:
name: out-yml-${{ matrix.platform }}
path: |
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:
runs-on: ubuntu-22.04
permissions:
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') }}
env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
@ -263,32 +264,32 @@ jobs:
- uses: actions/download-artifact@v4
with:
name: out-arm64-win
name: out-arm64-windows
path: out
- uses: actions/download-artifact@v4
with:
name: out-x64-win
name: out-x64-windows
path: out
- uses: actions/download-artifact@v4
with:
name: out-yml-win
name: out-yml-windows
path: out
- uses: actions/download-artifact@v4
with:
name: out-arm64-mac
name: out-arm64-macos
path: out
- uses: actions/download-artifact@v4
with:
name: out-x64-mac
name: out-x64-macos
path: out
- uses: actions/download-artifact@v4
with:
name: out-yml-mac
name: out-yml-macos
path: out
- uses: actions/download-artifact@v4
@ -310,8 +311,8 @@ jobs:
env:
NOTES: ${{ needs.prepare-files.outputs.notes }}
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' }}
URL_CODED_NAME: ${{ env.IS_NIGHTLY == 'true' && 'Zoo%20Design%20Studio%20%28Nightly%29' || 'Zoo%20Design%20Studio' }}
WEBSITE_DIR: ${{ env.IS_STAGING == 'true' && 'dl.zoo.dev/releases/modeling-app/staging' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ env.IS_STAGING == 'true' && 'Zoo%20Design%20Studio%20%28Staging%29' || 'Zoo%20Design%20Studio' }}
run: |
RELEASE_DIR=https://${WEBSITE_DIR}
jq --null-input \
@ -360,26 +361,26 @@ jobs:
run: "ls -R out"
- name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }}
if: ${{ env.IS_STAGING == 'true' }}
uses: 'google-github-actions/auth@v2.1.8'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- 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
with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload nightly files to public bucket
if: ${{ env.IS_NIGHTLY == 'true' }}
- name: Upload staging files to public bucket
if: ${{ env.IS_STAGING == 'true' }}
uses: google-github-actions/upload-cloud-storage@v2.2.2
with:
path: out
glob: '*'
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
if: ${{ env.IS_NIGHTLY == 'true' }}
run: npm run files:invalidate-bucket:nightly
if: ${{ env.IS_STAGING == 'true' }}
run: npm run files:invalidate-bucket:staging

View File

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

View File

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

View File

@ -251,7 +251,8 @@ Before you submit a contribution PR to this repo, please ensure that:
#### 1. Create a 'Cut release $VERSION' issue
It will be used to document changelog discussions and release testing.
Use the **Release** issue template.
This will be used to facilitate changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new
@ -270,27 +271,9 @@ The workflow should be listed right away [in this list](https://github.com/Kitty
#### 3. Manually test artifacts
##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in step 2).
Manually test against [this list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
A prompt should show up asking for a downgrade to the last release version. Running through that at the end of testing
and making sure the current release candidate has the ability to be updated to what electron-updater points to is critical,
but what is actually being downloaded and installed isn't.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing.
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage
```
Assign someone to each section of the manual checklist generated by the issue template.
#### 4. Bump the KCL version

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
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)`.

View File

@ -4,8 +4,6 @@ excerpt: "Project specific settings for the app. These live in `project.toml` in
layout: manual
---
# Project Settings
Project specific settings for the app. These live in `project.toml` in the base of the project directory. Updating the settings for the project in the app will update this file automatically. Do not edit this file manually, as it may be overwritten by the app. Manual edits can cause corruption of the settings file.
## Project Configuration Structure
@ -184,4 +182,4 @@ color = 240.0
# Use inches as the default measurement unit
base_unit = "in"
```
```

View File

@ -4,8 +4,6 @@ excerpt: "User specific settings for the app. These live in `user.toml` in the a
layout: manual
---
# User Settings
User specific settings for the app. These live in `user.toml` in the app's configuration directory. Updating the settings in the app will update this file automatically. Do not edit this file manually, as it may be overwritten by the app. Manual edits can cause corruption of the settings file.
## User Configuration Structure
@ -234,4 +232,4 @@ base_unit = "mm"
# Disable text wrapping in the editor
text_wrapping = false
```
```

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
exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0])
|> angledLine(angle = 30, length = 2 * E ^ 2)
|> angledLine(angle = 30deg, length = 2 * E ^ 2)
|> yLine(endAbsolute = 0)
|> 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
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)`.
You might prefer to use `units::toRadians` or `units::toDegrees` to convert between angles with
different units.
It is better to use `units::toRadians` or `units::toDegrees` to convert between angles with
different units where possible.
### Examples

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ max(@input: [number; 1+]): number
```kcl
exampleSketch = startSketchOn(XZ)
|> 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])
|> close()

View File

@ -29,7 +29,7 @@ min(@input: [number; 1+]): number
```kcl
exampleSketch = startSketchOn(XZ)
|> 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])
|> close()

View File

@ -33,7 +33,7 @@ polar(
```kcl
exampleSketch = startSketchOn(XZ)
|> 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 = [segEndX(thing), 0])
|> line(end = [-20, 10])

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ layout: manual
Compute the tangent of a number.
```kcl
tan(@num: number(Angle)): number(_)
tan(@num: number(Angle)): number
```
@ -21,7 +21,7 @@ tan(@num: number(Angle)): number(_)
### Returns
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
[`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
@ -29,7 +29,7 @@ tan(@num: number(Angle)): number(_)
```kcl
exampleSketch = startSketchOn(XZ)
|> 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)
|> close()

View File

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

View File

@ -42,7 +42,7 @@ exampleSketch = startSketchOn(XZ)
|> line(endAbsolute = [5, 10])
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|> line(endAbsolute = [0, 20])
|> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10)
|> angledLineThatIntersects(angle = 80deg, intersectTag = lineToIntersect, offset = 10)
|> close()
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)
|> startProfile(at = [0, 0])
|> line(end = [10, 0])
|> arc(angleStart = 0, angleEnd = 280, radius = 16)
|> arc(angleStart = 0, angleEnd = 280deg, radius = 16)
|> close()
example = extrude(exampleSketch, length = 10)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ part001 = startSketchOn(XY)
|> line(end = [0, -5.5])
|> line(end = [-2, 0])
|> 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.
sketch001 = startSketchOn(XY)
|> 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 = [-2, 0])
|> 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 = [-2, 0])
|> close()
|> revolve(axis = Y, angle = 180)
|> revolve(axis = Y, angle = 180deg)
part002 = startSketchOn(part001, face = END)
|> startProfile(at = [4.5, -5])
@ -131,7 +131,7 @@ box = startSketchOn(XY)
sketch001 = startSketchOn(box, face = END)
|> 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)
|> 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)
|> 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)
|> 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)
|> 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)
|> 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)
|> 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)
|> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10, tag = $thing)
|> tangentialArc(angle = -120, radius = 5)
|> angledLine(angle = -60, length = segLen(thing))
|> tangentialArc(angle = -120deg, radius = 5)
|> angledLine(angle = -60deg, length = segLen(thing))
|> close()
example = extrude(exampleSketch, length = 5)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ Its properties are:
- `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")
@ -135,7 +135,7 @@ fn transform(@i) {
pow(0.9, exp = i)
],
// 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 {
translate = [0, 0, -i * width],
rotation = {
angle = 90 * i,
angle = 90deg * i,
// Rotate around the overall scene's origin.
origin = "global"
}
@ -219,7 +219,7 @@ fn transform(@i) {
// Transform functions can return multiple transforms. They'll be applied in order.
return [
{ translate = [30 * i, 0, 0] },
{ rotation = { angle = 45 * i } }
{ rotation = { angle = 45deg * i } }
]
}
startSketchOn(XY)

View File

@ -72,9 +72,9 @@ rotation.
sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -98,9 +98,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -124,9 +124,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -137,7 +137,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole)
|> 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
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)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -176,7 +176,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole)
|> 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)
sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90)
|> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
// 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()
|> 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])

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)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -93,7 +93,7 @@ cube
sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> 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))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
@ -103,7 +103,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90)
|> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93)
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)
|> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7])
|> tangentialArc(angle = 90, radius = 5)
|> tangentialArc(angle = 90deg, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc(angle = -90, radius = 5)
|> tangentialArc(angle = -90deg, radius = 5)
|> line(end = [0, 7])
// Create a hole for the pipe.
@ -91,7 +91,7 @@ cube
sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> 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))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
@ -101,7 +101,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90)
|> tangentialArc(radius = 80, angle = -90deg)
|> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
@ -162,7 +162,7 @@ profile001 = square()
profile002 = square()
|> translate(z = 20)
|> rotate(axis = [0, 0, 1.0], angle = 45)
|> rotate(axis = [0, 0, 1.0], angle = 45deg)
loft([profile001, profile002])

View File

@ -29,7 +29,7 @@ units::toDegrees(@num: number(Angle)): number(deg)
```kcl
exampleSketch = startSketchOn(XZ)
|> 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)
|> close()

View File

@ -29,7 +29,7 @@ units::toRadians(@num: number(Angle)): number(rad)
```kcl
exampleSketch = startSketchOn(XZ)
|> 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)
|> close()

File diff suppressed because one or more lines are too long

View File

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

View File

@ -302,7 +302,7 @@ test.describe('Command bar tests', () => {
// Assert that the an alternative variable name is chosen,
// 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(
'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 expect(cmdBar.currentArgumentInput).toBeVisible()
const addVariableButton = page.getByRole('button', {
name: 'Create new variable',
})
await addVariableButton.click()
await cmdBar.variableCheckbox.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',

View File

@ -172,6 +172,10 @@ export class CmdBarFixture {
return this.page.getByTestId('cmd-bar-arg-value')
}
get variableCheckbox() {
return this.page.getByTestId('cmd-bar-variable-checkbox')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
@ -191,7 +195,7 @@ export class CmdBarFixture {
* Clicks the Create new variable button for kcl input
*/
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',
})
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(
'angle001'
)

View File

@ -170,7 +170,7 @@ test(
// error text on hover
await page.hover('.cm-lint-marker-error')
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()
// black pixel means the scene has been cleared.
@ -369,7 +369,7 @@ test(
// error text on hover
await page.hover('.cm-lint-marker-error')
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()
// black pixel means the scene has been cleared.
@ -408,7 +408,7 @@ test(
// error text on hover
await page.hover('.cm-lint-marker-error')
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()
}
)

View File

@ -1,45 +1,54 @@
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', () => {
;[
{
codeLength: 1000,
showsErrorOnWindows: false,
},
{
codeLength: 2000,
showsErrorOnWindows: true,
},
].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
}
test(
`Open in desktop app with 2000-long code works non-Windows`,
{ tag: ['@web', '@macos', '@linux'] },
async ({ page }) => {
test.skip(process.platform === 'win32')
const codeLength = 2000
await navigateAndClickOpenInDesktopApp(page, codeLength)
await expect(getToastError(page)).not.toBeVisible()
}
)
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()
const toastError = page.getByText(
'The URL is too long to open in the desktop app on Windows'
)
if (isWindows && showsErrorOnWindows) {
await expect(toastError).toBeVisible()
} else {
await expect(toastError).not.toBeVisible()
// TODO: check if we could verify the deep link dialog shows up
}
}
)
})
test(
`Open in desktop app with 1000-long code works on Windows`,
{ tag: ['@web', '@windows'] },
async ({ page }) => {
test.skip(process.platform !== 'win32')
const codeLength = 1000
await navigateAndClickOpenInDesktopApp(page, codeLength)
await expect(getToastError(page)).not.toBeVisible()
}
)
test(
`Open in desktop app with 2000-long code doesn't work on Windows`,
{ tag: ['@web', '@windows'] },
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()
})
})
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 ({
scene,
toolbar,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

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()}`)
})
}
/**
* 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",
"electron-updater": "^6.6.2",
"fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0",
@ -93,6 +94,7 @@
"@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2",
"@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5",
@ -7491,6 +7493,13 @@
"@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": {
"version": "4.0.4",
"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_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": {
"version": "17.4.4",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz",

View File

@ -49,6 +49,7 @@
"diff": "^7.0.0",
"electron-updater": "^6.6.2",
"fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.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",
"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:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
"files:flip-to-nightly:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-nightly.ps1",
"files:flip-to-staging": "./scripts/flip-files-to-staging.sh",
"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:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"files:invalidate-bucket:staging": "./scripts/invalidate-files-bucket.sh --staging",
"postinstall": "electron-rebuild",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start",
@ -168,6 +169,7 @@
"@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2",
"@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5",

20
rust/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.80"
version = "0.2.81"
edition = "2021"
license = "MIT"
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 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!(
first.artifact_graph.len() < second.artifact_graph.len(),
"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 err = result.unwrap_err();
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")]
@ -1962,7 +1965,7 @@ someFunction('INVALID')
let err = err.as_kcl_error().unwrap();
assert_eq!(
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!(
err.source_ranges(),
@ -2087,7 +2090,7 @@ async fn kcl_test_better_type_names() {
},
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")]

View File

@ -508,8 +508,9 @@ impl EngineManager for EngineConnection {
.await?;
// Wait for the response.
let response_timeout = 300;
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;
if *guard == SocketHealth::Inactive {
// Check if we have any pending errors.

View File

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

View File

@ -45,26 +45,6 @@ pub struct ArtifactCommand {
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<()>;
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")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(),

View File

@ -2,7 +2,9 @@ use indexmap::IndexMap;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, NodePath, SourceRange};
#[cfg(feature = "artifact-graph")]
use crate::parsing::ast::types::{Node, Program};
use crate::{parsing::ast::types::ItemVisibility, ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
@ -26,6 +28,20 @@ pub enum Operation {
is_error: bool,
},
#[serde(rename_all = "camelCase")]
VariableDeclaration {
/// The variable name.
name: String,
/// The value of the variable.
value: OpKclValue,
/// The visibility modifier of the variable, e.g. `export`. `Default`
/// means there is no visibility modifier.
visibility: ItemVisibility,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
#[serde(rename_all = "camelCase")]
GroupBegin {
/// The details of the group.
group: Group,
@ -37,32 +53,36 @@ pub enum Operation {
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 {
/// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::GroupBegin { .. } | Self::GroupEnd => {}
Self::VariableDeclaration { .. } | 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::VariableDeclaration {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}

View File

@ -6,13 +6,14 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{
annotations,
cad_op::OpKclValue,
fn_call::Args,
kcl_value::{FunctionSource, TypeDef},
memory,
state::ModuleState,
types::{NumericType, PrimitiveType, RuntimeType},
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, StatementKind,
TagIdentifier,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, ModelingCmdMeta, ModuleArtifactState,
Operation, PlaneType, StatementKind, TagIdentifier,
},
fmt,
modules::{ModuleId, ModulePath, ModuleRepr},
@ -24,7 +25,7 @@ use crate::{
},
source_range::SourceRange,
std::args::TyF64,
CompilationError,
CompilationError, NodePath,
};
impl<'a> StatementKind<'a> {
@ -83,7 +84,7 @@ impl ExecutorContext {
preserve_mem: bool,
module_id: ModuleId,
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()));
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
@ -108,13 +109,16 @@ impl ExecutorContext {
} else {
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);
}
local_state.artifacts
} else {
Default::default()
};
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.
@ -326,6 +330,16 @@ impl ExecutorContext {
.mut_stack()
.add(var_name.clone(), rhs.clone(), source_range)?;
if rhs.show_variable_in_feature_tree() {
exec_state.push_op(Operation::VariableDeclaration {
name: var_name.clone(),
value: OpKclValue::from(&rhs),
visibility: variable_declaration.visibility,
node_path: NodePath::placeholder(),
source_range,
});
}
// Track exports.
if let ItemVisibility::Export = variable_declaration.visibility {
if matches!(body_type, BodyType::Root) {
@ -450,12 +464,12 @@ impl ExecutorContext {
if matches!(body_type, BodyType::Root) {
// Flush the batch queue.
self.engine
exec_state
.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
// and chamfers where the engine would otherwise eat the ID of the segments.
true,
SourceRange::new(program.end, program.end, program.module_id),
)
.await?;
}
@ -535,12 +549,12 @@ impl ExecutorContext {
let result = match &mut repr {
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
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await
.map(|(val, er, items)| {
*cache = Some((val, er, items.clone()));
.map(|(val, er, items, module_artifacts)| {
*cache = Some((val, er, items.clone(), module_artifacts.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
@ -566,28 +580,28 @@ impl ExecutorContext {
let result = match &mut repr {
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) => {
let result = self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await;
match result {
Ok((val, env, items)) => {
*cached_items = Some((val.clone(), env, items));
Ok((val, env, items, module_artifacts)) => {
*cached_items = Some((val.clone(), env, items, module_artifacts));
Ok(val)
}
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
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
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result {
Ok(val) => {
*cached = val.clone();
*cached = Some((val.clone(), exec_state.mod_local.artifacts.clone()));
Ok(val)
}
Err(e) => Err(e),
@ -609,7 +623,7 @@ impl ExecutorContext {
exec_state: &mut ExecState,
source_range: SourceRange,
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);
let result = self
.exec_module_body(program, exec_state, preserve_mem, module_id, path)
@ -798,6 +812,10 @@ fn apply_ascription(
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.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(|_| {
let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `number(mm)`"
@ -806,9 +824,14 @@ fn apply_ascription(
} else {
""
};
let ty_str = if let Some(ty) = value.principal_type() {
format!("(with type `{ty}`) ")
} else {
String::new()
};
KclError::new_semantic(KclErrorDetails::new(
format!(
"could not coerce value of type {} to type {ty}{suggestion}",
"could not coerce {} {ty_str}to type `{ty}`{suggestion}",
value.human_friendly_type()
),
vec![source_range],
@ -1018,14 +1041,13 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
}),
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(&t);
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()],
)))
}
(being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Only arrays can be indexed, but you're trying to index {}",
being_indexed.human_friendly_type()
),
vec![self.clone().into()],
))),
}
}
}
@ -1153,7 +1175,7 @@ impl Node<BinaryExpression> {
KclValue::Number { value: l / r, meta, ty }
}
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);
KclValue::Number { value: l % r, meta, ty }
}
@ -1200,11 +1222,14 @@ impl Node<BinaryExpression> {
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown {
// TODO suggest how to fix this
exec_state.warn(CompilationError::err(
self.as_source_range(),
format!("{} numbers which have unknown or incompatible units.", verb),
));
let sr = self.as_source_range();
exec_state.clear_units_warnings(&sr);
let mut err = CompilationError::err(
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 +1778,7 @@ a = 42: string
let err = result.unwrap_err();
assert!(
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:?}"
);
@ -1764,7 +1789,7 @@ a = 42: Plane
let err = result.unwrap_err();
assert!(
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:?}"
);
@ -1775,7 +1800,7 @@ arr = [0]: [string]
let err = result.unwrap_err();
assert!(
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:?}"
);
@ -1786,8 +1811,9 @@ mixedArr = [0, "a"]: [number(mm)]
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
err.to_string().contains(
"could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
),
"Expected error but found {err:?}"
);
}
@ -2092,4 +2118,19 @@ y = x: number(Length)"#;
assert_eq!(num.n, 2.0);
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(())
}
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_name: Option<&str>,
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.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
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
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
&rty,
true,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
"{label} requires {}",
type_err_str(ty, &arg.value, &arg.source_range, exec_state),
);
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.
@ -630,28 +669,20 @@ fn type_check_params_kw(
if let Some(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg {
arg.1.value = arg
.1
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?,
true,
exec_state,
)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"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],
))
})?;
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state),
),
vec![arg.1.source_range],
))
})?;
}
} else if let Some((name, _)) = &fn_def.input_arg {
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(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"This function requires its result to be of type `{}`, but found {}",
ty.human_friendly_type(),
val.human_friendly_type(),
"This function requires its result to be {}",
type_err_str(ret_ty, &val, &(&val).into(), exec_state)
),
ret_ty.as_source_ranges(),
))
@ -928,7 +958,7 @@ msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err();
assert_eq!(
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::{
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,
parsing::ast::types::{Annotation, Node},
source_range::SourceRange,
@ -257,15 +260,22 @@ pub struct PreImportedGeometry {
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(
pre.id,
pre.command.files.iter().map(|f| f.path.to_string()).collect(),
vec![pre.source_range.into()],
);
ctxt.engine
.async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
exec_state
.async_modeling_cmd(
ModelingCmdMeta::with_id(ctxt, pre.source_range, pre.id),
&ModelingCmd::from(pre.command.clone()),
)
.await?;
Ok(imported_geometry)

View File

@ -4,7 +4,6 @@ use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use super::types::UnitType;
use crate::{
errors::KclErrorDetails,
execution::{
@ -278,72 +277,85 @@ impl KclValue {
}
}
/// Returns true if we should generate an [`crate::execution::Operation`] to
/// display in the Feature Tree for variable declarations initialized with
/// this value.
pub(crate) fn show_variable_in_feature_tree(&self) -> bool {
match self {
KclValue::Uuid { .. } => false,
KclValue::Bool { .. } | KclValue::Number { .. } | KclValue::String { .. } => true,
KclValue::Tuple { .. }
| KclValue::HomArray { .. }
| KclValue::Object { .. }
| KclValue::TagIdentifier(_)
| KclValue::TagDeclarator(_)
| KclValue::Plane { .. }
| KclValue::Face { .. }
| KclValue::Sketch { .. }
| KclValue::Solid { .. }
| KclValue::Helix { .. }
| KclValue::ImportedGeometry(_)
| KclValue::Function { .. }
| KclValue::Module { .. }
| KclValue::Type { .. }
| KclValue::KclNone { .. } => false,
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
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 {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "Solid",
KclValue::Sketch { .. } => "Sketch",
KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane { .. } => "Plane",
KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(),
KclValue::TagDeclarator(_) => "a tag declarator".to_owned(),
KclValue::TagIdentifier(_) => "a tag identifier".to_owned(),
KclValue::Solid { .. } => "a solid".to_owned(),
KclValue::Sketch { .. } => "a sketch".to_owned(),
KclValue::Helix { .. } => "a helix".to_owned(),
KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(),
KclValue::Function { .. } => "a function".to_owned(),
KclValue::Plane { .. } => "a plane".to_owned(),
KclValue::Face { .. } => "a face".to_owned(),
KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(),
KclValue::Number {
ty: NumericType::Unknown,
..
} => "number(unknown units)",
} => "a number with unknown units".to_owned(),
KclValue::Number {
ty: NumericType::Known(UnitType::Length(_)),
ty: NumericType::Known(units),
..
} => "number(Length)",
KclValue::Number {
ty: NumericType::Known(UnitType::Angle(_)),
..
} => "number(Angle)",
KclValue::Number { .. } => "number",
KclValue::String { .. } => "string (text)",
KclValue::Tuple { .. } => "tuple (list)",
KclValue::HomArray { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::Module { .. } => "module",
KclValue::Type { .. } => "type",
KclValue::KclNone { .. } => "None",
} => format!("a number ({units})"),
KclValue::Number { .. } => "a number".to_owned(),
KclValue::String { .. } => "a string".to_owned(),
KclValue::Object { .. } => "an object".to_owned(),
KclValue::Module { .. } => "a module".to_owned(),
KclValue::Type { .. } => "a type".to_owned(),
KclValue::KclNone { .. } => "none".to_owned(),
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
if value.is_empty() {
"an empty array".to_owned()
} else {
// A max of 3 is good because it's common to use 3D points.
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 {
@ -602,6 +614,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> {
match self {
KclValue::Bool { value, .. } => Some(format!("{value}")),
@ -649,6 +668,8 @@ impl From<GeometryWithImportedGeometry> for KclValue {
#[cfg(test)]
mod tests {
use crate::exec::UnitType;
use super::*;
#[test]
@ -658,21 +679,21 @@ mod tests {
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
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 {
value: 1.0,
ty: NumericType::Unknown,
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 {
value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
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 {
value: vec![mm.clone()],
@ -680,7 +701,7 @@ mod tests {
};
assert_eq!(
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 {
@ -689,7 +710,7 @@ mod tests {
};
assert_eq!(
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 {
@ -698,7 +719,7 @@ mod tests {
};
assert_eq!(
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 {
@ -712,14 +733,14 @@ mod tests {
};
assert_eq!(
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 {
value: vec![],
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 {
value: vec![array2_mm.clone()],
@ -727,7 +748,7 @@ mod tests {
};
assert_eq!(
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

@ -8,7 +8,8 @@ pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketc
use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")]
pub use cad_op::{Group, Operation};
pub use cad_op::Group;
pub use cad_op::Operation;
pub use geometry::*;
pub use id_generator::IdGenerator;
pub(crate) use import::PreImportedGeometry;
@ -22,8 +23,10 @@ use kcmc::{
};
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
pub use memory::EnvironmentRef;
pub(crate) use modeling::ModelingCmdMeta;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) use state::ModuleArtifactState;
pub use state::{ExecState, MetaSettings};
use uuid::Uuid;
@ -56,6 +59,7 @@ mod import;
mod import_graph;
pub(crate) mod kcl_value;
mod memory;
mod modeling;
mod state;
pub mod typed_path;
pub(crate) mod types;
@ -76,9 +80,6 @@ pub struct ExecOutcome {
/// the Feature Tree.
#[cfg(feature = "artifact-graph")]
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.
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
@ -575,7 +576,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().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);
cache::write_old_memory((mem, module_infos)).await;
@ -773,15 +774,12 @@ impl ExecutorContext {
))
.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)
}
/// 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`.
pub async fn run(
&self,
@ -794,9 +792,6 @@ impl ExecutorContext {
/// Perform the execution of a program using a concurrent
/// 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`.
pub async fn run_concurrent(
&self,
@ -842,6 +837,8 @@ impl ExecutorContext {
let module_id = *module_id;
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
// Clone before mutating.
let module_exec_state = exec_state.clone();
self.add_import_module_ops(
exec_state,
@ -853,7 +850,6 @@ impl ExecutorContext {
);
let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone();
let results_tx = results_tx.clone();
@ -873,11 +869,13 @@ impl ExecutorContext {
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
}
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
.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(
format!("Module {module_path} not found in universe"),
@ -889,7 +887,7 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")]
{
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 result = exec_module(
@ -911,7 +909,7 @@ impl ExecutorContext {
#[cfg(not(target_arch = "wasm32"))]
{
set.spawn(async move {
let mut exec_state = exec_state;
let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_module(
@ -964,6 +962,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
}
@ -993,6 +1000,18 @@ impl ExecutorContext {
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")]
fn add_import_module_ops(
&self,
@ -1042,18 +1061,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
/// output everything.
async fn inner_run(
@ -1121,26 +1128,32 @@ impl ExecutorContext {
&ModulePath::Main,
)
.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")]
{
// Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op {
Operation::StdLibCall {
node_path,
source_range,
..
op.fill_node_paths(program, cached_body_items);
}
#[cfg(test)]
{
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 +1166,7 @@ impl ExecutorContext {
self.engine.clear_queues().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)),
}
}
@ -1163,6 +1176,9 @@ impl ExecutorContext {
/// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
if exec_state.stack().memory.requires_std() {
#[cfg(feature = "artifact-graph")]
let initial_ops = exec_state.global.artifacts.operations.len();
let path = vec!["std".to_owned(), "prelude".to_owned()];
let resolved_path = ModulePath::from_std_import_path(&path)?;
let id = self
@ -1171,6 +1187,14 @@ impl ExecutorContext {
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
exec_state.mut_stack().memory.set_std(module_memory);
// Operations generated by the prelude are not useful, so clear them
// out.
//
// TODO: Should we also clear them out of each module so that they
// don't appear in test output?
#[cfg(feature = "artifact-graph")]
exec_state.global.artifacts.operations.truncate(initial_ops);
}
Ok(())
@ -1919,13 +1943,13 @@ notNull = !myNull
"#;
assert_eq!(
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";
assert_eq!(
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#"
@ -1933,7 +1957,7 @@ notEmptyString = !""
"#;
assert_eq!(
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#"
@ -1942,7 +1966,7 @@ notMember = !obj.a
"#;
assert_eq!(
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 = "
@ -1950,7 +1974,7 @@ a = []
notArray = !a";
assert_eq!(
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 = "
@ -1958,7 +1982,7 @@ x = {}
notObject = !x";
assert_eq!(
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 = "
@ -1984,7 +2008,7 @@ notTagDeclarator = !myTagDeclarator";
assert!(
tag_declarator_err
.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: {:?}",
tag_declarator_err
);
@ -1998,7 +2022,7 @@ notTagIdentifier = !myTag";
assert!(
tag_identifier_err
.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: {:?}",
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 indexmap::IndexMap;
#[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 serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -50,6 +52,8 @@ pub(super) struct GlobalState {
pub errors: Vec<CompilationError>,
#[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))]
pub artifacts: ArtifactState,
#[cfg_attr(not(all(test, feature = "artifact-graph")), expect(dead_code))]
pub root_module_artifacts: ModuleArtifactState,
}
#[cfg(feature = "artifact-graph")]
@ -77,6 +81,20 @@ pub(super) struct ArtifactState {
#[derive(Debug, Clone, Default)]
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)]
pub(super) struct ModuleState {
/// The id generator for this module.
@ -96,6 +114,7 @@ pub(super) struct ModuleState {
pub settings: MetaSettings,
pub(super) explicit_length_units: bool,
pub(super) path: ModulePath,
pub artifacts: ModuleArtifactState,
}
impl ExecState {
@ -126,6 +145,17 @@ impl ExecState {
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] {
&self.global.errors
}
@ -133,7 +163,7 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as
/// 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
// state when we add more to ExecState.
ExecOutcome {
@ -142,22 +172,18 @@ impl ExecState {
#[cfg(feature = "artifact-graph")]
operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifacts.graph,
errors: self.global.errors,
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 {
variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(),
errors: self.global.errors,
filenames: Default::default(),
@ -188,12 +214,22 @@ impl ExecState {
}
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")]
self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))]
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 {
ModuleId::from_usize(self.global.path_to_source_id.len())
}
@ -241,6 +277,21 @@ impl ExecState {
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 {
NumericType::Default {
len: self.length_unit(),
@ -349,6 +400,7 @@ impl GlobalState {
path_to_source_id: Default::default(),
module_infos: Default::default(),
artifacts: Default::default(),
root_module_artifacts: Default::default(),
mod_loader: Default::default(),
errors: 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 {
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
ModuleState {
@ -403,6 +464,7 @@ impl ModuleState {
default_angle_units: Default::default(),
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 {
#[cfg(target_arch = "wasm32")]
{

View File

@ -438,7 +438,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Any => write!(f, "any"),
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
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::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"),
@ -453,8 +453,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
PrimitiveType::Function => write!(f, "function"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "fn"),
}
}
}
@ -499,20 +499,6 @@ impl NumericType {
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
/// 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),
(Any, bt) => (a.n, b.n, bt),
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
// 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::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(UnitType::Count), Default { .. }) | (Default { .. }, 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),
(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),
(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 {
match suffix {
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.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
@ -1510,6 +1508,23 @@ impl KclValue {
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)]
@ -2342,10 +2357,10 @@ d = cos(30)
let result = parse_execute(program).await.unwrap();
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("c", &result, 1.0, NumericType::count());
assert_value_and_type("d", &result, 1.0, NumericType::count());
assert_value_and_type("c", &result, 1.0, NumericType::default());
assert_value_and_type("d", &result, 1.0, NumericType::default());
}
#[tokio::test(flavor = "multi_thread")]

View File

@ -47,7 +47,7 @@ impl Tag {
match self {
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
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 {
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!(),
}
@ -3900,7 +3900,7 @@ startSketchOn(XY)
match hover.unwrap().contents {
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!(),
}

View File

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

View File

@ -3174,6 +3174,19 @@ impl PrimitiveType {
_ => 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 {
@ -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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

@ -4,8 +4,6 @@ excerpt: "{{{description}}}"
layout: manual
---
# {{title}}
{{{description}}}
## {{config_type}} Structure
@ -64,4 +62,4 @@ This setting has further nested options. See the schema for full details.
```toml
{{{example}}}
```
```

View File

@ -3,13 +3,18 @@ use std::{
path::{Path, PathBuf},
};
use indexmap::IndexMap;
use insta::rounded_redaction;
use crate::{errors::KclError, ModuleId};
use crate::{
errors::KclError,
execution::{EnvironmentRef, ModuleArtifactState},
ExecOutcome, ExecState, ExecutorContext, ModuleId,
};
#[cfg(feature = "artifact-graph")]
use crate::{
exec::ArtifactCommand,
execution::{ArtifactGraph, Operation},
modules::{ModulePath, ModuleRepr},
};
mod kcl_samples;
@ -19,8 +24,7 @@ mod kcl_samples;
struct Test {
/// The name of the test.
name: String,
/// The name of the KCL file that's the entry point, e.g. "main.kcl", in the
/// `input_dir`.
/// The KCL file that's the entry point, e.g. "main.kcl", in the `input_dir`.
entry_point: PathBuf,
/// Input KCL files are in this directory.
input_dir: PathBuf,
@ -34,6 +38,9 @@ struct Test {
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
#[cfg(feature = "artifact-graph")]
const REPO_ROOT: &str = "../..";
impl Test {
fn new(name: &str) -> 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)
where
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");
}
}
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(|| {
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")]
assert_common_snapshots(
test,
outcome.operations,
outcome.artifact_commands,
outcome.artifact_graph,
);
assert_artifact_snapshots(test, module_state, outcome.operations, outcome.artifact_graph);
mem_result.unwrap();
}
Err(e) => {
@ -238,7 +311,23 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
}));
#[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();
}
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
/// when it results in an error.
/// Assert snapshots for artifacts that should happen both when KCL execution
/// succeeds and when it results in an error.
#[cfg(feature = "artifact-graph")]
fn assert_common_snapshots(
fn assert_artifact_snapshots(
test: &Test,
operations: Vec<Operation>,
artifact_commands: Vec<ArtifactCommand>,
module_state: IndexMap<String, ModuleArtifactState>,
global_operations: Vec<Operation>,
artifact_graph: ArtifactGraph,
) {
let operations = {
// Make the operations deterministic by sorting them by their module ID,
// then by their range.
let mut operations = operations.clone();
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 module_operations = module_state
.iter()
.map(|(path, s)| (path, &s.operations))
.collect::<IndexMap<_, _>>();
let result1 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Operations executed", || {
insta::assert_json_snapshot!("ops", operations, {
"[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
"[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
"[].**.value.value" => rounded_redaction(3),
"[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
"[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
insta::assert_json_snapshot!("ops", module_operations, {
".*[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
".*[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
".*[].**.value.value" => rounded_redaction(3),
".*[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
".*[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
".**.sourceRange" => Vec::new(),
".**.functionSourceRange" => Vec::new(),
".**.moduleId" => 0,
});
})
}));
let module_commands = module_state
.iter()
.map(|(path, s)| (path, &s.commands))
.collect::<IndexMap<_, _>>();
let result2 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Artifact commands", || {
insta::assert_json_snapshot!("artifact_commands", artifact_commands, {
"[].command.**.value" => rounded_redaction(3),
"[].command.**.x" => rounded_redaction(3),
"[].command.**.y" => rounded_redaction(3),
"[].command.**.z" => rounded_redaction(3),
insta::assert_json_snapshot!("artifact_commands", module_commands, {
".*[].command.**.value" => rounded_redaction(3),
".*[].command.**.x" => rounded_redaction(3),
".*[].command.**.y" => rounded_redaction(3),
".*[].command.**.z" => rounded_redaction(3),
".**.range" => Vec::new(),
});
})
@ -337,6 +414,25 @@ fn assert_common_snapshots(
result1.unwrap();
result2.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 {
@ -402,8 +498,8 @@ mod any_type {
super::execute(TEST_NAME, false).await
}
}
mod error_with_point_shows_numeric_units {
const TEST_NAME: &str = "error_with_point_shows_numeric_units";
mod coerce_from_trig_to_point {
const TEST_NAME: &str = "coerce_from_trig_to_point";
/// Test parsing KCL.
#[test]
@ -3546,3 +3642,27 @@ mod var_ref_in_own_def_decl {
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,
};
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid_id,
color,
metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)
.await?;
exec_state
.batch_modeling_cmd(
(&args).into(),
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
object_id: solid_id,
color,
metalness: metalness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0,
ambient_occlusion: 0.0,
}),
)
.await?;
// 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.

View File

@ -1,14 +1,10 @@
use std::num::NonZeroU32;
use anyhow::Result;
use kcmc::{
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::Serialize;
use super::fillet::EdgeReference;
pub use crate::execution::fn_call::Args;
use crate::{
errors::{KclError, KclErrorDetails},
@ -28,8 +24,6 @@ use crate::{
ModuleId,
};
use super::fillet::EdgeReference;
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`";
@ -166,7 +160,7 @@ impl Args {
None => msg_base,
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}");
}
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -263,7 +257,7 @@ impl Args {
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}");
}
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.
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
}
// TODO: Move this to the modeling module.
fn get_tag_info_from_memory<'a, 'e>(
&'a self,
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>(
&'a self,
exec_state: &'e mut ExecState,
@ -345,6 +311,7 @@ impl Args {
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>(
&'a self,
exec_state: &'e mut ExecState,
@ -362,63 +329,6 @@ impl Args {
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> {
let meta = Metadata {
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(
&self,
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`.
pub trait FromKclValue<'a>: Sized {
/// Try to convert a KclValue into this type.
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 {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok()

View File

@ -7,7 +7,10 @@ use kittycad_modeling_cmds as kcmc;
use super::args::TyF64;
use crate::{
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,
std::{fillet::EdgeReference, Args},
};
@ -52,20 +55,21 @@ async fn inner_chamfer(
};
let id = exec_state.next_uuid();
args.batch_end_cmd(
id,
ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id: None,
edge_ids: vec![edge_id],
extra_face_ids: vec![],
strategy: Default::default(),
object_id: solid.id,
radius: LengthUnit(length.to_mm()),
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
cut_type: CutType::Chamfer,
}),
)
.await?;
exec_state
.batch_end_cmd(
ModelingCmdMeta::from_args_id(&args, id),
ModelingCmd::from(mcmd::Solid3dFilletEdge {
edge_id: None,
edge_ids: vec![edge_id],
extra_face_ids: vec![],
strategy: Default::default(),
object_id: solid.id,
radius: LengthUnit(length.to_mm()),
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
cut_type: CutType::Chamfer,
}),
)
.await?;
solid.edge_cuts.push(EdgeCut::Chamfer {
id,

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