Compare commits

...

17 Commits

Author SHA1 Message Date
06b35b76ff Refactor logic to skip unreliable tests (#5919) 2025-03-21 03:52:30 +00:00
89d1f7f3d3 fix resize (#5921)
* fix resize

* assert stream dimensions
2025-03-21 02:13:26 +00:00
66b9b501ac test: Fix snapshot test to use mask (#5914)
* Fix snapshot test to use mask

* A snapshot a day keeps the bugs away! 📷🐛

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-21 01:20:47 +00:00
9248b2e42d #5905 Cleanup console warnings (#5908)
* Fix ToolBar WebkitAppRegion warning

* make intersectionPlane non-nullable, avoid trying to create it multiple times to get rid of warning

* remove derived scene from sceneEntities

* intersectionPlane is now always non-null, make it readonly too

* sceneInfra small cleanups

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* Clean up snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-03-21 00:55:20 +00:00
054bb5b500 Add sectional argument and edit flow for point-and-click Sweep (#5480)
* WIP: Expose the sectional argument in the Sweep command flow
Fixes #5301

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Working edit flow

* Lint

* Allow in place editing, more consistent code

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Remove validation on non-selection arg

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Comment out bad test

* Clean up for review

* Hack sectional

* Made selection args hidden

* Fix edit issue in e2e

* Clean up

* Add face filtering filter for opposite and next adjacent faces

* Lint

* Fixme back

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* Use updateModelingState in codemod

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* Improve filtering readibility

* Fix base test

* I liked return but clippy didn't

* Working tests, isolating the change to sectional sweep, don't like the api change

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* Clean up snapshots

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 20:42:41 -04:00
12b546ea24 Bump the patch group across 1 directory with 7 updates (#5888)
* Bump the patch group across 1 directory with 7 updates

Bumps the patch group with 6 updates in the /rust directory:

| Package | From | To |
| --- | --- | --- |
| [async-trait](https://github.com/dtolnay/async-trait) | `0.1.87` | `0.1.88` |
| [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) | `0.2.105` | `0.2.107` |
| [zip](https://github.com/zip-rs/zip2) | `2.4.1` | `2.4.2` |
| [time](https://github.com/time-rs/time) | `0.3.39` | `0.3.40` |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.14` | `0.12.15` |
| [handlebars](https://github.com/sunng87/handlebars-rust) | `6.3.1` | `6.3.2` |



Updates `async-trait` from 0.1.87 to 0.1.88
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.87...0.1.88)

Updates `kittycad-modeling-cmds` from 0.2.105 to 0.2.107
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.105...kittycad-modeling-cmds-0.2.107)

Updates `uuid` from 1.15.1 to 1.16.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.15.1...v1.16.0)

Updates `zip` from 2.4.1 to 2.4.2
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v2.4.1...v2.4.2)

Updates `time` from 0.3.39 to 0.3.40
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.39...v0.3.40)

Updates `reqwest` from 0.12.14 to 0.12.15
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.14...v0.12.15)

Updates `handlebars` from 6.3.1 to 6.3.2
- [Release notes](https://github.com/sunng87/handlebars-rust/releases)
- [Changelog](https://github.com/sunng87/handlebars-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunng87/handlebars-rust/compare/v6.3.1...v6.3.2)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch
- dependency-name: zip
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: time
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: handlebars
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 22:50:43 +00:00
bc6c40c1e8 Remove unused xstate typegen (#5917) 2025-03-20 22:14:39 +00:00
1d550da40b More types stuff (#5901)
* parse union and fancy array types

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

* type aliases

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

* Treat Helix and Face as primitive types

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

* code motion: factor our execution::types module

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

* Tests for type coercion and subtyping

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

* Add Point2D/3D to std

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

* Rebasing and fixes

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-21 10:56:55 +13:00
9da8574103 Add edit flow for named constants / parameters (#5911)
* Add support for forcing kcl input create variable

* Command palette padding tweak

* Make traverse function work for ExpressionStatements

* Add utilities for getting earliest safe index in AST

* Fix the insertIndex logic to not be based on the selection anymore

* Add workflow to create a named constant

* Fix bug with nameEndInDigits matcher

* Tweak command config

* Add a three-dot menu to feature tree pane to create parameters

* Add E2E test for create parameter flow

* Remove edit flow oops

* Fix tsc error

* Fix E2E test

* Update named constant position in edit flow test

* Add tags into consideration for safe insert index

Per @Irev-dev's helpful feedback, with unit tests!

* Fix tsc by removing a generic type

* Remove unused imports

* Fix lints

* A snapshot a day keeps the bugs away! 📷🐛

* Add utilities for working with variable declarations

* Add "edit parameter" user flow

* Add edit flow config

* WIP working on de-bloating useCalculateKclExpreesion

* Add the ability to specify a `displayName` for an arg

* Add utility to type check on SourceRanges

* Review step design tweak fixes

* Refactor useCalculateKclExpression to take a sourceRange

* Make option arg validation work for objects and arrays

Using an admittedly dumb stringification approach

* Make edit flow never move the constant to be edited

* Add E2E test section

* Fix lints

* Remove lying comment, tiny CSS tweak

* A snapshot a day keeps the bugs away! 📷🐛

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 16:41:09 -04:00
2c6404f671 Continue running broken tests for Axiom metrics (#5883)
* Install Vector on Ubuntu to log failed test to Axiom

* Allow flaky tests to run on main for Axiom metrics

* Enable problematic tests on a dedicated branch
2025-03-20 16:28:08 -04:00
09c6f51141 Bump the patch group in /packages/codemirror-lsp-client with 2 updates (#5825) 2025-03-20 20:10:15 +00:00
755c7df59c Expand Makefile to run web and desktop apps (#5916) 2025-03-20 16:08:35 -04:00
73b38cd9e2 Bump the patch group with 2 updates (#5826)
Bumps the patch group with 2 updates: [taiki-e/install-action](https://github.com/taiki-e/install-action) and [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud).


Updates `taiki-e/install-action` from 2.49.15 to 2.49.27
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.15...v2.49.27)

Updates `google-github-actions/setup-gcloud` from 2.1.2 to 2.1.4
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/setup-gcloud/compare/v2.1.2...v2.1.4)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: google-github-actions/setup-gcloud
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 13:02:39 -07:00
227cb70d72 Bump the patch group in /rust/kcl-language-server with 2 updates (#5834)
* Bump the patch group in /rust/kcl-language-server with 2 updates

Bumps the patch group in /rust/kcl-language-server with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [esbuild](https://github.com/evanw/esbuild).


Updates `@types/node` from 22.13.9 to 22.13.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `esbuild` from 0.25.0 to 0.25.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.0...v0.25.1)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 12:12:59 -07:00
b36e416ab2 Only show "Edit sketch" button when code pane is focused with sketch selected (#5691)
* Only show "Edit sketch" button when code pane is focused with sketch selected

Closes #4273. WIP until tests are updated, since this will impact many
that look for the "Edit sketch" button.

* Start removing "edit sketch" point-and-click from E2E

* Update more E2E tests

* Update more tests that assumed Edit Sketch button

* A snapshot a day keeps the bugs away! 📷🐛

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 17:10:28 +00:00
612d03bf73 Bump the major group in /.github/actions/github-release with 2 updates (#5829)
Bumps the major group in /.github/actions/github-release with 2 updates: [@actions/github](https://github.com/actions/toolkit/tree/HEAD/packages/github) and [glob](https://github.com/isaacs/node-glob).


Updates `@actions/github` from 5.1.1 to 6.0.0
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/github/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/github)

Updates `glob` from 7.2.3 to 11.0.1
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v7.2.3...v11.0.1)

---
updated-dependencies:
- dependency-name: "@actions/github"
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major
- dependency-name: glob
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 09:27:44 -07:00
c8ec35cd4a Clean KCL Samples and Update Walkie Talkie (#5904)
* Clean KCL Samples and Update Walkie Talkie

* revolve keyword args

* Update kcl-samples simulation test output

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-20 09:22:17 -07:00
160 changed files with 13695 additions and 45402 deletions

View File

@ -4,7 +4,7 @@
"main": "main.js",
"dependencies": {
"@actions/core": "^1.6",
"@actions/github": "^5.0",
"glob": "^7.1.5"
"@actions/github": "^6.0",
"glob": "^11.0.1"
}
}

View File

@ -4,27 +4,27 @@
set -euo pipefail
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
# If no last run artifact, than run Playwright normally
echo "run playwright normally"
if [[ "$3" == *ubuntu* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true
elif [[ "$3" == *windows* ]]; then
yarn test:playwright:electron:windows -- --shard=$1/$2 || true
elif [[ "$3" == *macos* ]]; then
yarn test:playwright:electron:macos -- --shard=$1/$2 || true
else
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
if [[ "$3" == *ubuntu* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true
elif [[ "$3" == *windows* ]]; then
yarn test:playwright:electron:windows -- --shard=$1/$2 || true
elif [[ "$3" == *macos* ]]; then
yarn test:playwright:electron:macos -- --shard=$1/$2 || true
else
echo "Do not run Playwright. Unable to detect os runtime."
exit 1
fi
# Log failures for Axiom to pick up
node playwrightProcess.mjs > /tmp/github-actions.log
fi
retry=1
max_retrys=1
max_retries=1
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
# Retry failed tests, doing our own retries because using inbuilt Playwright retries causes connection issues
while [[ $retry -le $max_retries ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
@ -40,8 +40,8 @@ while [[ $retry -le $max_retrys ]]; do
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
# Log failures for Axiom to pick up
node playwrightProcess.mjs > /tmp/github-actions.log
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
@ -58,7 +58,7 @@ echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
# If it still fails after 3 retries, then fail the job
exit 1
fi
fi

View File

@ -24,7 +24,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with:
tool: wasm-pack
- name: Rust Cache

View File

@ -77,7 +77,7 @@ jobs:
with:
cache: false # Configured below.
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with:
tool: wasm-pack

View File

@ -137,7 +137,7 @@ jobs:
with:
cache: false # Configured below.
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
with:
tool: wasm-pack
@ -340,14 +340,22 @@ jobs:
if: needs.conditions.outputs.should-run == 'true'
run: yarn tronb:vite:dev
- name: Install good sed
if: startsWith(matrix.os, 'macos')
- name: Install vector
if: contains(matrix.os, 'ubuntu')
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
# TODO: Add back axiom logs
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- uses: actions/download-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}

View File

@ -376,7 +376,7 @@ jobs:
with:
credentials_json: "${{ secrets.GOOGLE_CLOUD_DL_SA }}"
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.2
uses: google-github-actions/setup-gcloud@v2.1.4
with:
project_id: kittycadapi
- name: "upload files to gcp"

View File

@ -37,7 +37,7 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with:
tool: wasm-pack
- run: yarn build:wasm
@ -57,7 +57,7 @@ jobs:
with:
workspaces: './rust'
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with:
tool: wasm-pack
- run: yarn build:wasm
@ -100,7 +100,7 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with:
tool: wasm-pack
- run: yarn build:wasm
@ -129,7 +129,7 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
- uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with:
tool: wasm-pack
- run: yarn build:wasm

1
.gitignore vendored
View File

@ -50,6 +50,7 @@ e2e/playwright/**/*.png
e2e/playwright/export-snapshots/*
!e2e/playwright/export-snapshots/*.png
!e2e/playwright/snapshot-tests.spec.ts-snapshots/*.png
trace.zip
/public/kcl-samples.zip
/public/kcl-samples/.github

118
Makefile
View File

@ -1,12 +1,111 @@
.PHONY: dev
.PHONY: all
all: install build check
KCL_WASM_LIB_FILES := $(wildcard rust/**/*.rs)
TS_SRC := $(wildcard src/**/*.tsx) $(wildcard src/**/*.ts)
XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
###############################################################################
# INSTALL
dev: node_modules public/kcl_wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
WASM_PACK ?= ~/.cargo/bin/wasm-pack
.PHONY: install
install: node_modules/.yarn-integrity $(WASM_PACK) ## Install dependencies
node_modules/.yarn-integrity: package.json yarn.lock
yarn install
@ touch $@
$(WASM_PACK):
yarn install:rust
yarn install:wasm-pack:sh
###############################################################################
# BUILD
RUST_SOURCES := $(wildcard rust/*) $(wildcard rust/**/*)
TYPESCRIPT_SOURCES := $(wildcard src/**/*.tsx) $(wildcard src/**/*.ts)
.PHONY: build
build: build-web build-desktop
.PHONY: build-web
build-web: public/kcl_wasm_lib_bg.wasm build/index.html
.PHONY: build-desktop
build-desktop: public/kcl_wasm_lib_bg.wasm .vite/build/main.js
public/kcl_wasm_lib_bg.wasm: $(RUST_SOURCES)
yarn build:wasm
build/index.html: $(TYPESCRIPT_SOURCES)
yarn build:local
.vite/build/main.js: $(TYPESCRIPT_SOURCES)
yarn tronb:vite:dev
###############################################################################
# CHECK
.PHONY: check
check: format lint
.PHONY: format
format: install ## Format the code
yarn fmt
.PHONY: lint
lint: install ## Lint the code
yarn tsc
yarn lint
###############################################################################
# RUN
.PHONY: run
run: run-web
.PHONY: run-web
run-web: install build-web ## Start the web app
yarn start
.PHONY: run-desktop
run-desktop: install build-desktop ## Start the desktop app
yarn tron:start
###############################################################################
# TEST
GREP ?= ""
.PHONY: test
test: test-unit test-e2e
.PHONY: test-unit
test-unit: install ## Run the unit tests
@ nc -z localhost 3000 || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 )
yarn test:unit
.PHONY: test-e2e
test-e2e: install build-desktop ## Run the e2e tests
yarn test:playwright:electron --workers=1 --grep=$(GREP)
###############################################################################
# CLEAN
.PHONY: clean
clean: ## Delete all artifacts
rm -rf .vite/ build/
rm -rf trace.zip playwright-report/ test-results/
rm -rf public/kcl_wasm_lib_bg.wasm
rm -rf rust/*/bindings/ rust/*/pkg/ rust/target/
rm -rf node_modules/ rust/*/node_modules/
.PHONY: help
help: install
@ grep -E '^[^[:space:]]+:.*## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help
###############################################################################
# I'm sorry this is so specific to my setup you may as well ignore this.
# This is so you don't have to deal with electron windows popping up constantly.
# It should work for you other Linux users.
@ -14,12 +113,3 @@ lee-electron-test:
Xephyr -br -ac -noreset -screen 1200x500 :2 &
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
killall Xephyr
$(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)'
public/kcl_wasm_lib_bg.wasm: $(KCL_WASM_LIB_FILES)
yarn build:wasm
node_modules: package.json yarn.lock
yarn install

View File

@ -22,8 +22,12 @@ layout: manual
* [`string`](kcl/types/string)
* [`tag`](kcl/types/tag)
* **std**
* [`Face`](kcl/types/Face)
* [`HALF_TURN`](kcl/consts/std-HALF_TURN)
* [`Helix`](kcl/types/Helix)
* [`Plane`](kcl/types/Plane)
* [`Point2d`](kcl/types/Point2d)
* [`Point3d`](kcl/types/Point3d)
* [`QUARTER_TURN`](kcl/consts/std-QUARTER_TURN)
* [`Sketch`](kcl/types/Sketch)
* [`Solid`](kcl/types/Solid)

View File

@ -1,28 +1,12 @@
---
title: "Face"
title: "std::Face"
excerpt: "A face."
layout: manual
---
A face.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |[`string`](/docs/kcl/types/string)| The id of the face. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |[`string`](/docs/kcl/types/string)| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |

View File

@ -1,26 +1,12 @@
---
title: "Helix"
title: "std::Helix"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |[`string`](/docs/kcl/types/string)| The id of the helix. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `revolutions` |[`number`](/docs/kcl/types/number)| Number of revolutions. | No |
| `angleStart` |[`number`](/docs/kcl/types/number)| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |

View File

@ -188,7 +188,7 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
| `value` |[`Face`](/docs/kcl/types/Face)| A face. | No |
| `value` |[`Face`](/docs/kcl/types/Face)| | No |
----
@ -236,7 +236,7 @@ Any KCL value.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |[`Helix`](/docs/kcl/types/Helix)| A helix. | No |
| `value` |[`Helix`](/docs/kcl/types/Helix)| | No |
----

17
docs/kcl/types/Point2d.md Normal file
View File

@ -0,0 +1,17 @@
---
title: "std::Point2d"
excerpt: "A point in two dimensional space."
layout: manual
---
A point in two dimensional space.
```kcl
type Point2d = [number; 2]
```
`Point2d` is an alias for a two-element array of [number](/docs/kcl/types/number)s. To write a value
with type `Point2d`, use an array, e.g., `[0, 0]` or `[5.0, 3.14]`.

View File

@ -1,22 +1,17 @@
---
title: "Point3d"
excerpt: ""
title: "std::Point3d"
excerpt: "A point in three dimensional space."
layout: manual
---
A point in three dimensional space.
**Type:** `object`
```kcl
type Point3d = [number; 3]
```
`Point3d` is an alias for a three-element array of [number](/docs/kcl/types/number)s. To write a value
with type `Point3d`, use an array, e.g., `[0, 0, 0]` or `[5.0, 3.14, 6.8]`.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `x` |[`number`](/docs/kcl/types/number)| | No |
| `y` |[`number`](/docs/kcl/types/number)| | No |
| `z` |[`number`](/docs/kcl/types/number)| | No |

View File

@ -22,7 +22,6 @@ A path to sweep along.
----
A helix.
[`Helix`](/docs/kcl/types/Helix)

View File

@ -5,6 +5,7 @@ import {
TEST_COLORS,
commonPoints,
PERSIST_MODELING_CONTEXT,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { HomePageFixture } from './fixtures/homePageFixture'
@ -153,7 +154,8 @@ async function doBasicSketch(
}
test.describe('Basic sketch', { tag: ['@skipWin'] }, () => {
test.fixme('code pane open at start', async ({ page, homePage }) => {
test('code pane open at start', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await doBasicSketch(page, homePage, ['code'])
})

View File

@ -1,6 +1,9 @@
import { test, expect } from './zoo-test'
import { getUtils, executorInputPath } from './test-utils'
import {
orRunWhenFullSuiteEnabled,
getUtils,
executorInputPath,
} from './test-utils'
import { join } from 'path'
import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
@ -46,11 +49,12 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await expect(codePaneButtonHolder).toContainText('notification')
})
test.skip('Opening and closing the code pane will consistently show error diagnostics', async ({
test('Opening and closing the code pane will consistently show error diagnostics', async ({
page,
homePage,
editor,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
// Load the app with the working starter code
@ -119,45 +123,47 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await expect(page.locator('.cm-tooltip').first()).toBeVisible()
})
test.fixme(
'When error is not in view you can click the badge to scroll to it',
async ({ page, homePage, context }) => {
// Load the app with the working starter code
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
test('When error is not in view you can click the badge to scroll to it', async ({
page,
homePage,
context,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
// Load the app with the working starter code
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(1000)
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]'
)
.first()
).toBeVisible()
}
)
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]'
)
.first()
).toBeVisible()
})
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
context,

View File

@ -1,6 +1,10 @@
import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises'
import { executorInputPath, getUtils } from './test-utils'
import {
executorInputPath,
getUtils,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path, { join } from 'path'
@ -47,7 +51,8 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
})
// TODO: fix this test after the electron migration
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
test('Fillet from command bar', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -488,7 +493,7 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
})
})
test(`Can add a named parameter or constant`, async ({
test(`Can add and edit a named parameter or constant`, async ({
page,
homePage,
context,
@ -510,7 +515,7 @@ c = 3 + a`
// but you do because all modeling commands have that requirement
await scene.settled(cmdBar)
await test.step(`Go through the command palette flow`, async () => {
await test.step(`Create a parameter via command bar`, async () => {
await cmdBar.cmdBarOpenBtn.click()
await cmdBar.chooseCommand('create parameter')
await cmdBar.expectState({
@ -535,5 +540,57 @@ c = 3 + a`
await editor.expectEditor.toContain(
`a = 5b = a * amyParameter001 = b - 5c = 3 + a`
)
const newValue = `2 * b + a`
await test.step(`Edit the parameter via command bar`, async () => {
await cmdBar.cmdBarOpenBtn.click()
await cmdBar.chooseCommand('edit parameter')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Edit parameter',
currentArgKey: 'Name',
currentArgValue: '',
headerArguments: {
Name: '',
Value: '',
},
highlightedHeaderArg: 'Name',
})
await cmdBar
.selectOption({
name: 'myParameter001',
})
.click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Edit parameter',
currentArgKey: 'value',
currentArgValue: 'b - 5',
headerArguments: {
Name: 'myParameter001',
Value: '',
},
highlightedHeaderArg: 'value',
})
await cmdBar.argumentInput.locator('[contenteditable]').fill(newValue)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
commandName: 'Edit parameter',
headerArguments: {
Name: 'myParameter001',
// KCL inputs show the *computed* value, not the input value, in the command palette header
Value: '55',
},
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'commandBarClosed',
})
})
await editor.expectEditor.toContain(
`a = 5b = a * amyParameter001 = ${newValue}c = 3 + a`
)
})
})

View File

@ -2,10 +2,10 @@ import { test, expect } from './zoo-test'
import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils'
import {
darkModeBgColor,
darkModePlaneColorXZ,
executorInputPath,
getUtils,
orRunWhenFullSuiteEnabled,
TEST_COLORS,
} from './test-utils'
import { join } from 'path'
@ -635,14 +635,16 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
test.fixme(
'error with 2 source ranges gets 2 diagnostics',
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`length = .750
test('error with 2 source ranges gets 2 diagnostics', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`length = .750
width = 0.500
height = 0.500
dia = 4
@ -657,53 +659,52 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
return squareHoleSketch
}
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click on the bottom of the code editor to add a new line
await u.codeLocator.click()
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`extrusion = startSketchOn('XY')
// Click on the bottom of the code editor to add a new line
await u.codeLocator.click()
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`extrusion = startSketchOn('XY')
|> circle(center: [0, 0], radius: dia/2)
|> hole(squareHole(length, width, height), %)
|> extrude(length = height)`)
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child')
await expect(
page.getByText('Expected 2 arguments, got 3').first()
).toBeVisible()
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child')
await expect(
page.getByText('Expected 2 arguments, got 3').first()
).toBeVisible()
// Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
}
)
// Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
})
test('if your kcl gets an error from the engine it is inlined', async ({
context,
page,
@ -1121,10 +1122,11 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
}
)
test.fixme(
test(
`Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' },
async ({ page, context }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'cube')
await fsp.mkdir(bracketDir, { recursive: true })
@ -1161,7 +1163,8 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await u.waitForPageLoad()
await expect
.poll(
async () => locationToHavColor(notTheOrigin, darkModePlaneColorXZ),
async () =>
locationToHavColor(notTheOrigin, TEST_COLORS.DARK_MODE_PLANE_XZ),
{
timeout: 5000,
message: 'XZ plane color is visible',
@ -1182,16 +1185,23 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await test.step(`Verify that we see the imported geometry and no errors`, async () => {
await expect(errorIndicators).toHaveCount(0)
await expect
.poll(async () => locationToHavColor(origin, darkModePlaneColorXZ), {
timeout: 3000,
message: 'Plane color should not be visible',
})
.poll(
async () =>
locationToHavColor(origin, TEST_COLORS.DARK_MODE_PLANE_XZ),
{
timeout: 3000,
message: 'Plane color should not be visible',
}
)
.toBeGreaterThan(15)
await expect
.poll(async () => locationToHavColor(origin, darkModeBgColor), {
timeout: 3000,
message: 'Background color should not be visible',
})
.poll(
async () => locationToHavColor(origin, TEST_COLORS.DARK_MODE_BKGD),
{
timeout: 3000,
message: 'Background color should not be visible',
}
)
.toBeGreaterThan(15)
})
}

View File

@ -1,7 +1,12 @@
import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import { createProject, executorInputPath, getUtils } from './test-utils'
import {
createProject,
executorInputPath,
getUtils,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
@ -266,12 +271,13 @@ test.describe('when using the file tree to', () => {
}
)
test.fixme(
test(
'loading small file, then large, then back to small',
{
tag: '@electron',
},
async ({ page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const {
panesOpen,
pasteCodeInEditor,

View File

@ -1,4 +1,4 @@
import type { Page, Locator } from '@playwright/test'
import { type Page, type Locator, test } from '@playwright/test'
import { expect } from '../zoo-test'
import {
checkIfPaneIsOpen,
@ -76,10 +76,6 @@ export class ToolbarFixture {
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
}
get editSketchBtn() {
return this.page.locator('[name="Edit Sketch"]')
}
get logoLink() {
return this.page.getByTestId('app-logo')
}
@ -115,11 +111,20 @@ export class ToolbarFixture {
).not.toBeDisabled()
}
editSketch = async () => {
await this.editSketchBtn.first().click()
// One of the rare times we want to allow a arbitrary wait
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
editSketch = async (operationIndex = 0) => {
await test.step(`Editing sketch`, async () => {
await this.openFeatureTreePane()
const operation = await this.getFeatureTreeOperation(
'Sketch',
operationIndex
)
await operation.dblclick()
// One of the rare times we want to allow a arbitrary wait
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
await expect(this.exitSketchBtn).toBeEnabled()
await this.closeFeatureTreePane()
})
}
private _getMode = () =>
this.page.locator('[data-current-mode]').getAttribute('data-current-mode')

View File

@ -6,6 +6,7 @@ import {
executorInputPath,
createProject,
settingsToToml,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths'
@ -319,237 +320,240 @@ test.describe('Onboarding tests', () => {
// (lee) The two avatar tests are weird because even on main, we don't have
// anything to do with the avatar inside the onboarding test. Due to the
// low impact of an avatar not showing I'm changing this to fixme.
test.fixme(
'Avatar text updates depending on image load success',
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('your avatar')
// This is to force the avatar to 404.
// For our test image (only triggers locally. on CI, it's Kurt's /
// gravatar image )
await page.route('/cat.jpg', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
})
// 404 the CI avatar image
await page.route(
'https://lh3.googleusercontent.com/**',
async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
}
)
await page.reload({ waitUntil: 'domcontentloaded' })
// Now expect the text to be different
await expect(avatarLocator).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
}
)
test.fixme(
"Avatar text doesn't mention avatar when no avatar",
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
}
}
)
})
test.fixme(
'Restarting onboarding on desktop takes one attempt',
async ({ context, page, tronApp }) => {
test('Avatar text updates depending on image load success', async ({
context,
page,
homePage,
tronApp,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: 'dismissed',
onboarding_status: '',
},
})
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl')
)
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
// Our constants
const u = await getUtils(page)
const projectCard = page.getByText('router-template-slate')
const helpMenuButton = page.getByRole('button', {
name: 'Help and resources',
})
const restartOnboardingButton = page.getByRole('button', {
name: 'Reset onboarding',
})
const nextButton = page.getByTestId('onboarding-next')
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Modeling App!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle')
const userMenuSettingsButton = page.getByRole('button', {
name: 'User settings',
})
const settingsHeading = page.getByRole('heading', {
name: 'Settings',
exact: true,
})
const restartOnboardingSettingsButton = page.getByRole('button', {
name: 'Replay onboarding',
})
// Test that the text in this step is correct
const avatarLocator = page.getByTestId('user-sidebar-toggle').locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
await test.step('Navigate into project', async () => {
await expect(
page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible()
await expect(projectCard).toBeVisible()
await projectCard.click()
await u.waitForPageLoad()
})
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('your avatar')
await test.step('Restart the onboarding from help menu', async () => {
await helpMenuButton.click()
await restartOnboardingButton.click()
await nextButton.hover()
await nextButton.click()
})
await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('// Shelf Bracket')
await u.codeLocator.selectText()
await u.codeLocator.fill('')
await test.step('Navigate to settings', async () => {
await userMenuButton.click()
await userMenuSettingsButton.click()
await expect(settingsHeading).toBeVisible()
await expect(restartOnboardingSettingsButton).toBeVisible()
// This is to force the avatar to 404.
// For our test image (only triggers locally. on CI, it's Kurt's /
// gravatar image )
await page.route('/cat.jpg', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
await restartOnboardingSettingsButton.click()
// Since the code is empty, we should not see the confirmation dialog
await expect(nextButton).not.toBeVisible()
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
})
// 404 the CI avatar image
await page.route('https://lh3.googleusercontent.com/**', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
})
await page.reload({ waitUntil: 'domcontentloaded' })
// Now expect the text to be different
await expect(avatarLocator).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
})
test("Avatar text doesn't mention avatar when no avatar", async ({
context,
page,
homePage,
tronApp,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
}
})
})
test('Restarting onboarding on desktop takes one attempt', async ({
context,
page,
tronApp,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: 'dismissed',
},
})
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl')
)
})
// Our constants
const u = await getUtils(page)
const projectCard = page.getByText('router-template-slate')
const helpMenuButton = page.getByRole('button', {
name: 'Help and resources',
})
const restartOnboardingButton = page.getByRole('button', {
name: 'Reset onboarding',
})
const nextButton = page.getByTestId('onboarding-next')
const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Modeling App!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle')
const userMenuSettingsButton = page.getByRole('button', {
name: 'User settings',
})
const settingsHeading = page.getByRole('heading', {
name: 'Settings',
exact: true,
})
const restartOnboardingSettingsButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await test.step('Navigate into project', async () => {
await expect(
page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible()
await expect(projectCard).toBeVisible()
await projectCard.click()
await u.waitForPageLoad()
})
await test.step('Restart the onboarding from help menu', async () => {
await helpMenuButton.click()
await restartOnboardingButton.click()
await nextButton.hover()
await nextButton.click()
})
await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
})
await test.step('Clear code and restart onboarding from settings', async () => {
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('// Shelf Bracket')
await u.codeLocator.selectText()
await u.codeLocator.fill('')
await test.step('Navigate to settings', async () => {
await userMenuButton.click()
await userMenuSettingsButton.click()
await expect(settingsHeading).toBeVisible()
await expect(restartOnboardingSettingsButton).toBeVisible()
})
await restartOnboardingSettingsButton.click()
// Since the code is empty, we should not see the confirmation dialog
await expect(nextButton).not.toBeVisible()
await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible()
})
})

View File

@ -5,7 +5,7 @@ import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
import fs from 'node:fs/promises'
import path from 'node:path'
import { getUtils } from './test-utils'
import { getUtils, orRunWhenFullSuiteEnabled } from './test-utils'
import { Locator } from '@playwright/test'
// test file is for testing point an click code gen functionality that's not sketch mode related
@ -850,157 +850,160 @@ openSketch = startSketchOn('XY')
})
})
test.fixme(
`Shift-click to select and deselect sketch segments`,
async ({ page, homePage, scene, editor }) => {
// Locators
const firstPointLocation = { x: 200, y: 100 }
const secondPointLocation = { x: 800, y: 100 }
const thirdPointLocation = { x: 800, y: 400 }
const fristSegmentLocation = { x: 750, y: 100 }
const secondSegmentLocation = { x: 800, y: 150 }
const planeLocation = { x: 700, y: 200 }
test(`Shift-click to select and deselect sketch segments`, async ({
page,
homePage,
scene,
editor,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
// Locators
const firstPointLocation = { x: 200, y: 100 }
const secondPointLocation = { x: 800, y: 100 }
const thirdPointLocation = { x: 800, y: 400 }
const fristSegmentLocation = { x: 750, y: 100 }
const secondSegmentLocation = { x: 800, y: 150 }
const planeLocation = { x: 700, y: 200 }
// Click helpers
const [clickFirstPoint] = scene.makeMouseHelpers(
firstPointLocation.x,
firstPointLocation.y
)
const [clickSecondPoint] = scene.makeMouseHelpers(
secondPointLocation.x,
secondPointLocation.y
)
const [clickThirdPoint] = scene.makeMouseHelpers(
thirdPointLocation.x,
thirdPointLocation.y
)
const [clickFirstSegment] = scene.makeMouseHelpers(
fristSegmentLocation.x,
fristSegmentLocation.y
)
const [clickSecondSegment] = scene.makeMouseHelpers(
secondSegmentLocation.x,
secondSegmentLocation.y
)
const [clickPlane] = scene.makeMouseHelpers(
planeLocation.x,
planeLocation.y
)
// Click helpers
const [clickFirstPoint] = scene.makeMouseHelpers(
firstPointLocation.x,
firstPointLocation.y
)
const [clickSecondPoint] = scene.makeMouseHelpers(
secondPointLocation.x,
secondPointLocation.y
)
const [clickThirdPoint] = scene.makeMouseHelpers(
thirdPointLocation.x,
thirdPointLocation.y
)
const [clickFirstSegment] = scene.makeMouseHelpers(
fristSegmentLocation.x,
fristSegmentLocation.y
)
const [clickSecondSegment] = scene.makeMouseHelpers(
secondSegmentLocation.x,
secondSegmentLocation.y
)
const [clickPlane] = scene.makeMouseHelpers(
planeLocation.x,
planeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220]
const edgeColorBlue: [number, number, number] = [20, 20, 200]
const backgroundColor: [number, number, number] = [30, 30, 30]
const tolerance = 40
const timeout = 150
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220]
const edgeColorBlue: [number, number, number] = [20, 20, 200]
const backgroundColor: [number, number, number] = [30, 30, 30]
const tolerance = 40
const timeout = 150
// Setup
await test.step(`Initial test setup`, async () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Setup
await test.step(`Initial test setup`, async () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Wait for the scene and stream to load
// Wait for the scene and stream to load
await scene.expectPixelColor(
backgroundColor,
secondPointLocation,
tolerance
)
})
await test.step('Select and deselect a single sketch segment', async () => {
await test.step('Get into sketch mode', async () => {
await editor.closePane()
await page.waitForTimeout(timeout)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(timeout)
await clickPlane()
await page.waitForTimeout(1000)
})
await test.step('Draw sketch', async () => {
await clickFirstPoint()
await page.waitForTimeout(timeout)
await clickSecondPoint()
await page.waitForTimeout(timeout)
await clickThirdPoint()
await page.waitForTimeout(timeout)
})
await test.step('Deselect line tool', async () => {
const btnLine = page.getByTestId('line')
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
if (btnLineAriaPressed === 'true') {
await btnLine.click()
}
await page.waitForTimeout(timeout)
})
await test.step('Select the first segment', async () => {
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await scene.expectPixelColor(
backgroundColor,
secondPointLocation,
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
await test.step('Select and deselect a single sketch segment', async () => {
await test.step('Get into sketch mode', async () => {
await editor.closePane()
await page.waitForTimeout(timeout)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(timeout)
await clickPlane()
await page.waitForTimeout(1000)
})
await test.step('Draw sketch', async () => {
await clickFirstPoint()
await page.waitForTimeout(timeout)
await clickSecondPoint()
await page.waitForTimeout(timeout)
await clickThirdPoint()
await page.waitForTimeout(timeout)
})
await test.step('Deselect line tool', async () => {
const btnLine = page.getByTestId('line')
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
if (btnLineAriaPressed === 'true') {
await btnLine.click()
}
await page.waitForTimeout(timeout)
})
await test.step('Select the first segment', async () => {
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
await test.step('Select the second segment (Shift-click)', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the first segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the second segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
await test.step('Select the second segment (Shift-click)', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
}
)
await test.step('Deselect the first segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the second segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
})
})
test(`Offset plane point-and-click`, async ({
context,
@ -1333,7 +1336,7 @@ loft001 = loft([sketch001, sketch002])
})
})
test(`Sweep point-and-click`, async ({
test(`Sweep point-and-click base`, async ({
context,
page,
homePage,
@ -1366,7 +1369,10 @@ sketch002 = startSketchOn('XZ')
testPoint.x - 50,
testPoint.y
)
const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
const sweepDeclaration =
'sweep001 = sweep(sketch001, path = sketch002, sectional = false)'
const editedSweepDeclaration =
'sweep001 = sweep(sketch001, path = sketch002, sectional = true)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
@ -1380,6 +1386,7 @@ sketch002 = startSketchOn('XZ')
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Sectional: '',
Target: '',
Trajectory: '',
},
@ -1392,6 +1399,7 @@ sketch002 = startSketchOn('XZ')
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Sectional: '',
Target: '1 face',
Trajectory: '',
},
@ -1401,18 +1409,50 @@ sketch002 = startSketchOn('XZ')
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await toolbar.openPane('code')
await page.waitForTimeout(500)
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Target: '1 face',
Trajectory: '1 segment',
Sectional: '',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
// await scene.expectPixelColor([135, 64, 73], testPoint, 15) // FIXME
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [sweepDeclaration],
highlightedCode: '',
await toolbar.closePane('code')
})
await test.step('Edit sweep via feature tree selection works', async () => {
await toolbar.openPane('feature-tree')
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
await operationButton.dblclick({ button: 'left' })
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'sectional',
currentArgValue: '',
headerArguments: {
Sectional: '',
},
highlightedHeaderArg: 'sectional',
stage: 'arguments',
})
await cmdBar.selectOption({ name: 'True' }).click()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Sectional: '',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(editedSweepDeclaration)
await toolbar.closePane('code')
})
@ -1473,6 +1513,7 @@ sketch002 = startSketchOn('XZ')
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Sectional: '',
Target: '',
Trajectory: '',
},
@ -1485,6 +1526,7 @@ sketch002 = startSketchOn('XZ')
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Sectional: '',
Target: '1 face',
Trajectory: '',
},

View File

@ -7,6 +7,7 @@ import {
Paths,
createProject,
getPlaywrightDownloadDir,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import fsp from 'fs/promises'
import fs from 'fs'
@ -1244,10 +1245,11 @@ test(
}
)
test.fixme(
test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const projectData = [
['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
@ -1466,10 +1468,11 @@ test(
}
)
test.fixme(
test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2050,10 +2053,11 @@ test(
)
// Flaky
test.fixme(
test(
'Original project name persist after onboarding',
{ tag: '@electron' },
async ({ page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
await page.setBodyDimensions({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all()

View File

@ -1,4 +1,5 @@
import { test, expect } from './zoo-test'
import { orRunWhenFullSuiteEnabled } from './test-utils'
/* eslint-disable jest/no-conditional-expect */
@ -196,60 +197,65 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
})
})
test.fixme(
`manual code selection rename`,
async ({ context, homePage, cmdBar, editor, page, scene }) => {
const body1CapCoords = { x: 571, y: 311 }
test(`manual code selection rename`, async ({
context,
homePage,
cmdBar,
editor,
page,
scene,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const body1CapCoords = { x: 571, y: 311 }
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
await test.step('wait for scene to load and select code in editor', async () => {
// Find and select the text "sketch002" in the editor
await editor.selectText('sketch002')
await test.step('wait for scene to load and select code in editor', async () => {
// Find and select the text "sketch002" in the editor
await editor.selectText('sketch002')
// Verify the selection was made
await editor.expectState({
highlightedCode: '',
activeLines: ["sketch002 = startSketchOn('XZ')"],
diagnostics: [],
})
// Verify the selection was made
await editor.expectState({
highlightedCode: '',
activeLines: ["sketch002 = startSketchOn('XZ')"],
diagnostics: [],
})
})
await test.step('fire off edit prompt', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await cmdBar.openCmdBar('promptToEdit')
await page
.getByTestId('cmd-bar-arg-value')
.fill('Please rename to mySketch001')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
})
await expect(successToast).toBeVisible()
await test.step('fire off edit prompt', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await cmdBar.openCmdBar('promptToEdit')
await page
.getByTestId('cmd-bar-arg-value')
.fill('Please rename to mySketch001')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
})
await expect(successToast).toBeVisible()
})
await test.step('verify rename change and accept it', async () => {
await editor.expectEditor.toContain('mySketch001 = startSketchOn')
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
await editor.expectEditor.toContain(
'extrude002 = extrude(mySketch001, length = 50)'
)
await test.step('verify rename change and accept it', async () => {
await editor.expectEditor.toContain('mySketch001 = startSketchOn')
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
await editor.expectEditor.toContain(
'extrude002 = extrude(mySketch001, length = 50)'
)
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
})
}
)
await acceptBtn.click()
await expect(successToast).not.toBeVisible()
})
})
test('multiple body selections', async ({
context,

View File

@ -2,9 +2,16 @@ import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import path from 'path'
import * as fsp from 'fs/promises'
import { getUtils, executorInputPath } from './test-utils'
import {
getUtils,
executorInputPath,
TEST_COLORS,
TestColor,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl'
import { reportRejection } from 'lib/trap'
test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
// bugs we found that don't fit neatly into other categories
@ -315,6 +322,89 @@ extrude001 = extrude(sketch001, length = 50)
}
)
test(
'window resize updates should reconfigure the stream',
{ tag: '@skipLocalEngine' },
async ({ scene, page, homePage, cmdBar, toolbar }) => {
await page.addInitScript(
async ({ code }) => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = mm)
sketch002 = startSketchOn('XY')
profile002 = startProfileAt([72.24, -52.05], sketch002)
|> angledLine([0, 181.26], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
21.54
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude002 = extrude(profile002, length = 150)
`
)
;(window as any).playwrightSkipFilePicker = true
},
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
)
const websocketPromise = page.waitForEvent('websocket')
await page.setBodyDimensions({ width: 500, height: 500 })
await homePage.goToModelingScene()
const websocket = await websocketPromise
await scene.connectionEstablished()
await scene.settled(cmdBar)
await toolbar.closePane('code')
// expect pixel color to be background color
const offModelBefore = { x: 446, y: 250 }
const onModelBefore = { x: 422, y: 250 }
const offModelAfter = { x: 692, y: 262 }
const onModelAfter = { x: 673, y: 266 }
await scene.expectPixelColor(
TEST_COLORS.DARK_MODE_BKGD,
offModelBefore,
15
)
const standardModelGrey: TestColor = [100, 100, 100]
await scene.expectPixelColor(standardModelGrey, onModelBefore, 15)
await test.step('resize window and expect "reconfigure_stream" websocket message', async () => {
// note this is a bit low level for our tests, usually this would go into a fixture
// but it's pretty unique to this resize test, it can be moved/abstracted if we have further need
// to listen to websocket messages
await Promise.all([
new Promise((resolve) => {
websocket
// @ts-ignore
.waitForEvent('framesent', (frame) => {
frame.payload
.toString()
.includes(
'"type":"reconfigure_stream","width":1000,"height":500'
) && resolve(true)
})
.catch(reportRejection)
}),
page.setBodyDimensions({ width: 1000, height: 500 }),
])
})
await scene.expectPixelColor(
TEST_COLORS.DARK_MODE_BKGD,
offModelAfter,
15
)
await scene.expectPixelColor(standardModelGrey, onModelAfter, 15)
}
)
test(
'when engine fails export we handle the failure and alert the user',
{ tag: '@skipLocalEngine' },
@ -483,10 +573,11 @@ extrude001 = extrude(sketch001, length = 50)
}
)
test.fixme(
test(
`Network health indicator only appears in modeling view`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
@ -533,7 +624,7 @@ extrude001 = extrude(sketch001, length = 50)
// Constants and locators
const planeColor: [number, number, number] = [170, 220, 170]
const bgColor: [number, number, number] = [27, 27, 27]
const bgColor: [number, number, number] = TEST_COLORS.DARK_MODE_BKGD
const middlePixelIsColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff({ x: 600, y: 250 }, color)
}

View File

@ -9,9 +9,11 @@ import {
getUtils,
PERSIST_MODELING_CONTEXT,
TEST_COLORS,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
@ -187,12 +189,14 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})
test.fixme('Can edit segments by dragging their handles', () => {
test('Can edit segments by dragging their handles', () => {
test.fixme(orRunWhenFullSuiteEnabled())
const doEditSegmentsByDraggingHandle = async (
page: Page,
homePage: HomePageFixture,
openPanes: string[],
scene: SceneFixture,
toolbar: ToolbarFixture,
cmdBar: CmdBarFixture
) => {
// Load the app with the code panes
@ -282,11 +286,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
// Select the sketch
await page.mouse.click(700, 370)
}
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await toolbar.editSketch()
if (openPanes.includes('code')) {
prevContent = await page.locator('.cm-content').innerText()
}
@ -417,7 +417,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene, cmdBar }) => {
async ({ page, homePage, scene, toolbar, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -435,6 +435,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
homePage,
['code'],
scene,
toolbar,
cmdBar
)
}
@ -443,7 +444,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene, cmdBar }) => {
async ({ page, homePage, scene, toolbar, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -451,7 +452,14 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [], scene, cmdBar)
await doEditSegmentsByDraggingHandle(
page,
homePage,
[],
scene,
toolbar,
cmdBar
)
}
)
})
@ -1080,107 +1088,108 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
)
})
// TODO: fix after electron migration is merged
test.fixme(
'empty-scene default-planes act as expected',
async ({ page, homePage }) => {
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
test('empty-scene default-planes act as expected', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
const u = await getUtils(page)
await homePage.goToModelingScene()
const u = await getUtils(page)
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
await u.openAndClearDebugPanel()
await u.codeLocator.fill(`sketch001 = startSketchOn('XY')
await u.codeLocator.fill(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line(end = [20, 0])
|> line(end = [0, 20])
|> xLine(length = -20)
`)
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.expectCmdLog('[data-message-type="execution-done"]')
const noPlanesColor: [number, number, number] = [30, 30, 30]
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
const noPlanesColor: [number, number, number] = [30, 30, 30]
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
await u.clearCommandLogs()
await u.removeCurrentCode()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
await u.expectCmdLog('[data-message-type="execution-done"]')
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), {
timeout: 5_000,
})
.toBeLessThan(8)
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), {
timeout: 5_000,
})
.toBeLessThan(8)
// click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 })
const hoveredColor: [number, number, number] = [93, 93, 127]
// now that we're expecting the user to select a plan, it does respond to hover
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor))
.toBeLessThan(8)
// click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 })
const hoveredColor: [number, number, number] = [93, 93, 127]
// now that we're expecting the user to select a plan, it does respond to hover
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor))
.toBeLessThan(8)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(600)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(600)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line(end = [3.39, -3.39])
`)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line(end = [3.39, -3.39])
`
)
})
)
})
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// expect there to be no planes on load since there's something in the scene
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
}
)
// expect there to be no planes on load since there's something in the scene
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
})
test('Can attempt to sketch on revolved face', async ({ page, homePage }) => {
const u = await getUtils(page)
@ -1454,10 +1463,11 @@ test.describe(`Sketching with offset planes`, () => {
})
test.describe('multi-profile sketching', () => {
test.fixme(
test(
`test it removes half-finished expressions when changing tools in sketch mode`,
{ tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
@ -2545,7 +2555,7 @@ profile002 = startProfileAt([85.81, 52.55], sketch002)
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
const [nextPoint] = scene.makeMouseHelpers(763, 130)
await page.getByText('startProfileAt([85.81, 52.55], sketch002)').click()
await toolbar.editSketch()
await toolbar.editSketch(1)
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
@ -2765,7 +2775,7 @@ extrude003 = extrude(profile011, length = 2.5)
await test.step(title, async () => {
await camPositionForSelectingSketchOnWallProfiles()
await selectClick()
await toolbar.editSketch()
await toolbar.editSketch(1)
await page.waitForTimeout(600)
await verifyWallProfilesAreDrawn()
await toolbar.exitSketchBtn.click()
@ -2846,13 +2856,18 @@ loft([profile001, profile002])
)
}
)
test.fixme(
'Can enter sketch loft edges offsetPlane and continue sketch',
async ({ scene, toolbar, editor, page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
test('Can enter sketch loft edges offsetPlane and continue sketch', async ({
scene,
toolbar,
editor,
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([34, 42.66], sketch001)
|> line(end = [102.65, 151.99])
|> line(end = [76, -138.66])
@ -2868,51 +2883,50 @@ profile002 = startProfileAt([39.43, 172.21], sketch002)
loft([profile001, profile002])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const topProfileEdgeClickCoords = { x: 602, y: 185 } as const
const [topProfileEdgeClick] = scene.makeMouseHelpers(
topProfileEdgeClickCoords.x,
topProfileEdgeClickCoords.y
)
const [sideProfileEdgeClick] = scene.makeMouseHelpers(788, 188)
})
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await scene.moveCameraTo(
{ x: 8171, y: -7740, z: 1624 },
{ x: 3302, y: -627, z: 2892 }
)
const topProfileEdgeClickCoords = { x: 602, y: 185 } as const
const [topProfileEdgeClick] = scene.makeMouseHelpers(
topProfileEdgeClickCoords.x,
topProfileEdgeClickCoords.y
)
const [sideProfileEdgeClick] = scene.makeMouseHelpers(788, 188)
await topProfileEdgeClick()
await page.waitForTimeout(300)
await toolbar.editSketch()
await page.waitForTimeout(600)
await sideProfileEdgeClick()
await page.waitForTimeout(300)
await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15)
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfileAt([47.76, -17.13], plane001)`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine([0, 106.42], %, $rectangleSegmentA001)`
)
await page.waitForTimeout(100)
}
)
await scene.moveCameraTo(
{ x: 8171, y: -7740, z: 1624 },
{ x: 3302, y: -627, z: 2892 }
)
await topProfileEdgeClick()
await page.waitForTimeout(300)
await toolbar.editSketch()
await page.waitForTimeout(600)
await sideProfileEdgeClick()
await page.waitForTimeout(300)
await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15)
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfileAt([47.76, -17.13], plane001)`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine([0, 106.42], %, $rectangleSegmentA001)`
)
await page.waitForTimeout(100)
})
})
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4891

View File

@ -1,6 +1,12 @@
import { test, expect } from './zoo-test'
import { secrets } from './secrets'
import { Paths, doExport, getUtils, settingsToToml } from './test-utils'
import {
Paths,
doExport,
getUtils,
settingsToToml,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { Models } from '@kittycad/lib'
import fsp from 'fs/promises'
import { spawn } from 'child_process'
@ -36,10 +42,11 @@ test.setTimeout(60_000)
// a snapshot of it feels weird. I'd rather our regular tests fail.
// The primary failure is doExport now relies on the filesystem. We can follow
// up with another PR if we want this back.
test.skip(
test(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context, scene, cmdBar, tronApp }) => {
test.fixme(orRunWhenFullSuiteEnabled())
if (!tronApp) {
fail()
}
@ -406,9 +413,6 @@ test(
'Draft segments should look right',
{ tag: '@snapshot' },
async ({ page, scene, toolbar }) => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -585,9 +589,6 @@ test(
'Draft circle should look right',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -868,6 +869,7 @@ part002 = startSketchOn(part001, seg01)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
}
)
@ -907,6 +909,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
}
)
@ -947,14 +950,12 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
}
)
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({
page,
cmdBar,
@ -1097,7 +1098,8 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
})
})
test.fixme('theme persists', async ({ page, context }) => {
test('theme persists', async ({ page, context }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
@ -1205,6 +1207,7 @@ sweepSketch = startSketchOn('XY')
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})
@ -1262,6 +1265,7 @@ sweepSketch = startSketchOn('XY')
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -1,13 +1,14 @@
import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils'
import { commonPoints, getUtils, orRunWhenFullSuiteEnabled } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.fixme('Test network and connection issues', () => {
test.describe('Test network and connection issues', () => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -83,7 +84,8 @@ test.fixme('Test network and connection issues', () => {
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
async ({ page, homePage, toolbar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)
@ -173,11 +175,7 @@ test.fixme('Test network and connection issues', () => {
.click()
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
await toolbar.editSketch()
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
@ -201,6 +199,7 @@ test.fixme('Test network and connection issues', () => {
type: 'default_camera_get_settings',
},
}
await toolbar.openPane('debug')
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)

View File

@ -31,11 +31,13 @@ const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '')
}
type TestColor = [number, number, number]
export const TEST_COLORS = {
WHITE: [249, 249, 249] as TestColor,
YELLOW: [255, 255, 0] as TestColor,
BLUE: [0, 0, 255] as TestColor,
export type TestColor = [number, number, number]
export const TEST_COLORS: { [key: string]: TestColor } = {
WHITE: [249, 249, 249],
YELLOW: [255, 255, 0],
BLUE: [0, 0, 255],
DARK_MODE_BKGD: [27, 27, 27],
DARK_MODE_PLANE_XZ: [50, 50, 99],
} as const
export const PERSIST_MODELING_CONTEXT = 'persistModelingContext'
@ -50,17 +52,13 @@ export const commonPoints = {
num3: -2.44,
} as const
/** A semi-reliable color to check the default XZ plane on
* in dark mode in the default camera position
*/
export const darkModePlaneColorXZ: [number, number, number] = [50, 50, 99]
/** A semi-reliable color to check the default dark mode bg color against */
export const darkModeBgColor: [number, number, number] = [27, 27, 27]
export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
export function orRunWhenFullSuiteEnabled() {
return process.env.GITHUB_HEAD_REF !== 'all-e2e'
}
async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
@ -938,8 +936,8 @@ function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// Fail when running on CI and FAIL_ON_CONSOLE_ERRORS is set
// use expect to prevent page from closing and not cleaning up
expect(`An error was detected in the console: \r\n message:${exception.message} \r\n name:${exception.name} \r\n stack:${exception.stack}
*Either fix the console error or add it to the whitelist defined in ./lib/console-error-whitelist.ts (if the error can be safely ignored)
*Either fix the console error or add it to the whitelist defined in ./lib/console-error-whitelist.ts (if the error can be safely ignored)
`).toEqual('Console error detected')
} else {
// the (test-results/exceptions.txt) file will be uploaded as part of an upload artifact in GH

View File

@ -1,7 +1,7 @@
import { test, expect } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { getUtils } from './test-utils'
import { getUtils, orRunWhenFullSuiteEnabled } from './test-utils'
test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
test('Can move camera reliably', async ({ page, context, homePage }) => {
@ -179,169 +179,170 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
})
// TODO: fix after electron migration is merged
test.fixme(
'Zoom should be consistent when exiting or entering sketches',
async ({ page, homePage }) => {
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
// than again for sketching
test('Zoom should be consistent when exiting or entering sketches', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
// than again for sketching
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.openDebugPanel()
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 325)
let code = `sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift')
await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' })
await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
let y = 350,
x = 948
await u.canvasLocator.click({ position: { x: 783, y } })
code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line(end = [11.18, 0])`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line(end = [0, 6.99])`
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'line Line', exact: true }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.mouse.move(700, 325)
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 325)
let code = `sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift')
await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' })
await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
let y = 350,
x = 948
await u.canvasLocator.click({ position: { x: 783, y } })
code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line(end = [11.18, 0])`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line(end = [0, 6.99])`
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'line Line', exact: true }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.mouse.move(700, 325)
await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
timeout: 10_000,
})
}
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200)
// hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over vertical line
await u.canvasLocator.hover({ position: { x, y: 325 } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// click exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await hoverOverNothing()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
timeout: 10_000,
})
}
)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200)
// hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over vertical line
await u.canvasLocator.hover({ position: { x, y: 325 } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// click exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await hoverOverNothing()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
})
test(`Zoom by scroll should not fire while orbiting`, async ({
homePage,

View File

@ -5,6 +5,7 @@ import {
TEST_COLORS,
pollEditorLinesSelectedLength,
executorInputPath,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { XOR } from 'lib/utils'
import path from 'node:path'
@ -1005,114 +1006,108 @@ part002 = startSketchOn('XZ')
}
})
test.fixme(
'Horizontally constrained line remains selected after applying constraint',
async ({ page, homePage }) => {
test.setTimeout(70_000)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
test('Horizontally constrained line remains selected after applying constraint', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
test.setTimeout(70_000)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-1.05, -1.07], %)
|> line(end = [3.79, 2.68], tag = $seg01)
|> line(end = [3.13, -2.4])`
)
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.getByText('line(end = [3.79, 2.68], tag = $seg01)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
{ timeout: 10_000 }
)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
await page.waitForTimeout(100)
const lineBefore = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.WHITE)
).toBeLessThan(3)
await page.mouse.move(lineBefore.x, lineBefore.y)
await page.waitForTimeout(50)
await page.mouse.click(lineBefore.x, lineBefore.y)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.BLUE)
).toBeLessThan(3)
await page
.getByRole('button', {
name: 'Length: open menu',
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
.click()
await page.waitForTimeout(500)
await page.getByRole('button', { name: 'Horizontal', exact: true }).click()
await page.waitForTimeout(500)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await pollEditorLinesSelectedLength(page, 1)
let activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length = 3.13)`)
await page.getByText('line(end = [3.79, 2.68], tag = $seg01)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 10_000 })
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for code editor to settle.
await page.waitForTimeout(2000)
// Wait for overlays to populate
await page.waitForTimeout(1000)
// If the overlay-angle is updated the THREE.js scene is in a good state
await expect(
await page.locator('[data-overlay-index="1"]')
).toHaveAttribute('data-overlay-angle', '0')
await page.waitForTimeout(100)
const lineBefore = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.WHITE)
).toBeLessThan(3)
await page.mouse.move(lineBefore.x, lineBefore.y)
await page.waitForTimeout(50)
await page.mouse.click(lineBefore.x, lineBefore.y)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.BLUE)
).toBeLessThan(3)
const lineAfter = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.waitForTimeout(500)
await page
.getByRole('button', { name: 'Horizontal', exact: true })
.click()
await page.waitForTimeout(500)
const linebb = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(linebb.x, linebb.y, { steps: 25 })
await page.mouse.click(linebb.x, linebb.y)
await pollEditorLinesSelectedLength(page, 1)
let activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length = 3.13)`)
await expect
.poll(async () => await u.getGreatestPixDiff(lineAfter, TEST_COLORS.BLUE))
.toBeLessThan(3)
// Wait for code editor to settle.
await page.waitForTimeout(2000)
await page.waitForTimeout(500)
// If the overlay-angle is updated the THREE.js scene is in a good state
await expect(
await page.locator('[data-overlay-index="1"]')
).toHaveAttribute('data-overlay-angle', '0')
// await expect(page.getByRole('button', { name: 'length', exact: true })).toBeVisible()
await page.waitForTimeout(200)
// await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('constraint-length').click()
const lineAfter = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
await page.getByTestId('cmd-bar-arg-value').getByRole('textbox').fill('10')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
const linebb = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(linebb.x, linebb.y, { steps: 25 })
await page.mouse.click(linebb.x, linebb.y)
await pollEditorLinesSelectedLength(page, 1)
activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(
`|> xLine(length = length001)`
)
await expect
.poll(
async () => await u.getGreatestPixDiff(lineAfter, TEST_COLORS.BLUE)
)
.toBeLessThan(3)
await page.waitForTimeout(500)
// await expect(page.getByRole('button', { name: 'length', exact: true })).toBeVisible()
await page.waitForTimeout(200)
// await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('constraint-length').click()
await page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
.fill('10')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await pollEditorLinesSelectedLength(page, 1)
activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(
`|> xLine(length = length001)`
)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
}
)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
})
})
test.describe('Electron constraint tests', () => {
test(

View File

@ -316,14 +316,11 @@ test.describe(`Testing gizmo, fixture-based`, () => {
})
await test.step(`Gizmo should be disabled when in sketch mode`, async () => {
const sketchModeButton = page.getByRole('button', {
name: 'Edit sketch',
})
const exitSketchButton = page.getByRole('button', {
name: 'Exit sketch',
})
await sketchModeButton.click()
await toolbar.editSketch()
await expect(exitSketchButton).toBeVisible()
const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings',

View File

@ -1,8 +1,9 @@
import { test, expect } from './zoo-test'
import { getUtils } from './test-utils'
import { getUtils, orRunWhenFullSuiteEnabled } from './test-utils'
test.describe('Test toggling perspective', () => {
test.fixme('via command palette and toggle', async ({ page, homePage }) => {
test('via command palette and toggle', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
// Locators and constants

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils'
import { commonPoints, getUtils, orRunWhenFullSuiteEnabled } from './test-utils'
import { Coords2d } from 'lang/std/sketch'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { uuidv4 } from 'lib/utils'
@ -10,6 +10,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test('Selections work on fresh and edited sketch', async ({
page,
homePage,
toolbar,
}) => {
// tests mapping works on fresh sketch and edited sketch
// tests using hovers which is the same as selections, because if
@ -216,12 +217,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await emptySpaceHover()
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(450) // wait for animation
await toolbar.editSketch()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -452,15 +448,20 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
})
test.fixme(
'parent Solid should be select and deletable and uses custom planes to position children',
async ({ page, homePage, scene, cmdBar, editor }) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`part001 = startSketchOn('XY')
test('parent Solid should be select and deletable and uses custom planes to position children', async ({
page,
homePage,
scene,
cmdBar,
editor,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`part001 = startSketchOn('XY')
yo = startProfileAt([4.83, 12.56], part001)
|> line(end = [15.1, 2.48])
|> line(end = [3.15, -9.85], tag = $seg01)
@ -491,35 +492,34 @@ profile001 = startProfileAt([7.49, 9.96], sketch001)
|> close()
`
)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const extrudeWall = { x: 575, y: 238 }
// DELETE with selection on face of parent
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [-15.17, -4.1])'
)
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await editor.expectEditor.not.toContain(`yoo = extrude(yo, length = 4)`, {
shouldNormalise: true,
})
await editor.expectEditor.toContain(`startSketchOn({plane={origin`, {
shouldNormalise: true,
})
await editor.snapshot()
}
)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const extrudeWall = { x: 575, y: 238 }
// DELETE with selection on face of parent
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [-15.17, -4.1])'
)
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await editor.expectEditor.not.toContain(`yoo = extrude(yo, length = 4)`, {
shouldNormalise: true,
})
await editor.expectEditor.toContain(`startSketchOn({plane={origin`, {
shouldNormalise: true,
})
await editor.snapshot()
})
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
page,
homePage,

View File

@ -6,6 +6,8 @@ import {
executorInputPath,
createProject,
tomlToSettings,
TEST_COLORS,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
@ -55,91 +57,87 @@ test.describe('Testing settings', () => {
})
// The behavior is actually broken. Parent always takes precedence
test.fixme(
'Project settings can be set and override user settings',
async ({ page, homePage }) => {
const u = await getUtils(page)
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
})
// Selectors and constants
const paneButtonLocator = page.getByTestId('debug-pane-button')
const headingLocator = page.getByRole('heading', {
name: 'Settings',
exact: true,
})
const inputLocator = page.locator('input[name="app-showDebugPanel"]')
await test.step('Open settings dialog and set "Show debug panel" to on', async () => {
await page.keyboard.press('ControlOrMeta+,')
await expect(headingLocator).toBeVisible()
/** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
await test.step(`Confirm that this dialog has a solid background`, async () => {
await expect
.poll(
() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]),
{
timeout: 1000,
message:
'Checking for solid background, should not see default plane colors',
}
)
.toBeLessThan(15)
})
await page.locator('#showDebugPanel').getByText('OffOn').click()
})
// Close it and open again with keyboard shortcut, while KCL editor is focused
// Put the cursor in the editor
await test.step('Open settings with keyboard shortcut', async () => {
await page.getByTestId('settings-close-button').click()
await page.locator('.cm-content').click()
await page.keyboard.press('ControlOrMeta+,')
await expect(headingLocator).toBeVisible()
})
// Verify the toast appeared
await expect(
page.getByText(`Set show debug panel to "false" for this project`)
).toBeVisible()
await expect(
page.getByText(`Set show debug panel to "false" for this project`)
).not.toBeVisible()
// Check that the debug panel button is gone
await expect(paneButtonLocator).not.toBeVisible()
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(inputLocator).toBeChecked()
// Roll back to default of "off"
await await page
.getByText(
'show debug panelRoll back show debug panelRoll back to match'
)
.hover()
test('Project settings can be set and override user settings', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page
.getByRole('button', {
name: 'Roll back show debug panel',
})
.click()
await expect(inputLocator).not.toBeChecked()
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
})
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(
page.locator('input[name="app-showDebugPanel"]')
).not.toBeChecked()
}
)
// Selectors and constants
const paneButtonLocator = page.getByTestId('debug-pane-button')
const headingLocator = page.getByRole('heading', {
name: 'Settings',
exact: true,
})
const inputLocator = page.locator('input[name="app-showDebugPanel"]')
await test.step('Open settings dialog and set "Show debug panel" to on', async () => {
await page.keyboard.press('ControlOrMeta+,')
await expect(headingLocator).toBeVisible()
/** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
await test.step(`Confirm that this dialog has a solid background`, async () => {
await expect
.poll(() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]), {
timeout: 1000,
message:
'Checking for solid background, should not see default plane colors',
})
.toBeLessThan(15)
})
await page.locator('#showDebugPanel').getByText('OffOn').click()
})
// Close it and open again with keyboard shortcut, while KCL editor is focused
// Put the cursor in the editor
await test.step('Open settings with keyboard shortcut', async () => {
await page.getByTestId('settings-close-button').click()
await page.locator('.cm-content').click()
await page.keyboard.press('ControlOrMeta+,')
await expect(headingLocator).toBeVisible()
})
// Verify the toast appeared
await expect(
page.getByText(`Set show debug panel to "false" for this project`)
).toBeVisible()
await expect(
page.getByText(`Set show debug panel to "false" for this project`)
).not.toBeVisible()
// Check that the debug panel button is gone
await expect(paneButtonLocator).not.toBeVisible()
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(inputLocator).toBeChecked()
// Roll back to default of "off"
await await page
.getByText('show debug panelRoll back show debug panelRoll back to match')
.hover()
await page
.getByRole('button', {
name: 'Roll back show debug panel',
})
.click()
await expect(inputLocator).not.toBeChecked()
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(
page.locator('input[name="app-showDebugPanel"]')
).not.toBeChecked()
})
test('Keybindings display the correct hotkey for Command Palette', async ({
page,
@ -175,103 +173,98 @@ test.describe('Testing settings', () => {
await expect(hotkey).toHaveText(text)
})
test.fixme(
'Project and user settings can be reset',
async ({ page, homePage }) => {
const u = await getUtils(page)
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
})
// Selectors and constants
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
const resetButton = (level: SettingsLevel) =>
page.getByRole('button', {
name: `Reset ${level}-level settings`,
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const settingValues = {
default: '259',
user: '120',
project: '50',
}
const resetToast = (level: SettingsLevel) =>
page.getByText(`${level}-level settings were reset`)
await test.step(`Open the settings modal`, async () => {
await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
})
await test.step('Set up theme color', async () => {
// Verify we're looking at the project-level settings,
// and it's set to default value
await expect(projectSettingsTab).toBeChecked()
await expect(themeColorSetting).toHaveValue(settingValues.default)
// Set project-level value to 50
await themeColorSetting.fill(settingValues.project)
// Set user-level value to 120
await userSettingsTab.click()
await themeColorSetting.fill(settingValues.user)
await projectSettingsTab.click()
})
await test.step('Reset project settings', async () => {
// Click the reset settings button.
await resetButton('project').click()
await expect(resetToast('project')).toBeVisible()
await expect(resetToast('project')).not.toBeVisible()
// Verify it is now set to the inherited user value
await expect(themeColorSetting).toHaveValue(settingValues.user)
await test.step(`Check that the user settings did not change`, async () => {
await userSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.user)
})
await test.step(`Set project-level again to test the user-level reset`, async () => {
await projectSettingsTab.click()
await themeColorSetting.fill(settingValues.project)
await userSettingsTab.click()
})
})
await test.step('Reset user settings', async () => {
// Click the reset settings button.
await resetButton('user').click()
await expect(resetToast('user')).toBeVisible()
await expect(resetToast('user')).not.toBeVisible()
// Verify it is now set to the default value
await expect(themeColorSetting).toHaveValue(settingValues.default)
await test.step(`Check that the project settings did not change`, async () => {
await projectSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.project)
})
test('Project and user settings can be reset', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
})
// Selectors and constants
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
const resetButton = (level: SettingsLevel) =>
page.getByRole('button', {
name: `Reset ${level}-level settings`,
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const settingValues = {
default: '259',
user: '120',
project: '50',
}
)
const resetToast = (level: SettingsLevel) =>
page.getByText(`${level}-level settings were reset`)
test.fixme(
await test.step(`Open the settings modal`, async () => {
await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
})
await test.step('Set up theme color', async () => {
// Verify we're looking at the project-level settings,
// and it's set to default value
await expect(projectSettingsTab).toBeChecked()
await expect(themeColorSetting).toHaveValue(settingValues.default)
// Set project-level value to 50
await themeColorSetting.fill(settingValues.project)
// Set user-level value to 120
await userSettingsTab.click()
await themeColorSetting.fill(settingValues.user)
await projectSettingsTab.click()
})
await test.step('Reset project settings', async () => {
// Click the reset settings button.
await resetButton('project').click()
await expect(resetToast('project')).toBeVisible()
await expect(resetToast('project')).not.toBeVisible()
// Verify it is now set to the inherited user value
await expect(themeColorSetting).toHaveValue(settingValues.user)
await test.step(`Check that the user settings did not change`, async () => {
await userSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.user)
})
await test.step(`Set project-level again to test the user-level reset`, async () => {
await projectSettingsTab.click()
await themeColorSetting.fill(settingValues.project)
await userSettingsTab.click()
})
})
await test.step('Reset user settings', async () => {
// Click the reset settings button.
await resetButton('user').click()
await expect(resetToast('user')).toBeVisible()
await expect(resetToast('user')).not.toBeVisible()
// Verify it is now set to the default value
await expect(themeColorSetting).toHaveValue(settingValues.default)
await test.step(`Check that the project settings did not change`, async () => {
await projectSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.project)
})
})
})
test(
`Project settings override user settings on desktop`,
{ tag: ['@electron', '@skipWin'] },
async ({ context, page }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
test.fixme(orRunWhenFullSuiteEnabled())
const projectName = 'bracket'
const { dir: projectDirName } = await context.folderSetupFn(
async (dir) => {
@ -407,12 +400,13 @@ test.describe('Testing settings', () => {
)
// It was much easier to test the logo color than the background stream color.
test.fixme(
test(
'user settings reload on external change, on project and modeling view',
{
tag: '@electron',
},
async ({ context, page, tronApp }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
if (!tronApp) {
fail()
}
@ -466,10 +460,11 @@ test.describe('Testing settings', () => {
}
)
test.fixme(
test(
'project settings reload on external change',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const { dir: projectDirName } = await context.folderSetupFn(
async () => {}
)
@ -724,7 +719,14 @@ test.describe('Testing settings', () => {
})
})
test('Changing theme in sketch mode', async ({ context, page, homePage }) => {
test('Changing theme in sketch mode', async ({
context,
page,
homePage,
toolbar,
scene,
cmdBar,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
const u = await getUtils(page)
@ -744,11 +746,10 @@ test.describe('Testing settings', () => {
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await scene.settled(cmdBar)
await page.waitForTimeout(1000)
// Selectors and constants
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
const lineToolButton = page.getByTestId('line')
const segmentOverlays = page.getByTestId('segment-overlay')
const sketchOriginLocation = { x: 600, y: 250 }
@ -757,8 +758,7 @@ test.describe('Testing settings', () => {
await test.step(`Get into sketch mode`, async () => {
await page.mouse.click(700, 200)
await expect(editSketchButton).toBeVisible()
await editSketchButton.click()
await toolbar.editSketch()
// We use the line tool as a proxy for sketch mode
await expect(lineToolButton).toBeVisible()
@ -813,7 +813,7 @@ test.describe('Testing settings', () => {
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const darkBackgroundColor = TEST_COLORS.DARK_MODE_BKGD
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
@ -977,63 +977,68 @@ fn cube`
/**
* This test assumes that the default value of the "highlight edges" setting is "on".
*/
test.fixme(
`Toggle stream settings multiple times`,
async ({ page, scene, homePage, context, toolbar, cmdBar }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const projectDir = join(dir, 'project-000')
await fsp.mkdir(projectDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(projectDir, 'main.kcl')
)
})
test(`Toggle stream settings multiple times`, async ({
page,
scene,
homePage,
context,
toolbar,
cmdBar,
}, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.folderSetupFn(async (dir) => {
const projectDir = join(dir, 'project-000')
await fsp.mkdir(projectDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(projectDir, 'main.kcl')
)
})
await test.step(`First snapshot`, async () => {
await homePage.openProject('project-000')
await toolbar.closePane('code')
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 20_000 })
await scene.clickNoWhere()
})
await test.step(`First snapshot`, async () => {
await homePage.openProject('project-000')
await toolbar.closePane('code')
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 20_000 })
await scene.clickNoWhere()
})
const toast = (value: boolean) =>
page.getByText(
`Set highlight edges to "${String(value)}" as a user default`
)
await test.step(`Toggle highlightEdges off`, async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Settings · modeling · highlight edges')
await cmdBar.selectOption({ name: 'off' }).click()
const falseToast = toast(false)
await expect(falseToast).toBeVisible()
await falseToast.waitFor({ state: 'detached' })
})
await expect(scene.streamWrapper).not.toHaveScreenshot(
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
}
const toast = (value: boolean) =>
page.getByText(
`Set highlight edges to "${String(value)}" as a user default`
)
await test.step(`Toggle highlightEdges on`, async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Settings · modeling · highlight edges')
await cmdBar.selectOption({ name: 'on' }).click()
const trueToast = toast(true)
await expect(trueToast).toBeVisible()
await trueToast.waitFor({ state: 'detached' })
})
await test.step(`Toggle highlightEdges off`, async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Settings · modeling · highlight edges')
await cmdBar.selectOption({ name: 'off' }).click()
const falseToast = toast(false)
await expect(falseToast).toBeVisible()
await falseToast.waitFor({ state: 'detached' })
})
await expect(scene.streamWrapper).toHaveScreenshot(
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
}
)
}
)
await expect(scene.streamWrapper).not.toHaveScreenshot(
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
}
)
await test.step(`Toggle highlightEdges on`, async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Settings · modeling · highlight edges')
await cmdBar.selectOption({ name: 'on' }).click()
const trueToast = toast(true)
await expect(trueToast).toBeVisible()
await trueToast.waitFor({ state: 'detached' })
})
await expect(scene.streamWrapper).toHaveScreenshot(
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
}
)
})
})

View File

@ -1,6 +1,10 @@
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { getUtils, createProject } from './test-utils'
import {
getUtils,
createProject,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { join } from 'path'
import fs from 'fs'
@ -431,10 +435,11 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
})
// This will be fine once greg makes prompt at top of file deterministic
test.fixme(
test(
'can do many at once and get many prompts back, and interact with many',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
@ -619,10 +624,11 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
})
}
test.fixme(
test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'

View File

@ -1,8 +1,14 @@
import { test, expect } from './zoo-test'
import { doExport, getUtils, makeTemplate } from './test-utils'
import {
doExport,
getUtils,
makeTemplate,
orRunWhenFullSuiteEnabled,
} from './test-utils'
test.fixme('Units menu', async ({ page, homePage }) => {
test('Units menu', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -263,186 +269,187 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
).not.toBeVisible()
})
test.fixme(
'Basic default modeling and sketch hotkeys work',
async ({ page, homePage }) => {
const u = await getUtils(page)
test('Basic default modeling and sketch hotkeys work', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
// This test can run long if it takes a little too long to load
// the engine.
test.setTimeout(90000)
// This test has a weird bug on ubuntu
// Funny, it's flaking on Windows too :). I think there is just something
// actually wrong.
test.skip(
process.platform === 'linux',
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
)
// Load the app with the code pane open
// This test can run long if it takes a little too long to load
// the engine.
test.setTimeout(90000)
// This test has a weird bug on ubuntu
// Funny, it's flaking on Windows too :). I think there is just something
// actually wrong.
test.skip(
process.platform === 'linux',
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
)
// Load the app with the code pane open
await test.step(`Set up test`, async () => {
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
})
const codePane = page.locator('.cm-content')
const lineButton = page.getByRole('button', {
name: 'line Line',
exact: true,
})
const arcButton = page.getByRole('button', {
name: 'arc Tangential Arc',
exact: true,
})
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
const commandBarComboBox = page.getByPlaceholder('Search commands')
const exitSketchButton = page.getByRole('button', { name: 'Exit Sketch' })
await test.step(`Type code with modeling hotkeys, shouldn't fire`, async () => {
await codePane.click()
await page.keyboard.type('//')
await page.keyboard.press('s')
await expect(commandBarComboBox).not.toBeVisible()
await page.keyboard.press('e')
await expect(commandBarComboBox).not.toBeVisible()
await expect(codePane).toHaveText('//se')
})
// Blur focus from the code editor, use the s command to sketch
await test.step(`Blur editor focus, enter sketch`, async () => {
/**
* TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the
* start of the sketch.
* and a separate Safari-only bug that causes the test to fail
* if the pane is open the entire test. The maintainer of CodeMirror
* has pinpointed this to the unusual browser behavior:
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
*/
await blurCodeEditor()
await page.waitForTimeout(1000)
await page.keyboard.press('s')
await page.waitForTimeout(1000)
await page.mouse.move(800, 300, { steps: 5 })
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 15_000,
})
})
// Use some sketch hotkeys to create a sketch (l and a for now)
await test.step(`Incomplete sketch with hotkeys`, async () => {
await test.step(`Draw a line`, async () => {
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
})
await test.step(`Unequip line tool`, async () => {
await page.keyboard.press('l')
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
})
await test.step(`Draw a tangential arc`, async () => {
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 10_000,
await test.step(`Set up test`, async () => {
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
})
await test.step(`Unequip with escape, equip line tool`, async () => {
await page.keyboard.press('Escape')
await page.keyboard.press('l')
await page.waitForTimeout(50)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
})
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
})
await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => {
// Since there's code now, we have to get to the end of the line
await page.locator('.cm-line').last().click()
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('ArrowRight')
await page.keyboard.up('ControlOrMeta')
const codePane = page.locator('.cm-content')
const lineButton = page.getByRole('button', {
name: 'line Line',
exact: true,
})
const arcButton = page.getByRole('button', {
name: 'arc Tangential Arc',
exact: true,
})
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
const commandBarComboBox = page.getByPlaceholder('Search commands')
const exitSketchButton = page.getByRole('button', { name: 'Exit Sketch' })
await page.keyboard.press('Enter')
await page.keyboard.type('//')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
await page.keyboard.press('a')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
await expect(codePane).toContainText('//la')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await test.step(`Type code with modeling hotkeys, shouldn't fire`, async () => {
await codePane.click()
await page.keyboard.type('//')
await page.keyboard.press('s')
await expect(commandBarComboBox).not.toBeVisible()
await page.keyboard.press('e')
await expect(commandBarComboBox).not.toBeVisible()
await expect(codePane).toHaveText('//se')
})
// Blur focus from the code editor, use the s command to sketch
await test.step(`Blur editor focus, enter sketch`, async () => {
/**
* TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the
* start of the sketch.
* and a separate Safari-only bug that causes the test to fail
* if the pane is open the entire test. The maintainer of CodeMirror
* has pinpointed this to the unusual browser behavior:
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
*/
await blurCodeEditor()
await page.waitForTimeout(1000)
await page.keyboard.press('s')
await page.waitForTimeout(1000)
await page.mouse.move(800, 300, { steps: 5 })
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 15_000,
})
})
await test.step(`Close profile and exit sketch`, async () => {
await blurCodeEditor()
// Use some sketch hotkeys to create a sketch (l and a for now)
await test.step(`Incomplete sketch with hotkeys`, async () => {
await test.step(`Draw a line`, async () => {
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
// On close it will unequip the line tool.
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
await expect(exitSketchButton).toBeEnabled()
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
})
// Extrude with e
await test.step(`Extrude the sketch`, async () => {
await page.mouse.click(750, 150)
await blurCodeEditor()
await expect(extrudeButton).toBeEnabled()
await page.keyboard.press('e')
await page.waitForTimeout(500)
await page.mouse.move(800, 200, { steps: 5 })
await page.mouse.click(800, 200)
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible({
timeout: 20_000,
await test.step(`Unequip line tool`, async () => {
await page.keyboard.press('l')
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
})
await test.step(`Draw a tangential arc`, async () => {
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 10_000,
})
await page.getByRole('button', { name: 'Continue' }).click()
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await expect(page.locator('.cm-content')).toContainText('extrude(')
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
})
// await codePaneButton.click()
// await expect(u.codeLocator).not.toBeVisible()
/**
* work-around: to stop `keyboard.press()` from typing in the editor even when it should be blurred
*/
async function blurCodeEditor() {
await page.getByRole('button', { name: 'Commands' }).click()
await page.waitForTimeout(100)
await test.step(`Unequip with escape, equip line tool`, async () => {
await page.keyboard.press('Escape')
await page.waitForTimeout(100)
}
await page.keyboard.press('l')
await page.waitForTimeout(50)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
})
})
await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => {
// Since there's code now, we have to get to the end of the line
await page.locator('.cm-line').last().click()
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('ArrowRight')
await page.keyboard.up('ControlOrMeta')
await page.keyboard.press('Enter')
await page.keyboard.type('//')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
await page.keyboard.press('a')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
await expect(codePane).toContainText('//la')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
})
await test.step(`Close profile and exit sketch`, async () => {
await blurCodeEditor()
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
// On close it will unequip the line tool.
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
await expect(exitSketchButton).toBeEnabled()
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
// Extrude with e
await test.step(`Extrude the sketch`, async () => {
await page.mouse.click(750, 150)
await blurCodeEditor()
await expect(extrudeButton).toBeEnabled()
await page.keyboard.press('e')
await page.waitForTimeout(500)
await page.mouse.move(800, 200, { steps: 5 })
await page.mouse.click(800, 200)
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible({
timeout: 20_000,
})
await page.getByRole('button', { name: 'Continue' }).click()
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await expect(page.locator('.cm-content')).toContainText('extrude(')
})
// await codePaneButton.click()
// await expect(u.codeLocator).not.toBeVisible()
/**
* work-around: to stop `keyboard.press()` from typing in the editor even when it should be blurred
*/
async function blurCodeEditor() {
await page.getByRole('button', { name: 'Commands' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Escape')
await page.waitForTimeout(100)
}
)
})
test('Delete key does not navigate back', async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })

View File

@ -20,7 +20,7 @@ statement[@isGroup=Statement] {
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
TypeDeclaration { kw<"export">? kw<"type"> identifier } |
TypeDeclaration { kw<"export">? kw<"type"> identifier ("=" type)? } |
ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } |
Annotation { AnnotationName AnnotationList? }
@ -79,7 +79,7 @@ type[@isGroup=Type] {
identifier,
"bool" | "number" | "string" | "tag" | "Sketch" | "SketchSurface" | "Solid" | "Plane"
> |
ArrayType { type !member "[" "]" } |
ArrayType { "[" type !member (";" Number "+"?)? "]" } |
ObjectType { "{" commaSep<ObjectProperty { PropertyName ":" type }> "}" }
}
@ -137,7 +137,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
"(" ")"
"{" "}"
"[" "]"
"," "?" ":" "." ".."
"," "?" ":" "." ".." ";"
}
@external propSource kclHighlight from "./highlight"

View File

@ -21,7 +21,7 @@
"@codemirror/autocomplete": "6.18.6",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@lezer/highlight": "^1.2.0",
"@lezer/highlight": "^1.2.1",
"@ts-stack/markdown": "^1.5.0",
"json-rpc-2.0": "^1.7.0",
"typescript": "^5.8.2",
@ -29,7 +29,7 @@
"vscode-uri": "^3.1.0"
},
"devDependencies": {
"@types/node": "^22.13.9",
"@types/node": "^22.13.10",
"ts-node": "^10.9.2"
}
}

View File

@ -70,10 +70,10 @@
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
dependencies:
"@lezer/common" "^1.0.0"
@ -116,10 +116,10 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/node@^22.13.9":
version "22.13.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
"@types/node@^22.13.10":
version "22.13.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4"
integrity sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==
dependencies:
undici-types "~6.20.0"

View File

@ -5,8 +5,8 @@
@settings(defaultLengthUnit = in)
// Define constants
lbumps = 10 // number of bumps long
wbumps = 5 // number of bumps wide
lbumps = 4 // number of bumps long
wbumps = 2 // number of bumps wide
pitch = 8.0
clearance = 0.1
bumpDiam = 4.8

View File

@ -1,6 +1,7 @@
// Pipe with bend
// A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow.
// Set units
@settings(defaultLengthUnit = in)
@ -14,22 +15,14 @@ bendAngle = 90
sketch000 = startSketchOn("XZ")
// create a profile for the outer diameter
outerProfile = circle(
sketch000,
center = [bendRadius, 0],
radius = outerDiameter / 2
)
outerProfile = circle(sketch000, center = [bendRadius, 0], radius = outerDiameter / 2)
// create a profile for the inner diameter
innerProfile = circle(
sketch000,
center = [bendRadius, 0],
radius = innerDiameter / 2
)
innerProfile = circle(sketch000, center = [bendRadius, 0], radius = innerDiameter / 2)
// create the profile of the pipe
pipeProfile = outerProfile
|> hole(innerProfile, %)
// revolve the pipe profile at the desired angle
pipe = revolve(pipeProfile, axis = "Y", angle = bendAngle)
pipe = revolve(pipeProfile, axis = 'Y', angle = bendAngle)

View File

@ -1,9 +1,11 @@
// Poopy Shoe
// poop shute for bambu labs printer - optimized for printing.
// Set units
@settings(defaultLengthUnit = in)
wallThickness = 0.125
wallsWidth = 3
height = 5.125
@ -30,8 +32,7 @@ sketch001 = startSketchOn("-YZ")
|> yLine(endAbsolute = segEndY(seg01))
|> angledLineToY({ angle = 180 - 60, to = 0 }, %)
|> close()
part001 = revolve(
sketch001,
part001 = revolve(sketch001,
angle = 90,
axis = {
custom = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,68 +1,64 @@
// Socket Head Cap Screw
// This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key.
// Set units
@settings(defaultLengthUnit = in)
// set units
@settings(defaultLengthUnit = in, defaultAngleUnit = deg)
// Define constants
screwLength = 1.0
screwDiameter = .190
headDiameter = .313
headLength = screwDiameter
hexWallToWall = 5 / 32
capRatio = screwDiameter / headDiameter
hexRatio = hexWallToWall / headDiameter
hexWallLength = hexWallToWall / 2 * 1 / cos(toRadians(30))
hexStartingAngle = 210 // first angle of hex pattern
hexInteriorAngle = 120
hexChangeAngle = 180 - hexInteriorAngle
export boltDiameter = 0.190
export boltLength = 1.00
export boltHeadLength = boltDiameter
export boltHeadDiameter = 0.313
export boltHexDrive = 5/32
export boltHexFlatLength = boltHexDrive / (2 * cos(toRadians(30)))
// Write a function that defines the Socket Head Cap Screw
fn capScrew(start, length, dia, capHeadLength) {
export fn bolt () {
// Create the head of the cap screw
screwHeadSketch = startSketchOn('XZ')
boltHead = startSketchOn('XZ')
|> circle(
center = [start[0], start[1]],
radius = dia / capRatio / 2
center = [0, 0],
radius = boltHeadDiameter / 2,
tag = $topEdge
)
// Extrude the screw head sketch
screwHead = extrude(screwHeadSketch, length = capHeadLength)
|> extrude(length = -boltHeadLength)
|> fillet(radius = 0.020, tags = [topEdge, getOppositeEdge(topEdge)])
// Define the sketch of the hex pattern on the screw head
hexPatternSketch = startSketchOn(screwHead, 'end')
|> startProfileAt([hexWallToWall / 2, 0], %)
|> yLine(length = -hexWallLength / 2)
hexPatternSketch = startSketchOn(boltHead, 'start')
|> startProfileAt([
boltHexDrive / 2,
boltHexFlatLength / 2
], %)
|> angledLine({
angle = hexStartingAngle,
length = hexWallLength
angle = 270,
length = boltHexFlatLength
}, %)
|> angledLine({
angle = hexStartingAngle - hexChangeAngle,
length = hexWallLength
angle = 210,
length = boltHexFlatLength
}, %)
|> angledLine({
angle = hexStartingAngle - (2 * hexChangeAngle),
length = hexWallLength
angle = 150,
length = boltHexFlatLength
}, %)
|> angledLine({
angle = hexStartingAngle - (3 * hexChangeAngle),
length = hexWallLength
angle = 90,
length = boltHexFlatLength
}, %)
|> angledLine({
angle = hexStartingAngle - (4 * hexChangeAngle),
length = hexWallLength
angle = 30,
length = boltHexFlatLength
}, %)
|> close()
hexPattern = extrude(hexPatternSketch, length = -headLength * 0.75)
|> extrude(length = -boltHeadLength * 0.75)
boltBody = startSketchOn(boltHead, 'end')
|> circle(center = [0, 0], radius = boltDiameter / 2, tag = $filletEdge)
|> extrude(length = boltLength)
|> fillet(radius = .020, tags = [getOppositeEdge(filletEdge)])
|> appearance(color = "#4dd043", metalness = 90, roughness = 90)
screwBodySketch = startSketchOn(screwHead, "start")
|> circle(
center = [start[0], start[1]],
radius = dia / 2
)
screwBody = extrude(screwBodySketch, length = length)
return screwBody
return boltBody
}
capScrew([0, 0], screwLength, screwDiameter, screwDiameter)
bolt()

View File

@ -1,50 +1,37 @@
// Antenna
// import constants
import antennaLength, antennaBaseWidth, antennaBaseHeight, antennaTopWidth, antennaTopHeight from "globals.kcl"
// Set units
@settings(defaultLengthUnit = in)
// import constants
import height, width, antennaBaseWidth, antennaBaseHeight, antennaTopWidth, antennaTopHeight from "globals.kcl"
// Calculate the origin
origin = [-width / 2 + .45, -0.10]
// Create the antenna
antennaX = origin[0]
antennaY = origin[1]
antennaPlane = {
plane = {
origin = { x = 0, y = 0, z = height / 2 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
export fn antenna () {
// Create the antenna base sketch
sketch001 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line(end = [antennaBaseWidth, 0])
|> line(end = [0, -antennaBaseHeight])
|> line(end = [-antennaBaseWidth, 0])
|> close()
// Create the antenna top sketch
loftPlane = offsetPlane('XY', offset = antennaLength)
sketch002 = startSketchOn(loftPlane)
|> startProfileAt([
(antennaBaseWidth - antennaTopWidth) / 2,
(antennaBaseHeight - antennaTopHeight) / 2
], %)
|> xLine(length = antennaTopWidth)
|> yLine(length = -antennaTopHeight)
|> xLine(length = -antennaTopWidth)
|> close()
// Create the antenna using a loft
antenna = loft([sketch001, sketch002])
|> appearance(color = "#000000")
return antenna
}
// Create the antenna base sketch
sketch001 = startSketchOn(antennaPlane)
|> startProfileAt([origin[0], origin[1]], %)
|> line(end = [antennaBaseWidth, 0])
|> line(end = [0, -antennaBaseHeight])
|> line(end = [-antennaBaseWidth, 0])
|> close()
// Create the antenna top sketch
loftPlane = offsetPlane('XY', offset = height / 2 + 2)
sketch002 = startSketchOn(loftPlane)
|> startProfileAt([
origin[0] + (antennaBaseWidth - antennaTopWidth) / 2,
origin[1] - ((antennaBaseHeight - antennaTopHeight) / 2)
], %)
|> xLine(length = antennaTopWidth)
|> yLine(length = -antennaTopHeight)
|> xLine(length = -antennaTopWidth)
|> close()
// Create the antenna using a loft
loft([sketch001, sketch002])
|> appearance(color = "#000000")

View File

@ -1,80 +1,86 @@
// Walkie talkie body
// Set units
@settings(defaultLengthUnit = in)
// Import constants
// import constants
import height, width, thickness, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight from "globals.kcl"
bodySketch = startSketchOn('XZ')
|> startProfileAt([-width / 2, height / 2], %)
|> xLine(length = width, tag = $chamfer1)
|> yLine(length = -height, tag = $chamfer2)
|> xLine(length = -width, tag = $chamfer3)
|> close(tag = $chamfer4)
bodyExtrude = extrude(bodySketch, length = thickness)
|> chamfer(
length = chamferLength,
tags = [
getNextAdjacentEdge(chamfer1),
getNextAdjacentEdge(chamfer2),
getNextAdjacentEdge(chamfer3),
getNextAdjacentEdge(chamfer4)
]
)
// set units
@settings(defaultLengthUnit = in)
// Define the offset for the indentation
sketch002 = startSketchOn(bodyExtrude, 'END')
|> startProfileAt([
-width / 2 + offset,
height / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({ angle = 45, to = height / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + offset / 2 * cos(toRadians(45))),
height / 2 - offset
])
|> angledLineToX({ angle = -45, to = width / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - offset,
-(height / 2 - (chamferLength + offset / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))),
-height / 2 + offset
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset
}, %)
|> close()
extrude002 = extrude(sketch002, length = -0.0625)
// create a function to define the body
export fn body () {
// Create the pocket for the screen
sketch003 = startSketchOn(extrude002, 'start')
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(length = screenWidth, tag = $seg01)
|> yLine(length = -screenHeight)
|> xLine(length = -segLen(seg01))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(sketch003, length = screenDepth)
// sketch and extrude the body of the walkie talkie
bodySketch = startSketchOn('XZ')
|> startProfileAt([-width / 2, height / 2], %)
|> xLine(length = width, tag = $chamfer1)
|> yLine(length = -height, tag = $chamfer2)
|> xLine(length = -width, tag = $chamfer3)
|> close(tag = $chamfer4)
bodyExtrude = extrude(bodySketch, length = thickness)
|> chamfer(
length = chamferLength,
tags = [
getNextAdjacentEdge(chamfer1),
getNextAdjacentEdge(chamfer2),
getNextAdjacentEdge(chamfer3),
getNextAdjacentEdge(chamfer4)
]
)
// Create the speaker box
sketch004 = startSketchOn(extrude002, 'start')
|> startProfileAt([-1.25 / 2, -.125], %)
|> xLine(length = speakerBoxWidth)
|> yLine(length = -speakerBoxHeight)
|> xLine(length = -speakerBoxWidth)
|> close()
extrude(sketch004, length = -.5)
|> appearance(
color = "#277bb0",
)
// cut out the indentation for the case
sketch002 = startSketchOn(bodyExtrude, 'END')
|> startProfileAt([
-width / 2 + offset,
height / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({ angle = 45, to = height / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + offset / 2 * cos(toRadians(45))),
height / 2 - offset
])
|> angledLineToX({ angle = -45, to = width / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - offset,
-(height / 2 - (chamferLength + offset / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))),
-height / 2 + offset
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset
}, %)
|> close()
extrude002 = extrude(sketch002, length = -0.0625)
// Create the pocket for the screen
sketch003 = startSketchOn(extrude002, 'start')
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(length = screenWidth, tag = $seg01)
|> yLine(length = -screenHeight)
|> xLine(length = -segLen(seg01))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(sketch003, length = screenDepth)
// Create the speaker box
sketch004 = startSketchOn(extrude002, 'start')
|> startProfileAt([-1.25 / 2, -.125], %)
|> xLine(length = speakerBoxWidth)
|> yLine(length = -speakerBoxHeight)
|> xLine(length = -speakerBoxWidth)
|> close()
body = extrude(sketch004, length = -.5)
|> appearance(
color = "#277bb0",
)
return body
}
body()

View File

@ -1,26 +1,27 @@
// Walkie Talkie button
// Set units
// set units
@settings(defaultLengthUnit = in)
// Import constants
import screenHeight, buttonWidth, tolerance, buttonHeight, buttonThickness from 'globals.kcl'
// import constants
import buttonWidth, buttonHeight, buttonThickness from 'globals.kcl'
// create a function to define the button
export fn button() {
// Create a function for the button
export fn button(origin, rotation, plane) {
buttonSketch = startSketchOn(plane)
|> startProfileAt([origin[0], origin[1]], %)
// sketch the button profile and extrude
buttonSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> angledLine({
angle = 180 + rotation,
angle = 180,
length = buttonWidth
}, %, $tag1)
|> angledLine({
angle = 270 + rotation,
angle = 270,
length = buttonHeight
}, %, $tag2)
|> angledLine({
angle = 0 + rotation,
angle = 0,
length = buttonWidth
}, %)
|> close()
@ -35,4 +36,4 @@ export fn button(origin, rotation, plane) {
|> appearance(color = "#ff0000")
return buttonExtrude
}
}

View File

@ -1,85 +1,90 @@
// Walkie talkie case
// Set units
@settings(defaultLengthUnit = in)
// Import constants and Zoo logo
// import constants and Zoo logo
import width, height, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight, squareHoleSideLength, caseTolerance from "globals.kcl"
import zLogo, oLogo, oLogo2 from "zoo-logo.kcl"
plane = offsetPlane("XZ", offset = 1)
// set units
@settings(defaultLengthUnit = in)
fn screenHole(sketchStart) {
sketch006 = startSketchOn(sketchStart)
// create a function to define the case
export fn case () {
// sketch the profile of the screen
sketch006 = startSketchOn(startSketchOn('XZ'))
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(length = screenWidth)
|> yLine(length = -screenHeight)
|> xLine(length = -screenWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
return sketch006
}
fn squareHolePattern(plane, x, y) {
// create transform functions for the speaker grid pattern
fn transformX(i) {
return { translate = [.125 * i, 0] }
}
fn transformY(i) {
return { translate = [0, -.125 * i] }
}
squareHolePatternSketch = startSketchOn(plane)
|> startProfileAt([-x, -y], %)
// sketch the square hole grid pattern
squareHolePatternSketch = startSketchOn(startSketchOn('XZ'))
|> startProfileAt([-screenWidth / 2 + .100, 0], %)
|> line(end = [squareHoleSideLength / 2, 0])
|> line(end = [0, -squareHoleSideLength / 2])
|> line(end = [-squareHoleSideLength / 2, 0])
|> close()
|> patternTransform2d(instances = 13, transform = transformX)
|> patternTransform2d(instances = 11, transform = transformY)
return squareHolePatternSketch
}
sketch005 = startSketchOn(offsetPlane("XZ", offset = 1))
|> startProfileAt([
-width / 2 + offset + caseTolerance,
height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({
angle = 45,
to = height / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))),
height / 2 - (offset + caseTolerance)
])
|> angledLineToX({
angle = -45,
to = width / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (offset + caseTolerance),
-(height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset + caseTolerance
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))),
-height / 2 + offset + caseTolerance
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset + caseTolerance
}, %)
|> close()
|> hole(screenHole(plane), %)
|> hole(squareHolePattern(plane, .75, .125), %)
|> hole(zLogo(plane, [-.30, -1.825], .20), %)
|> hole(oLogo(plane, [-.075, -1.825], .20), %)
|> hole(oLogo2(plane, [-.075, -1.825], .20), %)
|> hole(oLogo(plane, [.175, -1.825], .20), %)
|> hole(oLogo2(plane, [.175, -1.825], .20), %)
extrude(sketch005, length = -0.0625)
|> appearance(color = '#D0FF01', metalness = 0, roughness = 50)
// sketch the outer profile of the case and extrude with holes using the previously made profiles
sketch005 = startSketchOn(startSketchOn('XZ'))
|> startProfileAt([
-width / 2 + offset + caseTolerance,
height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({
angle = 45,
to = height / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))),
height / 2 - (offset + caseTolerance)
])
|> angledLineToX({
angle = -45,
to = width / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (offset + caseTolerance),
-(height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset + caseTolerance
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))),
-height / 2 + offset + caseTolerance
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset + caseTolerance
}, %)
|> close()
|> hole(sketch006, %)
|> hole(squareHolePatternSketch, %)
// create the Zoo logo
|> hole(zLogo(startSketchOn('XZ'), [-.30, -1.825], .20), %)
|> hole(oLogo(startSketchOn('XZ'), [-.075, -1.825], .20), %)
|> hole(oLogo2(startSketchOn('XZ'), [-.075, -1.825], .20), %)
|> hole(oLogo(startSketchOn('XZ'), [.175, -1.825], .20), %)
|> hole(oLogo2(startSketchOn('XZ'), [.175, -1.825], .20), %)
case = extrude(sketch005, length = -0.0625)
|> appearance(color = '#D0FF01', metalness = 0, roughness = 50)
return case
}

View File

@ -21,6 +21,7 @@ export antennaBaseWidth = .5
export antennaBaseHeight = .25
export antennaTopWidth = .30
export antennaTopHeight = .05
export antennaLength = 3
// button
export buttonWidth = .15

View File

@ -1,29 +1,16 @@
// Walkie talkie knob
// Walkie Talkie Frequency Knob
// Set units
@settings(defaultLengthUnit = in)
// Import constants
// import constants
import width, thickness, height, knobDiameter, knobHeight, knobRadius from "globals.kcl"
// Define the plane for the knob
knobPlane = {
plane = {
origin = {
x = width / 2 - 0.70,
y = -thickness / 2,
z = height / 2
},
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
// set units
@settings(defaultLengthUnit = in)
// create a function to define the knob
export fn knob () {
// Create the knob sketch and revolve
startSketchOn(knobPlane)
knob = startSketchOn('XZ')
|> startProfileAt([0.0001, 0], %)
|> xLine(length = knobDiameter / 2)
|> yLine(length = knobHeight - 0.05)
@ -34,5 +21,11 @@ startSketchOn(knobPlane)
}, %)
|> xLine(endAbsolute = 0.0001)
|> close()
|> revolve(axis = "Y")
|> revolve(
axis = "Y",
)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 50)
return knob
}

View File

@ -1,50 +1,47 @@
// Walkie Talkie
// A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings.
// Set units
// set units
@settings(defaultLengthUnit = in)
// Import parts and constants
import 'body.kcl'
import 'antenna.kcl'
import 'case.kcl'
import 'talk-button.kcl' as talkButton
import 'knob.kcl'
// import constants
import * from 'globals.kcl'
// import parts and constants
import body from 'body.kcl'
import case from 'case.kcl'
import antenna from 'antenna.kcl'
import talkButton from 'talk-button.kcl'
import knob from 'knob.kcl'
import button from "button.kcl"
import width, height, thickness, screenWidth, screenHeight, screenYPosition, tolerance from "globals.kcl"
// Import the body
body
// import the body
body()
// Import the case
case
// import the antenna
antenna()
|> translate(translate = [-width / 2 + .45, -0.10, height/2])
// Import the antenna
antenna
// import the case
case()
|> translate(translate = [0, -1, 0])
// Import the buttons
button([
-(screenWidth / 2 + tolerance),
screenYPosition
], 0, offsetPlane("XZ", offset = thickness))
button([
-(screenWidth / 2 + tolerance),
screenYPosition - (screenHeight / 2)
], 0, offsetPlane("XZ", offset = thickness))
button([
screenWidth / 2 + tolerance,
screenYPosition - screenHeight
], 180, offsetPlane("XZ", offset = thickness))
button([
screenWidth / 2 + tolerance,
screenYPosition - (screenHeight / 2)
], 180, offsetPlane("XZ", offset = thickness))
// Import the talk button
talkButton
// Import the frequency knob
knob
// import the talk button
talkButton()
|> translate(translate = [width / 2, -thickness / 2, .5])
// import the frequency knob
knob()
|> translate(translate = [width / 2 - 0.70, -thickness / 2, height / 2])
// import the buttons
button()
|> translate(translate = [-(screenWidth / 2 + tolerance), -1, screenYPosition])
button()
|> translate(translate = [-(screenWidth / 2 + tolerance), -1, screenYPosition - buttonHeight - tolerance*2])
button()
|> rotate(%, roll = 0, pitch = 180, yaw = 0)
|> translate(translate = [screenWidth / 2 + tolerance, -1, screenYPosition - buttonHeight], global = true)
button()
|> rotate(%, roll = 0, pitch = 180, yaw = 0)
|> translate(translate = [screenWidth / 2 + tolerance, -1, screenYPosition - buttonHeight*2 - tolerance * 2], global = true)

View File

@ -1,46 +1,37 @@
// Walkie talkie talk button
// Set units
// set units
@settings(defaultLengthUnit = in)
// Import constants
// import constants
import width, thickness, talkButtonSideLength, talkButtonHeight from "globals.kcl"
talkButtonPlane = {
plane = {
origin = {
x = width / 2,
y = -thickness / 2,
z = .5
},
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
export fn talkButton() {
// create the talk button sketch
talkButtonSketch = startSketchOn('YZ')
|> startProfileAt([
-talkButtonSideLength / 2,
talkButtonSideLength / 2
], %)
|> xLine(length = talkButtonSideLength, tag = $tag1)
|> yLine(length = -talkButtonSideLength, tag = $tag2)
|> xLine(length = -talkButtonSideLength, tag = $tag3)
|> close(tag = $tag4)
// create the talk button and apply fillets
talkButton = extrude(talkButtonSketch, length = talkButtonHeight)
|> fillet(
radius = 0.050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2),
getNextAdjacentEdge(tag3),
getNextAdjacentEdge(tag4)
]
)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 90)
return talkButton
}
// Create the talk button sketch
talkButtonSketch = startSketchOn(talkButtonPlane)
|> startProfileAt([
-talkButtonSideLength / 2,
talkButtonSideLength / 2
], %)
|> xLine(length = talkButtonSideLength, tag = $tag1)
|> yLine(length = -talkButtonSideLength, tag = $tag2)
|> xLine(length = -talkButtonSideLength, tag = $tag3)
|> close(tag = $tag4)
// Create the talk button and apply fillets
extrude(talkButtonSketch, length = talkButtonHeight)
|> fillet(
radius = 0.050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2),
getNextAdjacentEdge(tag3),
getNextAdjacentEdge(tag4)
]
)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 90)

View File

@ -1,6 +1,6 @@
// Zoo logo
// Define a function to draw the ZOO "Z"
// define a function to draw the ZOO "Z"
export fn zLogo(surface, origin, scale) {
zSketch = surface
|> startProfileAt([
@ -39,7 +39,7 @@ export fn zLogo(surface, origin, scale) {
return zSketch
}
// Define a function to draw the ZOO "O"
// define a function to draw the ZOO "O"
export fn oLogo(surface, origin, scale) {
oSketch001 = surface
|> startProfileAt([

40
rust/Cargo.lock generated
View File

@ -193,9 +193,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.87"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
@ -789,9 +789,9 @@ checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]]
name = "deranged"
version = "0.3.11"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
@ -1230,9 +1230,9 @@ dependencies = [
[[package]]
name = "handlebars"
version = "6.3.1"
version = "6.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d752747ddabc4c1a70dd28e72f2e3c218a816773e0d7faf67433f1acfa6cba7c"
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
dependencies = [
"derive_builder",
"log",
@ -2033,9 +2033,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.105"
version = "0.2.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64667cdf4b21ba87940b6733aad67ce9fd819b94fd3b6ca90f10389c1a3750c"
checksum = "cb129c1f4906a76e3518e58f61968f16cb56f1279366415d2bae6059aa68fce8"
dependencies = [
"anyhow",
"chrono",
@ -3086,9 +3086,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.14"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64",
"bytes",
@ -3890,9 +3890,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.39"
version = "0.3.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
dependencies = [
"deranged",
"itoa",
@ -3905,15 +3905,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.20"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
dependencies = [
"num-conv",
"time-core",
@ -4389,9 +4389,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.15.1"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.1",
"js-sys",
@ -5027,9 +5027,9 @@ dependencies = [
[[package]]
name = "zip"
version = "2.4.1"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes",
"arbitrary",

View File

@ -28,7 +28,7 @@ similar = { opt-level = 3 }
debug = "line-tables-only"
[workspace.dependencies]
async-trait = "0.1.85"
async-trait = "0.1.88"
anyhow = { version = "1" }
bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
clap = { version = "4.5.31", features = ["derive"] }
@ -36,7 +36,7 @@ dashmap = { version = "6.1.0" }
http = "1"
indexmap = "2.7.0"
kittycad = { version = "0.3.33", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.105", features = ["ts-rs", "websocket"] }
kittycad-modeling-cmds = { version = "0.2.107", features = ["ts-rs", "websocket"] }
lazy_static = "1.5.0"
miette = "7.5.0"
pyo3 = { version = "0.24.0" }
@ -50,7 +50,7 @@ tokio = { version = "1" }
tower-lsp = { version = "0.20.0", default-features = false }
tracing-subscriber = { version = "0.3.19", features = ["registry", "std", "fmt", "smallvec", "ansi", "tracing-log", "json"] }
uuid = { version = "1", features = ["v4", "serde"] }
zip = { version = "2.4.1", default-features = false }
zip = { version = "2.4.2", default-features = false }
[workspace.lints.clippy]
assertions_on_result_states = "warn"

View File

@ -21,7 +21,7 @@ slog = { workspace = true }
slog-async = { workspace = true }
slog-json = { workspace = true }
slog-term = { workspace = true }
time = "0.3.37"
time = "0.3.40"
tokio = { workspace = true, features = ["full"] }
tracing-subscriber = { workspace = true }
xshell = "0.2.6"

View File

@ -133,14 +133,14 @@
"@tsconfig/strictest": "^2.0.5",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.9",
"@types/node": "^22.13.10",
"@types/vscode": "^1.97.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^2.30.0",
"cross-env": "^7.0.3",
"esbuild": "^0.25.0",
"esbuild": "^0.25.1",
"glob": "^10.4.3",
"mocha": "^11.1.0",
"typescript": "^5.8.2"

View File

@ -120,130 +120,130 @@
jsonwebtoken "^9.0.0"
uuid "^8.3.0"
"@esbuild/aix-ppc64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64"
integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==
"@esbuild/aix-ppc64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz#c33cf6bbee34975626b01b80451cbb72b4c6c91d"
integrity sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==
"@esbuild/android-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f"
integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==
"@esbuild/android-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz#ea766015c7d2655164f22100d33d7f0308a28d6d"
integrity sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==
"@esbuild/android-arm@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b"
integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==
"@esbuild/android-arm@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.1.tgz#e84d2bf2fe2e6177a0facda3a575b2139fd3cb9c"
integrity sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==
"@esbuild/android-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163"
integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==
"@esbuild/android-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.1.tgz#58337bee3bc6d78d10425e5500bd11370cfdfbed"
integrity sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==
"@esbuild/darwin-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c"
integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==
"@esbuild/darwin-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz#a46805c1c585d451aa83be72500bd6e8495dd591"
integrity sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==
"@esbuild/darwin-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a"
integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==
"@esbuild/darwin-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz#0643e003bb238c63fc93ddbee7d26a003be3cd98"
integrity sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==
"@esbuild/freebsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce"
integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==
"@esbuild/freebsd-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz#cff18da5469c09986b93e87979de5d6872fe8f8e"
integrity sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==
"@esbuild/freebsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7"
integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==
"@esbuild/freebsd-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz#362fc09c2de14987621c1878af19203c46365dde"
integrity sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==
"@esbuild/linux-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73"
integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==
"@esbuild/linux-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz#aa90d5b02efc97a271e124e6d1cea490634f7498"
integrity sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==
"@esbuild/linux-arm@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3"
integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==
"@esbuild/linux-arm@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz#dfcefcbac60a20918b19569b4b657844d39db35a"
integrity sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==
"@esbuild/linux-ia32@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19"
integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==
"@esbuild/linux-ia32@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz#6f9527077ccb7953ed2af02e013d4bac69f13754"
integrity sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==
"@esbuild/linux-loong64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7"
integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==
"@esbuild/linux-loong64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz#287d2412a5456e5860c2839d42a4b51284d1697c"
integrity sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==
"@esbuild/linux-mips64el@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1"
integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==
"@esbuild/linux-mips64el@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz#530574b9e1bc5d20f7a4f44c5f045e26f3783d57"
integrity sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==
"@esbuild/linux-ppc64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951"
integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==
"@esbuild/linux-ppc64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz#5d7e6b283a0b321ea42c6bc0abeb9eb99c1f5589"
integrity sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==
"@esbuild/linux-riscv64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987"
integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==
"@esbuild/linux-riscv64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz#14fa0cd073c26b4ee2465d18cd1e18eea7859fa8"
integrity sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==
"@esbuild/linux-s390x@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4"
integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==
"@esbuild/linux-s390x@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz#e677b4b9d1b384098752266ccaa0d52a420dc1aa"
integrity sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==
"@esbuild/linux-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a"
integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==
"@esbuild/linux-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz#f1c796b78fff5ce393658313e8c58613198d9954"
integrity sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==
"@esbuild/netbsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b"
integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==
"@esbuild/netbsd-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz#0d280b7dfe3973f111b02d5fe9f3063b92796d29"
integrity sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==
"@esbuild/netbsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b"
integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==
"@esbuild/netbsd-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz#be663893931a4bb3f3a009c5cc24fa9681cc71c0"
integrity sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==
"@esbuild/openbsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7"
integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==
"@esbuild/openbsd-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz#d9021b884233673a05dc1cc26de0bf325d824217"
integrity sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==
"@esbuild/openbsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde"
integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==
"@esbuild/openbsd-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz#9f1dc1786ed2e2938c404b06bcc48be9a13250de"
integrity sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==
"@esbuild/sunos-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92"
integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==
"@esbuild/sunos-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz#89aac24a4b4115959b3f790192cf130396696c27"
integrity sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==
"@esbuild/win32-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c"
integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==
"@esbuild/win32-arm64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz#354358647a6ea98ea6d243bf48bdd7a434999582"
integrity sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==
"@esbuild/win32-ia32@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079"
integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==
"@esbuild/win32-ia32@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz#8cea7340f2647eba951a041dc95651e3908cd4cb"
integrity sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==
"@esbuild/win32-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b"
integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==
"@esbuild/win32-x64@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz#7d79922cb2d88f9048f06393dbf62d2e4accb584"
integrity sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==
"@eslint-community/eslint-utils@^4.4.0":
version "4.4.1"
@ -323,10 +323,10 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0"
integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==
"@types/node@*", "@types/node@^22.13.9":
version "22.13.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
"@types/node@*", "@types/node@^22.13.10":
version "22.13.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4"
integrity sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==
dependencies:
undici-types "~6.20.0"
@ -1025,36 +1025,36 @@ es-errors@^1.3.0:
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
esbuild@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
esbuild@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.1.tgz#a16b8d070b6ad4871935277bda6ccfe852e3fa2f"
integrity sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.0"
"@esbuild/android-arm" "0.25.0"
"@esbuild/android-arm64" "0.25.0"
"@esbuild/android-x64" "0.25.0"
"@esbuild/darwin-arm64" "0.25.0"
"@esbuild/darwin-x64" "0.25.0"
"@esbuild/freebsd-arm64" "0.25.0"
"@esbuild/freebsd-x64" "0.25.0"
"@esbuild/linux-arm" "0.25.0"
"@esbuild/linux-arm64" "0.25.0"
"@esbuild/linux-ia32" "0.25.0"
"@esbuild/linux-loong64" "0.25.0"
"@esbuild/linux-mips64el" "0.25.0"
"@esbuild/linux-ppc64" "0.25.0"
"@esbuild/linux-riscv64" "0.25.0"
"@esbuild/linux-s390x" "0.25.0"
"@esbuild/linux-x64" "0.25.0"
"@esbuild/netbsd-arm64" "0.25.0"
"@esbuild/netbsd-x64" "0.25.0"
"@esbuild/openbsd-arm64" "0.25.0"
"@esbuild/openbsd-x64" "0.25.0"
"@esbuild/sunos-x64" "0.25.0"
"@esbuild/win32-arm64" "0.25.0"
"@esbuild/win32-ia32" "0.25.0"
"@esbuild/win32-x64" "0.25.0"
"@esbuild/aix-ppc64" "0.25.1"
"@esbuild/android-arm" "0.25.1"
"@esbuild/android-arm64" "0.25.1"
"@esbuild/android-x64" "0.25.1"
"@esbuild/darwin-arm64" "0.25.1"
"@esbuild/darwin-x64" "0.25.1"
"@esbuild/freebsd-arm64" "0.25.1"
"@esbuild/freebsd-x64" "0.25.1"
"@esbuild/linux-arm" "0.25.1"
"@esbuild/linux-arm64" "0.25.1"
"@esbuild/linux-ia32" "0.25.1"
"@esbuild/linux-loong64" "0.25.1"
"@esbuild/linux-mips64el" "0.25.1"
"@esbuild/linux-ppc64" "0.25.1"
"@esbuild/linux-riscv64" "0.25.1"
"@esbuild/linux-s390x" "0.25.1"
"@esbuild/linux-x64" "0.25.1"
"@esbuild/netbsd-arm64" "0.25.1"
"@esbuild/netbsd-x64" "0.25.1"
"@esbuild/openbsd-arm64" "0.25.1"
"@esbuild/openbsd-x64" "0.25.1"
"@esbuild/sunos-x64" "0.25.1"
"@esbuild/win32-arm64" "0.25.1"
"@esbuild/win32-ia32" "0.25.1"
"@esbuild/win32-x64" "0.25.1"
escalade@^3.1.1:
version "3.1.2"

View File

@ -120,7 +120,7 @@ approx = "0.5"
base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.3.0"
handlebars = "6.3.2"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
kcl-directory-test-macro = { version = "0.1", path = "../kcl-directory-test-macro" }

View File

@ -23,7 +23,9 @@ use crate::{
const TYPES_DIR: &str = "../../docs/kcl/types";
const LANG_TOPICS: [&str; 5] = ["Types", "Modules", "Settings", "Known Issues", "Constants"];
// These types are declared in std.
const DECLARED_TYPES: [&str; 7] = ["number", "string", "tag", "bool", "Sketch", "Solid", "Plane"];
const DECLARED_TYPES: [&str; 11] = [
"number", "string", "tag", "bool", "Sketch", "Solid", "Plane", "Helix", "Face", "Point2d", "Point3d",
];
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
let mut hbs = handlebars::Handlebars::new();
@ -457,6 +459,7 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String)
let data = json!({
"name": ty.qual_name(),
"definition": ty.alias.as_ref().map(|t| format!("type {} = {t}", ty.name)),
"summary": ty.summary,
"description": ty.description,
"deprecated": ty.properties.deprecated,

View File

@ -1,5 +1,6 @@
use std::str::FromStr;
use regex::Regex;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent,
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
@ -282,7 +283,7 @@ impl ConstData {
documentation: self.short_docs().map(|s| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: s,
value: remove_md_links(&s),
})
}),
deprecated: Some(self.properties.deprecated),
@ -335,7 +336,7 @@ impl FnData {
name,
qual_name,
args: expr.params.iter().map(ArgData::from_ast).collect(),
return_type: expr.return_type.as_ref().map(|t| t.recast(&Default::default(), 0)),
return_type: expr.return_type.as_ref().map(|t| t.to_string()),
properties: Properties {
exported: !var.visibility.is_default(),
deprecated: false,
@ -393,7 +394,7 @@ impl FnData {
documentation: self.short_docs().map(|s| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: s,
value: remove_md_links(&s),
})
}),
deprecated: Some(self.properties.deprecated),
@ -496,7 +497,7 @@ impl ArgData {
fn from_ast(arg: &crate::parsing::ast::types::Parameter) -> Self {
ArgData {
name: arg.identifier.name.clone(),
ty: arg.type_.as_ref().map(|t| t.recast(&Default::default(), 0)),
ty: arg.type_.as_ref().map(|t| t.to_string()),
// Doc comments are not yet supported on parameters.
docs: None,
kind: if arg.labeled {
@ -560,6 +561,7 @@ pub struct TyData {
/// The fully qualified name.
pub qual_name: String,
pub properties: Properties,
pub alias: Option<String>,
/// The summary of the function.
pub summary: Option<String>,
@ -583,6 +585,7 @@ impl TyData {
doc_hidden: false,
impl_kind: annotations::Impl::Kcl,
},
alias: ty.alias.as_ref().map(|t| t.to_string()),
summary: None,
description: None,
examples: Vec::new(),
@ -609,13 +612,16 @@ impl TyData {
fn to_completion_item(&self) -> CompletionItem {
CompletionItem {
label: self.name.clone(),
label_details: None,
label_details: self.alias.as_ref().map(|t| CompletionItemLabelDetails {
detail: Some(format!("type {} = {t}", self.name)),
description: None,
}),
kind: Some(CompletionItemKind::FUNCTION),
detail: Some(self.qual_name().to_owned()),
documentation: self.short_docs().map(|s| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: s,
value: remove_md_links(&s),
})
}),
deprecated: Some(self.properties.deprecated),
@ -635,6 +641,11 @@ impl TyData {
}
}
fn remove_md_links(s: &str) -> String {
let re = Regex::new(r"\[([^\]]*)\]\([^\)]*\)").unwrap();
re.replace_all(s, "$1").to_string()
}
trait ApplyMeta {
fn apply_docs(
&mut self,
@ -863,6 +874,24 @@ mod test {
panic!("didn't find PI");
}
#[test]
fn test_remove_md_links() {
assert_eq!(
remove_md_links("sdf dsf sd fj sdk fasdfs. asad[sdfs] dfsdf(dsfs, dsf)"),
"sdf dsf sd fj sdk fasdfs. asad[sdfs] dfsdf(dsfs, dsf)".to_owned()
);
assert_eq!(remove_md_links("[]()"), "".to_owned());
assert_eq!(remove_md_links("[foo](bar)"), "foo".to_owned());
assert_eq!(
remove_md_links("asdasda dsa[foo](http://www.bar/baz/qux.md). asdasdasdas asdas"),
"asdasda dsafoo. asdasdasdas asdas".to_owned()
);
assert_eq!(
remove_md_links("a [foo](bar) b [2](bar) c [_](bar)"),
"a foo b 2 c _".to_owned()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn test_examples() -> miette::Result<()> {
let std = walk_prelude();

View File

@ -18,7 +18,7 @@ use tower_lsp::lsp_types::{
};
use crate::{
execution::{kcl_value::NumericType, Sketch},
execution::{types::NumericType, Sketch},
std::Primitive,
};

View File

@ -10,6 +10,12 @@ layout: manual
{{/if}}
{{{summary}}}
{{#if definition}}
```kcl
{{{definition}}}
```
{{/if}}
{{{description}}}

View File

@ -6,7 +6,7 @@ use kittycad_modeling_cmds::coord::{System, KITTYCAD, OPENGL, VULKAN};
use crate::{
errors::KclErrorDetails,
execution::kcl_value::{UnitAngle, UnitLen},
execution::types::{UnitAngle, UnitLen},
parsing::ast::types::{Annotation, Expr, Node, ObjectProperty},
KclError, SourceRange,
};

View File

@ -2,7 +2,7 @@ use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{kcl_value::NumericType, ArtifactId, KclValue};
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{docs::StdLibFn, std::get_stdlib_fn, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations

View File

@ -8,9 +8,10 @@ use crate::{
execution::{
annotations,
cad_op::{OpArg, OpKclValue, Operation},
kcl_value::{FunctionSource, NumericType, RuntimeType},
kcl_value::FunctionSource,
memory,
state::ModuleState,
types::{NumericType, RuntimeType},
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, TagEngineInfo,
TagIdentifier,
},
@ -29,6 +30,8 @@ use crate::{
CompilationError,
};
use super::kcl_value::TypeDef;
enum StatementKind<'a> {
Declaration { name: &'a str },
Expression,
@ -304,8 +307,9 @@ impl ExecutorContext {
}));
}
};
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
let value = KclValue::Type {
value: Some(crate::std::std_ty(std_path, &ty.name.name)),
value: TypeDef::RustRepr(t, props),
meta: vec![metadata],
};
exec_state
@ -324,12 +328,40 @@ impl ExecutorContext {
}
// Do nothing for primitive types, they get special treatment and their declarations are just for documentation.
annotations::Impl::Primitive => {}
annotations::Impl::Kcl => {
return Err(KclError::Semantic(KclErrorDetails {
message: "User-defined types are not yet supported.".to_owned(),
source_ranges: vec![metadata.source_range],
}));
}
annotations::Impl::Kcl => match &ty.alias {
Some(alias) => {
let value = KclValue::Type {
value: TypeDef::Alias(
RuntimeType::from_parsed(
alias.inner.clone(),
exec_state,
metadata.source_range,
)
.map_err(|e| KclError::Semantic(e.into()))?,
),
meta: vec![metadata],
};
exec_state
.mut_stack()
.add(
format!("{}{}", memory::TYPE_PREFIX, ty.name.name),
value,
metadata.source_range,
)
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!("Redefinition of type {}.", ty.name.name),
source_ranges: vec![metadata.source_range],
})
})?;
}
None => {
return Err(KclError::Semantic(KclErrorDetails {
message: "User-defined types are not yet supported.".to_owned(),
source_ranges: vec![metadata.source_range],
}))
}
},
}
last_expr = None;
@ -646,30 +678,28 @@ impl ExecutorContext {
let result = self
.execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
.await?;
coerce(&result, &expr.ty, exec_state).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"could not coerce {} value to type {}",
result.human_friendly_type(),
expr.ty
),
source_ranges: vec![expr.into()],
})
})?
coerce(&result, &expr.ty, exec_state, expr.into())?
}
};
Ok(item)
}
}
fn coerce(value: &KclValue, ty: &Node<Type>, exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce(
value: &KclValue,
ty: &Node<Type>,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<KclValue, KclError> {
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| {
exec_state.err(e);
})
.ok()??;
.map_err(|e| KclError::Semantic(e.into()))?;
value.coerce(&ty, exec_state)
value.coerce(&ty, exec_state).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("could not coerce {} value to type {}", value.human_friendly_type(), ty),
source_ranges: vec![source_range],
})
})
}
impl BinaryPart {

View File

@ -104,7 +104,7 @@ impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
.into_iter()
.map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
.collect(),
ty: crate::execution::PrimitiveType::Solid,
ty: crate::execution::types::RuntimeType::solid(),
}
}
}
@ -119,7 +119,7 @@ impl From<SolidOrSketchOrImportedGeometry> for crate::execution::KclValue {
.into_iter()
.map(|s| crate::execution::KclValue::Sketch { value: Box::new(s) })
.collect(),
ty: crate::execution::PrimitiveType::Sketch,
ty: crate::execution::types::RuntimeType::sketch(),
}
}
}

View File

@ -17,7 +17,7 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{annotations, kcl_value::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
execution::{annotations, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
fs::FileSystem,
parsing::ast::types::{Annotation, Node},
source_range::SourceRange,

View File

@ -1,29 +1,20 @@
use std::{collections::HashMap, fmt};
use std::collections::HashMap;
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use super::{
memory::{self, EnvironmentRef},
MetaSettings, Point3d,
};
use super::{memory::EnvironmentRef, MetaSettings};
use crate::{
errors::KclErrorDetails,
execution::{
types::{NumericType, PrimitiveType, RuntimeType},
ExecState, ExecutorContext, Face, Helix, ImportedGeometry, Metadata, Plane, Sketch, Solid, TagIdentifier,
},
parsing::{
ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node,
PrimitiveType as AstPrimitiveType, TagDeclarator, TagNode, Type,
},
token::NumericSuffix,
},
std::{
args::{Arg, FromKclValue},
StdFnProps,
parsing::ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
},
std::{args::Arg, StdFnProps},
CompilationError, KclError, ModuleId, SourceRange,
};
@ -65,7 +56,7 @@ pub enum KclValue {
value: Vec<KclValue>,
// The type of values, not the array type.
#[serde(skip)]
ty: PrimitiveType,
ty: RuntimeType,
},
Object {
value: KclObjectFields,
@ -105,7 +96,7 @@ pub enum KclValue {
#[ts(skip)]
Type {
#[serde(skip)]
value: Option<(PrimitiveType, StdFnProps)>,
value: TypeDef,
#[serde(skip)]
meta: Vec<Metadata>,
},
@ -142,6 +133,12 @@ impl JsonSchema for FunctionSource {
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeDef {
RustRepr(PrimitiveType, StdFnProps),
Alias(RuntimeType),
}
impl From<Vec<Sketch>> for KclValue {
fn from(mut eg: Vec<Sketch>) -> Self {
if eg.len() == 1 {
@ -154,7 +151,7 @@ impl From<Vec<Sketch>> for KclValue {
.into_iter()
.map(|s| KclValue::Sketch { value: Box::new(s) })
.collect(),
ty: crate::execution::PrimitiveType::Sketch,
ty: RuntimeType::Primitive(PrimitiveType::Sketch),
}
}
}
@ -169,7 +166,7 @@ impl From<Vec<Solid>> for KclValue {
} else {
KclValue::HomArray {
value: eg.into_iter().map(|s| KclValue::Solid { value: Box::new(s) }).collect(),
ty: crate::execution::PrimitiveType::Solid,
ty: RuntimeType::Primitive(PrimitiveType::Solid),
}
}
}
@ -553,259 +550,6 @@ impl KclValue {
Ok(*b)
}
/// True if `self` has a type which is a subtype of `ty` without coercion.
pub fn has_type(&self, ty: &RuntimeType) -> bool {
let Some(self_ty) = self.principal_type() else {
return false;
};
self_ty.subtype(ty)
}
/// Coerce `self` to a new value which has `ty` as it's closest supertype.
///
/// If the result is Some, then:
/// - result.principal_type().unwrap().subtype(ty)
///
/// If self.principal_type() == ty then result == self
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> {
match ty {
RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state),
RuntimeType::Tuple(tys) => self.coerce_to_tuple_type(tys, exec_state),
RuntimeType::Union(tys) => self.coerce_to_union_type(tys, exec_state),
RuntimeType::Object(tys) => self.coerce_to_object_type(tys, exec_state),
}
}
fn coerce_to_primitive_type(&self, ty: &PrimitiveType, exec_state: &mut ExecState) -> Option<KclValue> {
let value = match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } if value.len() == 1 => &value[0],
_ => self,
};
match ty {
// TODO numeric type coercions
PrimitiveType::Number(_ty) => match value {
KclValue::Number { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::String => match value {
KclValue::String { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Boolean => match value {
KclValue::Bool { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Sketch => match value {
KclValue::Sketch { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Solid => match value {
KclValue::Solid { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Plane => match value {
KclValue::Plane { .. } => Some(value.clone()),
KclValue::Object { value, meta } => {
let origin = value.get("origin").and_then(Point3d::from_kcl_val)?;
let x_axis = value.get("xAxis").and_then(Point3d::from_kcl_val)?;
let y_axis = value.get("yAxis").and_then(Point3d::from_kcl_val)?;
let z_axis = value.get("zAxis").and_then(Point3d::from_kcl_val)?;
let id = exec_state.mod_local.id_generator.next_uuid();
let plane = Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: super::PlaneType::Uninit,
// TODO use length unit from origin
units: exec_state.length_unit(),
meta: meta.clone(),
};
Some(KclValue::Plane { value: Box::new(plane) })
}
_ => None,
},
PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Some(value.clone()),
_ => None,
},
}
}
fn coerce_to_array_type(&self, ty: &PrimitiveType, len: ArrayLen, exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::HomArray { value, ty: aty } => {
// TODO could check types of values individually
if aty != ty {
return None;
}
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
Some(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::MixedArray { value, .. } => {
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
let rt = RuntimeType::Primitive(ty.clone());
let value = value
.iter()
.map(|v| v.coerce(&rt, exec_state))
.collect::<Option<Vec<_>>>()?;
Some(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::KclNone { .. } if len.satisfied(0) => Some(KclValue::HomArray {
value: Vec::new(),
ty: ty.clone(),
}),
value if len.satisfied(1) => {
if value.has_type(&RuntimeType::Primitive(ty.clone())) {
Some(KclValue::HomArray {
value: vec![value.clone()],
ty: ty.clone(),
})
} else {
None
}
}
_ => None,
}
}
fn coerce_to_tuple_type(&self, tys: &[PrimitiveType], exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } => {
if value.len() < tys.len() {
return None;
}
let mut result = Vec::new();
for (i, t) in tys.iter().enumerate() {
result.push(value[i].coerce_to_primitive_type(t, exec_state)?);
}
Some(KclValue::MixedArray {
value: result,
meta: Vec::new(),
})
}
KclValue::KclNone { meta, .. } if tys.is_empty() => Some(KclValue::MixedArray {
value: Vec::new(),
meta: meta.clone(),
}),
value if tys.len() == 1 => {
if value.has_type(&RuntimeType::Primitive(tys[0].clone())) {
Some(KclValue::MixedArray {
value: vec![value.clone()],
meta: Vec::new(),
})
} else {
None
}
}
_ => None,
}
}
fn coerce_to_union_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Option<KclValue> {
for t in tys {
if let Some(v) = self.coerce(t, exec_state) {
return Some(v);
}
}
None
}
fn coerce_to_object_type(&self, tys: &[(String, RuntimeType)], _exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::Object { value, .. } => {
for (s, t) in tys {
// TODO coerce fields
if !value.get(s)?.has_type(t) {
return None;
}
}
// TODO remove non-required fields
Some(self.clone())
}
_ => None,
}
}
pub fn principal_type(&self) -> Option<RuntimeType> {
match self {
KclValue::Bool { .. } => Some(RuntimeType::Primitive(PrimitiveType::Boolean)),
KclValue::Number { ty, .. } => Some(RuntimeType::Primitive(PrimitiveType::Number(ty.clone()))),
KclValue::String { .. } => Some(RuntimeType::Primitive(PrimitiveType::String)),
KclValue::Object { value, .. } => {
let properties = value
.iter()
.map(|(k, v)| v.principal_type().map(|t| (k.clone(), t)))
.collect::<Option<Vec<_>>>()?;
Some(RuntimeType::Object(properties))
}
KclValue::Plane { .. } => Some(RuntimeType::Primitive(PrimitiveType::Plane)),
KclValue::Sketch { .. } => Some(RuntimeType::Primitive(PrimitiveType::Sketch)),
KclValue::Solid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Solid)),
KclValue::ImportedGeometry(..) => Some(RuntimeType::Primitive(PrimitiveType::ImportedGeometry)),
KclValue::MixedArray { value, .. } => Some(RuntimeType::Tuple(
value
.iter()
.map(|v| v.principal_type().and_then(RuntimeType::primitive))
.collect::<Option<Vec<_>>>()?,
)),
KclValue::HomArray { ty, value, .. } => Some(RuntimeType::Array(ty.clone(), ArrayLen::Known(value.len()))),
KclValue::Face { .. } => None,
KclValue::Helix { .. }
| KclValue::Function { .. }
| KclValue::Module { .. }
| KclValue::TagIdentifier(_)
| KclValue::TagDeclarator(_)
| KclValue::KclNone { .. }
| KclValue::Type { .. }
| KclValue::Uuid { .. } => None,
}
}
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
/// If it's not a function, return Err.
pub async fn call_fn(
@ -919,447 +663,3 @@ impl KclValue {
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeType {
Primitive(PrimitiveType),
Array(PrimitiveType, ArrayLen),
Union(Vec<RuntimeType>),
Tuple(Vec<PrimitiveType>),
Object(Vec<(String, RuntimeType)>),
}
impl RuntimeType {
pub fn from_parsed(
value: Type,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<Option<Self>, CompilationError> {
Ok(match value {
Type::Primitive(pt) => {
PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(RuntimeType::Primitive)
}
Type::Array(pt) => {
PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(|t| RuntimeType::Array(t, ArrayLen::None))
}
Type::Object { properties } => properties
.into_iter()
.map(|p| {
let pt = match p.type_ {
Some(t) => t,
None => return Ok(None),
};
Ok(RuntimeType::from_parsed(pt.inner, exec_state, source_range)?
.map(|ty| (p.identifier.inner.name, ty)))
})
.collect::<Result<Option<Vec<_>>, CompilationError>>()?
.map(RuntimeType::Object),
})
}
pub fn human_friendly_type(&self) -> String {
match self {
RuntimeType::Primitive(ty) => ty.to_string(),
RuntimeType::Array(ty, ArrayLen::None) => format!("an array of {}", ty.display_multiple()),
RuntimeType::Array(ty, ArrayLen::NonEmpty) => format!("one or more {}", ty.display_multiple()),
RuntimeType::Array(ty, ArrayLen::Known(n)) => format!("an array of {n} {}", ty.display_multiple()),
RuntimeType::Union(tys) => tys
.iter()
.map(Self::human_friendly_type)
.collect::<Vec<_>>()
.join(" or "),
RuntimeType::Tuple(tys) => format!(
"an array with values of types ({})",
tys.iter().map(PrimitiveType::to_string).collect::<Vec<_>>().join(", ")
),
RuntimeType::Object(_) => format!("an object with fields {}", self),
}
}
// Subtype with no coercion, including refining numeric types.
fn subtype(&self, sup: &RuntimeType) -> bool {
use RuntimeType::*;
match (self, sup) {
(Primitive(t1), Primitive(t2)) => t1 == t2,
// TODO arrays could be covariant
(Array(t1, l1), Array(t2, l2)) => t1 == t2 && l1.subtype(*l2),
(Tuple(t1), Tuple(t2)) => t1 == t2,
(Tuple(t1), Array(t2, l2)) => (l2.satisfied(t1.len())) && t1.iter().all(|t| t == t2),
(Union(ts1), Union(ts2)) => ts1.iter().all(|t| ts2.contains(t)),
(t1, Union(ts2)) => ts2.contains(t1),
// TODO record subtyping - subtype can be larger, fields can be covariant.
(Object(t1), Object(t2)) => t1 == t2,
_ => false,
}
}
fn primitive(self) -> Option<PrimitiveType> {
match self {
RuntimeType::Primitive(t) => Some(t),
_ => None,
}
}
}
impl fmt::Display for RuntimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RuntimeType::Primitive(t) => t.fmt(f),
RuntimeType::Array(t, l) => match l {
ArrayLen::None => write!(f, "[{t}]"),
ArrayLen::NonEmpty => write!(f, "[{t}; 1+]"),
ArrayLen::Known(n) => write!(f, "[{t}; {n}]"),
},
RuntimeType::Tuple(ts) => write!(
f,
"[{}]",
ts.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(", ")
),
RuntimeType::Union(ts) => write!(
f,
"{}",
ts.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" | ")
),
RuntimeType::Object(items) => write!(
f,
"{{ {} }}",
items
.iter()
.map(|(n, t)| format!("{n}: {t}"))
.collect::<Vec<_>>()
.join(", ")
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArrayLen {
None,
NonEmpty,
Known(usize),
}
impl ArrayLen {
pub fn subtype(self, other: ArrayLen) -> bool {
match (self, other) {
(_, ArrayLen::None) => true,
(ArrayLen::NonEmpty, ArrayLen::NonEmpty) => true,
(ArrayLen::Known(size), ArrayLen::NonEmpty) if size > 0 => true,
(ArrayLen::Known(s1), ArrayLen::Known(s2)) if s1 == s2 => true,
_ => false,
}
}
/// True if the length constraint is satisfied by the supplied length.
fn satisfied(self, len: usize) -> bool {
match self {
ArrayLen::None => true,
ArrayLen::NonEmpty => len > 0,
ArrayLen::Known(s) => len == s,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PrimitiveType {
Number(NumericType),
String,
Boolean,
Sketch,
Solid,
Plane,
ImportedGeometry,
}
impl PrimitiveType {
fn from_parsed(
value: AstPrimitiveType,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<Option<Self>, CompilationError> {
Ok(match value {
AstPrimitiveType::String => Some(PrimitiveType::String),
AstPrimitiveType::Boolean => Some(PrimitiveType::Boolean),
AstPrimitiveType::Number(suffix) => Some(PrimitiveType::Number(NumericType::from_parsed(
suffix,
&exec_state.mod_local.settings,
))),
AstPrimitiveType::Named(name) => {
let ty_val = exec_state
.stack()
.get(&format!("{}{}", memory::TYPE_PREFIX, name.name), source_range)
.map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", name.name)))?;
let (ty, _) = match ty_val {
KclValue::Type { value: Some(ty), .. } => ty,
_ => unreachable!(),
};
Some(ty.clone())
}
_ => None,
})
}
fn display_multiple(&self) -> String {
match self {
PrimitiveType::Number(NumericType::Known(unit)) => format!("numbers({unit})"),
PrimitiveType::Number(_) => "numbers".to_owned(),
PrimitiveType::String => "strings".to_owned(),
PrimitiveType::Boolean => "bools".to_owned(),
PrimitiveType::Sketch => "Sketches".to_owned(),
PrimitiveType::Solid => "Solids".to_owned(),
PrimitiveType::Plane => "Planes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
}
}
}
impl fmt::Display for PrimitiveType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
PrimitiveType::Number(_) => write!(f, "number"),
PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"),
PrimitiveType::Sketch => write!(f, "Sketch"),
PrimitiveType::Solid => write!(f, "Solid"),
PrimitiveType::Plane => write!(f, "Plane"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum NumericType {
// Specified by the user (directly or indirectly)
Known(UnitType),
// Unspecified, using defaults
Default { len: UnitLen, angle: UnitAngle },
// Exceeded the ability of the type system to track.
Unknown,
// Type info has been explicitly cast away.
Any,
}
impl NumericType {
pub fn count() -> Self {
NumericType::Known(UnitType::Count)
}
/// Combine two types when we expect them to be equal.
pub fn combine_eq(self, other: &NumericType) -> NumericType {
if &self == other {
self
} else {
NumericType::Unknown
}
}
/// Combine n types when we expect them to be equal.
///
/// Precondition: tys.len() > 0
pub fn combine_n_eq(tys: &[NumericType]) -> NumericType {
let ty0 = tys[0].clone();
for t in &tys[1..] {
if t != &ty0 {
return NumericType::Unknown;
}
}
ty0
}
/// Combine two types in addition-like operations.
pub fn combine_add(a: NumericType, b: NumericType) -> NumericType {
if a == b {
return a;
}
NumericType::Unknown
}
/// Combine two types in multiplication-like operations.
pub fn combine_mul(a: NumericType, b: NumericType) -> NumericType {
if a == NumericType::count() {
return b;
}
if b == NumericType::count() {
return a;
}
NumericType::Unknown
}
/// Combine two types in division-like operations.
pub fn combine_div(a: NumericType, b: NumericType) -> NumericType {
if b == NumericType::count() {
return a;
}
NumericType::Unknown
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix {
NumericSuffix::None => NumericType::Default {
len: settings.default_length_units,
angle: settings.default_angle_units,
},
NumericSuffix::Count => NumericType::Known(UnitType::Count),
NumericSuffix::Mm => NumericType::Known(UnitType::Length(UnitLen::Mm)),
NumericSuffix::Cm => NumericType::Known(UnitType::Length(UnitLen::Cm)),
NumericSuffix::M => NumericType::Known(UnitType::Length(UnitLen::M)),
NumericSuffix::Inch => NumericType::Known(UnitType::Length(UnitLen::Inches)),
NumericSuffix::Ft => NumericType::Known(UnitType::Length(UnitLen::Feet)),
NumericSuffix::Yd => NumericType::Known(UnitType::Length(UnitLen::Yards)),
NumericSuffix::Deg => NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
NumericSuffix::Rad => NumericType::Known(UnitType::Angle(UnitAngle::Radians)),
}
}
}
impl From<UnitLen> for NumericType {
fn from(value: UnitLen) -> Self {
NumericType::Known(UnitType::Length(value))
}
}
impl From<UnitAngle> for NumericType {
fn from(value: UnitAngle) -> Self {
NumericType::Known(UnitType::Angle(value))
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitType {
Count,
Length(UnitLen),
Angle(UnitAngle),
}
impl std::fmt::Display for UnitType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitType::Count => write!(f, "_"),
UnitType::Length(l) => l.fmt(f),
UnitType::Angle(a) => a.fmt(f),
}
}
}
// 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)]
#[serde(tag = "type")]
pub enum UnitLen {
#[default]
Mm,
Cm,
M,
Inches,
Feet,
Yards,
}
impl std::fmt::Display for UnitLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitLen::Mm => write!(f, "mm"),
UnitLen::Cm => write!(f, "cm"),
UnitLen::M => write!(f, "m"),
UnitLen::Inches => write!(f, "in"),
UnitLen::Feet => write!(f, "ft"),
UnitLen::Yards => write!(f, "yd"),
}
}
}
impl TryFrom<NumericSuffix> for UnitLen {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Mm => Ok(Self::Mm),
NumericSuffix::Cm => Ok(Self::Cm),
NumericSuffix::M => Ok(Self::M),
NumericSuffix::Inch => Ok(Self::Inches),
NumericSuffix::Ft => Ok(Self::Feet),
NumericSuffix::Yd => Ok(Self::Yards),
_ => Err(()),
}
}
}
impl From<crate::UnitLength> for UnitLen {
fn from(unit: crate::UnitLength) -> Self {
match unit {
crate::UnitLength::Cm => UnitLen::Cm,
crate::UnitLength::Ft => UnitLen::Feet,
crate::UnitLength::In => UnitLen::Inches,
crate::UnitLength::M => UnitLen::M,
crate::UnitLength::Mm => UnitLen::Mm,
crate::UnitLength::Yd => UnitLen::Yards,
}
}
}
impl From<UnitLen> for crate::UnitLength {
fn from(unit: UnitLen) -> Self {
match unit {
UnitLen::Cm => crate::UnitLength::Cm,
UnitLen::Feet => crate::UnitLength::Ft,
UnitLen::Inches => crate::UnitLength::In,
UnitLen::M => crate::UnitLength::M,
UnitLen::Mm => crate::UnitLength::Mm,
UnitLen::Yards => crate::UnitLength::Yd,
}
}
}
impl From<UnitLen> for kittycad_modeling_cmds::units::UnitLength {
fn from(unit: UnitLen) -> Self {
match unit {
UnitLen::Cm => kittycad_modeling_cmds::units::UnitLength::Centimeters,
UnitLen::Feet => kittycad_modeling_cmds::units::UnitLength::Feet,
UnitLen::Inches => kittycad_modeling_cmds::units::UnitLength::Inches,
UnitLen::M => kittycad_modeling_cmds::units::UnitLength::Meters,
UnitLen::Mm => kittycad_modeling_cmds::units::UnitLength::Millimeters,
UnitLen::Yards => kittycad_modeling_cmds::units::UnitLength::Yards,
}
}
}
/// A unit of angle.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitAngle {
#[default]
Degrees,
Radians,
}
impl std::fmt::Display for UnitAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitAngle::Degrees => write!(f, "deg"),
UnitAngle::Radians => write!(f, "rad"),
}
}
}
impl TryFrom<NumericSuffix> for UnitAngle {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Deg => Ok(Self::Degrees),
NumericSuffix::Rad => Ok(Self::Radians),
_ => Err(()),
}
}
}

View File

@ -1055,7 +1055,7 @@ mod env {
#[cfg(test)]
mod test {
use super::*;
use crate::execution::kcl_value::{FunctionSource, NumericType};
use crate::execution::{kcl_value::FunctionSource, types::NumericType};
fn sr() -> SourceRange {
SourceRange::default()

View File

@ -15,7 +15,7 @@ pub(crate) use import::{
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
};
use indexmap::IndexMap;
pub use kcl_value::{KclObjectFields, KclValue, PrimitiveType, UnitAngle, UnitLen};
pub use kcl_value::{KclObjectFields, KclValue};
use kcmc::{
each_cmd as mcmd,
ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
@ -34,6 +34,7 @@ use crate::{
execution::{
artifact::build_artifact_graph,
cache::{CacheInformation, CacheResult},
types::{UnitAngle, UnitLen},
},
fs::FileManager,
modules::{ModuleId, ModulePath},
@ -55,6 +56,7 @@ mod import;
pub(crate) mod kcl_value;
mod memory;
mod state;
pub(crate) mod types;
/// Outcome of executing a program. This is used in TS.
#[derive(Debug, Clone, Serialize, ts_rs::TS)]
@ -1397,6 +1399,22 @@ const answer = returnX()"#;
assert!(errs.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn type_aliases() {
let text = r#"type MyTy = [number; 2]
fn foo(x: MyTy) {
return x[0]
}
foo([0, 1])
type Other = MyTy | Helix
"#;
let result = parse_execute(text).await.unwrap();
let errs = result.exec_state.errors();
assert!(errs.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cannot_shebang_in_fn() {
let ast = r#"

View File

@ -12,10 +12,9 @@ use crate::{
execution::{
annotations,
id_generator::IdGenerator,
kcl_value,
memory::{ProgramMemory, Stack},
Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue,
Operation, UnitAngle, UnitLen,
types, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, EnvironmentRef, ExecOutcome, ExecutorSettings,
KclValue, Operation, UnitAngle, UnitLen,
},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
parsing::ast::types::Annotation,
@ -310,8 +309,8 @@ impl ModuleState {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MetaSettings {
pub default_length_units: kcl_value::UnitLen,
pub default_angle_units: kcl_value::UnitAngle,
pub default_length_units: types::UnitLen,
pub default_angle_units: types::UnitAngle,
pub std_path: Option<String>,
}
@ -326,12 +325,12 @@ impl MetaSettings {
match &*p.inner.key.name {
annotations::SETTINGS_UNIT_LENGTH => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitLen::from_str(value, annotation.as_source_range())?;
let value = types::UnitLen::from_str(value, annotation.as_source_range())?;
self.default_length_units = value;
}
annotations::SETTINGS_UNIT_ANGLE => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitAngle::from_str(value, annotation.as_source_range())?;
let value = types::UnitAngle::from_str(value, annotation.as_source_range())?;
self.default_angle_units = value;
}
name => {

File diff suppressed because it is too large Load Diff

View File

@ -344,8 +344,8 @@ impl Node<Type> {
let range = self.as_source_range();
if range.contains(pos) {
match &self.inner {
Type::Array(t) | Type::Primitive(t) => {
let mut name = t.to_string();
Type::Array { ty, .. } | Type::Primitive(ty) => {
let mut name = ty.to_string();
if name.ends_with(')') {
name.truncate(name.find('(').unwrap());
}
@ -379,7 +379,7 @@ impl FunctionExpression {
if let Some(value) = self.body.get_expr_for_position(pos) {
let mut vars = opts.vars.clone().unwrap_or_default();
for arg in &self.params {
let ty = arg.type_.as_ref().map(|ty| ty.recast(&FormatOptions::default(), 0));
let ty = arg.type_.as_ref().map(|ty| ty.to_string());
vars.insert(arg.identifier.inner.name.clone(), ty);
}
return value.get_hover_value_for_position(

View File

@ -1900,7 +1900,7 @@ async fn test_kcl_lsp_diagnostic_has_errors() {
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
assert_eq!(
diagnostics.full_document_diagnostic_report.items[0].message,
"lexical: found unknown token ';'"
"Unexpected token: ;"
);
} else {
panic!("Expected full diagnostics");

View File

@ -194,9 +194,21 @@ impl Type {
hasher.update(b"FnArgType::Primitive");
hasher.update(prim.compute_digest())
}
Type::Array(prim) => {
Type::Array { ty, len } => {
hasher.update(b"FnArgType::Array");
hasher.update(prim.compute_digest())
hasher.update(ty.compute_digest());
match len {
crate::execution::types::ArrayLen::None => {}
crate::execution::types::ArrayLen::NonEmpty => hasher.update(usize::MAX.to_ne_bytes()),
crate::execution::types::ArrayLen::Known(n) => hasher.update(n.to_ne_bytes()),
}
}
Type::Union { tys } => {
hasher.update(b"FnArgType::Union");
hasher.update(tys.len().to_ne_bytes());
for t in tys.iter_mut() {
hasher.update(t.compute_digest());
}
}
Type::Object { properties } => {
hasher.update(b"FnArgType::Object");
@ -300,6 +312,9 @@ impl TypeDeclaration {
hasher.update(a.compute_digest());
}
}
if let Some(alias) = &mut slf.alias {
hasher.update(alias.compute_digest());
}
});
}

View File

@ -25,7 +25,7 @@ pub use crate::parsing::ast::types::{
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{annotations, KclValue, Metadata, TagIdentifier},
execution::{annotations, types::ArrayLen, KclValue, Metadata, TagIdentifier},
parsing::{ast::digest::Digest, token::NumericSuffix, PIPE_OPERATOR},
source_range::SourceRange,
ModuleId,
@ -150,7 +150,7 @@ impl<T> Node<T> {
self.start <= pos && pos <= self.end
}
pub fn map<U>(self, f: fn(T) -> U) -> Node<U> {
pub fn map<U>(self, f: impl Fn(T) -> U) -> Node<U> {
Node {
inner: f(self.inner),
start: self.start,
@ -1895,6 +1895,7 @@ pub struct TypeDeclaration {
pub args: Option<NodeList<Identifier>>,
#[serde(default, skip_serializing_if = "ItemVisibility::is_default")]
pub visibility: ItemVisibility,
pub alias: Option<Node<Type>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@ -3024,7 +3025,14 @@ pub enum Type {
/// A primitive type.
Primitive(PrimitiveType),
// An array of a primitive type.
Array(PrimitiveType),
Array {
ty: PrimitiveType,
len: ArrayLen,
},
// Union/enum types
Union {
tys: NodeList<PrimitiveType>,
},
// An object type.
Object {
properties: Vec<Parameter>,
@ -3035,7 +3043,22 @@ impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Primitive(primitive_type) => primitive_type.fmt(f),
Type::Array(primitive_type) => write!(f, "[{primitive_type}]"),
Type::Array { ty, len } => {
write!(f, "[{ty}")?;
match len {
ArrayLen::None => {}
ArrayLen::NonEmpty => write!(f, "; 1+")?,
ArrayLen::Known(n) => write!(f, "; {n}")?,
}
write!(f, "]")
}
Type::Union { tys } => {
write!(
f,
"{}",
tys.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" | ")
)
}
Type::Object { properties } => {
write!(f, "{{")?;
let mut first = true;
@ -3624,11 +3647,17 @@ const cylinder = startSketchOn('-XZ')
assert_eq!(params.len(), 3);
assert_eq!(
params[0].type_.as_ref().unwrap().inner,
Type::Array(PrimitiveType::Number(NumericSuffix::None))
Type::Array {
ty: PrimitiveType::Number(NumericSuffix::None),
len: ArrayLen::None
}
);
assert_eq!(
params[1].type_.as_ref().unwrap().inner,
Type::Array(PrimitiveType::String)
Type::Array {
ty: PrimitiveType::String,
len: ArrayLen::None
}
);
assert_eq!(
params[2].type_.as_ref().unwrap().inner,
@ -3656,7 +3685,10 @@ const cylinder = startSketchOn('-XZ')
assert_eq!(params.len(), 3);
assert_eq!(
params[0].type_.as_ref().unwrap().inner,
Type::Array(PrimitiveType::Number(NumericSuffix::None))
Type::Array {
ty: PrimitiveType::Number(NumericSuffix::None),
len: ArrayLen::None
}
);
assert_eq!(
params[1].type_.as_ref().unwrap().inner,
@ -3692,7 +3724,15 @@ const cylinder = startSketchOn('-XZ')
56,
module_id,
),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 59, 65, module_id)),
type_: Some(Node::new(
Type::Array {
ty: PrimitiveType::String,
len: ArrayLen::None
},
59,
65,
module_id
)),
default_value: None,
labeled: true,
digest: None
@ -3773,7 +3813,15 @@ const cylinder = startSketchOn('-XZ')
34,
module_id,
),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 37, 43, module_id)),
type_: Some(Node::new(
Type::Array {
ty: PrimitiveType::String,
len: ArrayLen::None
},
37,
43,
module_id
)),
default_value: None,
labeled: true,
digest: None
@ -3946,7 +3994,7 @@ startSketchOn('XY')"#;
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
crate::execution::types::UnitLen::Inches
);
}
@ -3962,13 +4010,13 @@ startSketchOn('XY')"#;
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
crate::execution::types::UnitLen::Inches
);
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
default_length_units: crate::execution::types::UnitLen::Mm,
..Default::default()
})
.unwrap();
@ -3977,10 +4025,7 @@ startSketchOn('XY')"#;
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
assert_eq!(meta_settings.default_length_units, crate::execution::types::UnitLen::Mm);
let formatted = new_program.recast(&Default::default(), 0);
@ -4003,7 +4048,7 @@ startSketchOn('XY')
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
default_length_units: crate::execution::types::UnitLen::Mm,
..Default::default()
})
.unwrap();
@ -4012,10 +4057,7 @@ startSketchOn('XY')
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
assert_eq!(meta_settings.default_length_units, crate::execution::types::UnitLen::Mm);
let formatted = new_program.recast(&Default::default(), 0);

View File

@ -19,6 +19,7 @@ use super::{
use crate::{
docs::StdLibFn,
errors::{CompilationError, Severity, Tag},
execution::types::ArrayLen,
parsing::{
ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
@ -2215,7 +2216,7 @@ fn ty_decl(i: &mut TokenSlice) -> PResult<BoxNode<TypeDeclaration>> {
let name = identifier(i)?;
let mut end = name.end;
let args = if peek(open_paren).parse_next(i).is_ok() {
let args = if peek((opt(whitespace), open_paren)).parse_next(i).is_ok() {
ignore_whitespace(i);
open_paren(i)?;
ignore_whitespace(i);
@ -2228,11 +2229,28 @@ fn ty_decl(i: &mut TokenSlice) -> PResult<BoxNode<TypeDeclaration>> {
None
};
let alias = if peek((opt(whitespace), equals)).parse_next(i).is_ok() {
ignore_whitespace(i);
equals(i)?;
ignore_whitespace(i);
let ty = argument_type(i)?;
ParseContext::warn(CompilationError::err(
ty.as_source_range(),
"Type aliases are experimental, likely to change in the future, and likely to not work properly.",
));
Some(ty)
} else {
None
};
let module_id = name.module_id;
let result = Node::boxed(
TypeDeclaration {
name,
args,
alias,
visibility,
digest: None,
},
@ -2336,21 +2354,7 @@ impl TryFrom<Token> for Node<TagDeclarator> {
format!("Cannot assign a tag to a reserved keyword: {}", token.value.as_str()),
)),
TokenType::Bang
| TokenType::At
| TokenType::Hash
| TokenType::Colon
| TokenType::Period
| TokenType::Operator
| TokenType::DoublePeriod
| TokenType::QuestionMark
| TokenType::BlockComment
| TokenType::Function
| TokenType::String
| TokenType::Dollar
| TokenType::Keyword
| TokenType::Unknown
| TokenType::LineComment => Err(CompilationError::fatal(
_ => Err(CompilationError::fatal(
token.as_source_range(),
// this is `start with` because if most of these cases are in the middle, it ends
// up hitting a different error path(e.g. including a bang) or being valid(e.g. including a comment) since it will get broken up into
@ -2617,6 +2621,14 @@ fn colon(i: &mut TokenSlice) -> PResult<Token> {
TokenType::Colon.parse_from(i)
}
fn semi_colon(i: &mut TokenSlice) -> PResult<Token> {
TokenType::SemiColon.parse_from(i)
}
fn plus(i: &mut TokenSlice) -> PResult<Token> {
one_of((TokenType::Operator, "+")).parse_next(i)
}
fn equals(i: &mut TokenSlice) -> PResult<Token> {
one_of((TokenType::Operator, "="))
.context(expected("the equals operator, ="))
@ -2659,6 +2671,12 @@ fn comma_sep(i: &mut TokenSlice) -> PResult<()> {
Ok(())
}
/// Parse a `|`, optionally followed by some whitespace.
fn pipe_sep(i: &mut TokenSlice) -> PResult<()> {
(opt(whitespace), one_of((TokenType::Operator, "|")), opt(whitespace)).parse_next(i)?;
Ok(())
}
/// Arguments are passed into a function.
fn arguments(i: &mut TokenSlice) -> PResult<Vec<Expr>> {
separated(0.., expression, comma_sep)
@ -2685,7 +2703,15 @@ fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
let type_ = alt((
// Object types
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
(open_brace, parameters, close_brace).map(|(open, params, close)| {
(open_brace, parameters, close_brace).try_map(|(open, params, close)| {
for p in &params {
if p.type_.is_none() {
return Err(CompilationError::fatal(
p.identifier.as_source_range(),
"Missing type for field in record type",
));
}
}
Ok(Node::new(
Type::Object { properties: params },
open.start,
@ -2694,12 +2720,21 @@ fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
))
}),
// Array types
(open_bracket, primitive_type, close_bracket).map(|(_, t, _)| Ok(t.map(Type::Array))),
// Primitive types
primitive_type.map(|t| Ok(t.map(Type::Primitive))),
array_type,
// Primitive or union types
separated(1.., primitive_type, pipe_sep).map(|mut tys: Vec<_>| {
if tys.len() == 1 {
tys.pop().unwrap().map(Type::Primitive)
} else {
let start = tys[0].start;
let module_id = tys[0].module_id;
let end = tys.last().unwrap().end;
Node::new(Type::Union { tys }, start, end, module_id)
}
}),
))
.parse_next(i)?
.map_err(|e: CompilationError| ErrMode::Backtrack(ContextError::from(e)))?;
.parse_next(i)?;
Ok(type_)
}
@ -2721,6 +2756,55 @@ fn primitive_type(i: &mut TokenSlice) -> PResult<Node<PrimitiveType>> {
Ok(result)
}
fn array_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
fn opt_whitespace(i: &mut TokenSlice) -> PResult<()> {
ignore_whitespace(i);
Ok(())
}
open_bracket(i)?;
let ty = primitive_type(i)?;
let len = opt((
semi_colon,
opt_whitespace,
any.try_map(|token: Token| match token.token_type {
TokenType::Number => {
let value = token.uint_value().ok_or_else(|| {
CompilationError::fatal(
token.as_source_range(),
format!("Expected unsigned integer literal, found: {}", token.value),
)
})?;
Ok(value as usize)
}
_ => Err(CompilationError::fatal(token.as_source_range(), "invalid array length")),
}),
opt(plus),
))
.parse_next(i)?;
close_bracket(i)?;
let len = if let Some((tok, _, n, plus)) = len {
if plus.is_some() {
if n != 1 {
return Err(ErrMode::Cut(ContextError::from(CompilationError::fatal(
tok.as_source_range(),
"Non-empty arrays are specified using `1+`, for a fixed-size array use just an integer",
))));
} else {
ArrayLen::NonEmpty
}
} else {
ArrayLen::Known(n)
}
} else {
ArrayLen::None
};
Ok(ty.map(|ty| Type::Array { ty, len }))
}
fn uom_for_type(i: &mut TokenSlice) -> PResult<NumericSuffix> {
any.try_map(|t: Token| t.value.parse()).parse_next(i)
}

View File

@ -367,6 +367,8 @@ pub enum TokenType {
QuestionMark,
/// The @ symbol.
At,
/// `;`
SemiColon,
}
/// Most KCL tokens correspond to LSP semantic tokens (but not all).
@ -396,6 +398,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::Hash
| TokenType::Dollar
| TokenType::At
| TokenType::SemiColon
| TokenType::Unknown => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}
@ -488,6 +491,18 @@ impl Token {
value.parse().ok()
}
pub fn uint_value(&self) -> Option<u32> {
if self.token_type != TokenType::Number {
return None;
}
let value = &self.value;
let value = value
.split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
.map(|(s, _)| s)
.unwrap_or(value);
value.parse().ok()
}
pub fn numeric_suffix(&self) -> NumericSuffix {
if self.token_type != TokenType::Number {
return NumericSuffix::None;

View File

@ -88,6 +88,7 @@ pub(super) fn token(i: &mut Input<'_>) -> PResult<Token> {
'@' => at,
'0'..='9' => number,
':' => colon,
';' => semi_colon,
'.' => alt((number, double_period, period)),
'#' => hash,
'$' => dollar,
@ -282,6 +283,16 @@ fn colon(i: &mut Input<'_>) -> PResult<Token> {
))
}
fn semi_colon(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = ';'.with_span().parse_next(i)?;
Ok(Token::from_range(
range,
i.state.module_id,
TokenType::SemiColon,
value.to_string(),
))
}
fn period(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = '.'.with_span().parse_next(i)?;
Ok(Token::from_range(
@ -689,7 +700,7 @@ const things = "things"
#[test]
fn test_unrecognized_token() {
let module_id = ModuleId::from_usize(1);
let actual = lex("12 ; 8", module_id).unwrap();
let actual = lex("12 ~ 8", module_id).unwrap();
use TokenType::*;
assert_tokens(&[(Number, 0, 2), (Unknown, 3, 4), (Number, 5, 6)], actual.as_slice());

View File

@ -12,10 +12,7 @@ use validator::Validate;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, Solid,
},
execution::{types::RuntimeType, ExecState, KclValue, Solid},
std::Args,
};
@ -42,11 +39,7 @@ struct AppearanceData {
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
let color: String = args.get_kw_arg("color")?;
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;

View File

@ -12,9 +12,10 @@ use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{ArrayLen, FunctionSource, NumericType, RuntimeType},
ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata, PrimitiveType, Sketch,
SketchSurface, Solid, TagIdentifier,
kcl_value::FunctionSource,
types::{NumericType, PrimitiveType, RuntimeType},
ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata, Sketch, SketchSurface,
Solid, TagIdentifier,
},
parsing::ast::types::TagNode,
source_range::SourceRange,
@ -188,8 +189,10 @@ impl Args {
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch")
| (RuntimeType::Array(PrimitiveType::Solid, _), "Sketch") => Some(
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Array(t, _), "Sketch") if **t == RuntimeType::Primitive(PrimitiveType::Solid) => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
_ => None,
@ -309,8 +312,10 @@ impl Args {
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch")
| (RuntimeType::Array(PrimitiveType::Solid, _), "Sketch") => Some(
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
(RuntimeType::Array(ty, _), "Sketch") if **ty == RuntimeType::Primitive(PrimitiveType::Solid) => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
_ => None,
@ -597,7 +602,7 @@ impl Args {
};
let sarg = arg0
.value
.coerce(&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::None), exec_state)
.coerce(&RuntimeType::sketches(), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected an array of sketches, found {}",
@ -685,7 +690,7 @@ impl Args {
};
let sarg = arg1
.value
.coerce(&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::None), exec_state)
.coerce(&RuntimeType::sketches(), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected one or more sketches for second argument, found {}",

View File

@ -8,8 +8,8 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, PrimitiveType,
Solid,
types::{PrimitiveType, RuntimeType},
ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid,
},
parsing::ast::types::TagNode,
std::{fillet::EdgeReference, Args},

View File

@ -5,20 +5,14 @@ use kcl_derive_docs::stdlib;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, Solid,
},
execution::{types::RuntimeType, ExecState, KclValue, Solid},
std::Args,
};
/// Union two or more solids into a single solid.
pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed(
"objects",
&RuntimeType::Union(vec![RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty)]),
exec_state,
)?;
let solids: Vec<Solid> =
args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::Union(vec![RuntimeType::solids()]), exec_state)?;
if solids.len() < 2 {
return Err(KclError::UndefinedValue(KclErrorDetails {
@ -74,11 +68,7 @@ async fn inner_union(solids: Vec<Solid>, exec_state: &mut ExecState, args: Args)
/// Intersect returns the shared volume between multiple solids, preserving only
/// overlapping regions.
pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed(
"objects",
&RuntimeType::Union(vec![RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty)]),
exec_state,
)?;
let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
if solids.len() < 2 {
return Err(KclError::UndefinedValue(KclErrorDetails {
@ -139,16 +129,8 @@ async fn inner_intersect(solids: Vec<Solid>, exec_state: &mut ExecState, args: A
/// Subtract removes tool solids from base solids, leaving the remaining material.
pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed(
"objects",
&RuntimeType::Union(vec![RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty)]),
exec_state,
)?;
let tools: Vec<Solid> = args.get_kw_arg_typed(
"tools",
&RuntimeType::Union(vec![RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty)]),
exec_state,
)?;
let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
let tools: Vec<Solid> = args.get_kw_arg_typed("tools", &RuntimeType::solids(), exec_state)?;
let solids = inner_subtract(solids, tools, exec_state, args).await?;
Ok(solids.into())

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