Compare commits

...

49 Commits

Author SHA1 Message Date
5713526e99 Lint again 2025-03-18 12:25:21 -04:00
3612c195eb Revert "Lint"
This reverts commit 53a04d0f24.
2025-03-18 12:19:28 -04:00
53a04d0f24 Lint 2025-03-18 12:10:17 -04:00
089b3397b6 A snapshot a day keeps the bugs away! 📷🐛 2025-03-18 16:01:08 +00:00
77a072cc38 A snapshot a day keeps the bugs away! 📷🐛 2025-03-18 15:51:35 +00:00
3090827cc1 A snapshot a day keeps the bugs away! 📷🐛 2025-03-18 15:41:54 +00:00
3daf6acf4e pierremtb/adhoc/disable-failing-tests 2025-03-18 11:32:26 -04:00
f2c5661710 Use less Namespace resources by shifting ubuntu to AWS (#5847)
* Attempt: Hourly tests on ubuntu only

* Attempt: Hourly tests on ubuntu only through os exlusion rules

* Attempt: Hourly tests on ubuntu only through dummy boolean key

* Clean up for PR

* Draft: Use less namespace vCPUs for e2e

* All on gh, just to see

* Disable concurrency, bump to 45min step retry timeout

* 2 workers for non-windows machineis

* Step retry down to 3

* Change to ubuntu aws, change macos to macos-latest-large

* Fix name for ubuntu aws, back to 10 step retry

* Back to 4 shards to see the diff

* Snaptshots on ubuntu aws

* Cleaning up to get closer to a review-able state

* Should be ready for review

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

* 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-18 09:44:54 -04:00
ecb2359bc5 Fix tsc errors (#5857)
fix tsc errors
2025-03-18 08:56:08 -04:00
6e5058bbdc Bump zip from 2.2.3 to 2.4.1 in /rust in the security group (#5854) 2025-03-18 00:40:56 -04:00
988a068d6d Collect simple stats about engine usage (#5823)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-18 16:19:24 +13:00
0688ce7fe9 #4391 Move axis meshes behind so they don't block line segments from being selected (#5807)
Move axis meshes behind so they will be picked after line segment by raycaster
2025-03-17 22:00:21 -04:00
f9e09893e7 update template (#5853)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-18 01:53:32 +00:00
dd1534a61d Cleanup rust/ts interface a but more w new rustContext (#5848)
* do the rust side

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

* cleanup ts side

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

* updates

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

* typo

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-18 01:26:11 +00:00
e17c6e272c Add 3 point arc (#5722)
* bare minimum

* start of segment util added

* remove redundant handle

* some probably buggy handling of arc handles, can fix later

* probably bug implementation of update args, but data flow through is mostly there can fix bugs after

* fix update for arc

* fix math for center handle

* fix up length indicator

* tweak math

* stub out xState logic for arc

* more progress on adding point and click, implemented more of sketchLineHelper for arc

* small unrelated tweak

* fix up draft arc bugs

* fix arc last click

* fix draft segment animation and add comment

* add draft point snapping for arcs

* add helper stuff to arc

* clone arc point and click as base for arc-three-point

* rust change for arc three point

* can draw three point arc

* make arcTo editable

* can add new three point arc, so long as it continues existing profile

* get overlays working

* make snap to for continuing profile work for three point arcs

* add draft animation

* tangent issue fix

* action rename

* tmp test fix up

* fix silly bug

* fix couple problems causing tests to fail

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

* fix up

* add delet segment test for new segments

* update docs

* draft segments should look right

* add test for dragging new segment handles

* arc tools can be chained now

* make three point arc can start a new profile (not only extend existing paths)

* add test for equiping and unequiping the tool plus drawing with it

* fix console noise

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

* clean up

* update rust/docs

* put toolbar mode check into fixture

* do thing for lee

* use TEST_COLORSs

* fix colors

* don't await file write

* remove commented code

* remove unneeded template strings

* power to **2

* remove magic numbers

* more string templates

* some odd bits of clean up

* arc should be enable in dev

* 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)

* add new simulation test

* fix test code from kwark migration

* issues Frank found

* fix deleting half complete ark

* fix

* small fix on dele index

* tsc post main merge

* fix up snaping to profile start

* add cross hari for three point arc

* block snapping if it's the only segment

* add tests for canceling arcTo halfway through

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-18 11:14:12 +11:00
cb0470a31d Change playwright to always use env vars (#5851)
* Change playwright to always use env vars

* Change to not be so noisy
2025-03-18 00:00:07 +00:00
ff6186f4f0 Format a whole directory easily (#5816)
* recast a whole directory

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

* move

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

* updates

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

* fixes

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

* updates

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

* fixes

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-17 15:55:25 -07:00
4dd669bd46 Hourly e2e tests only on Ubuntu (#5846)
* Attempt: Hourly tests on ubuntu only

* Attempt: Hourly tests on ubuntu only through os exlusion rules

* Attempt: Hourly tests on ubuntu only through dummy boolean key

* Clean up for PR
2025-03-17 17:01:33 -04:00
09131722e3 fix not giving miette error in bindings (#5844)
* try python unit test

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

* try python unit test

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

* updates

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

* updates

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

* updates

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

* updates

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

* fix

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

* updates

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

* fixes;

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

* fixes;

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-17 13:35:49 -07:00
87ab8fe78d Only one playwright worker and less retries. Plus auto-retries on snapshots (#5842)
* 8 shards, 1 workers, 10 retries

* Trigger CI

* Add retries to snapshots

* Clean up for PR, retry to 15

* Trigger CI

* Back to 4 shards for simplicity

* Revert "Back to 4 shards for simplicity"

This reverts commit 413de2fc61.
2025-03-17 15:53:48 -04:00
bc928a34ef Bug Fix: Implement automatic write to disk on route change or refresh (#5761)
* chore: implement faster write function to call when rerouting

* fix: codespell typo

* 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)

* 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)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-17 15:52:23 -04:00
dca78acdf2 #4236 Fix corner rect panning bug (#5817)
* check mouse button in

* same for center rectangle

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2025-03-17 14:45:20 -04:00
40f4450995 Bump the minor group with 23 updates (#5836)
Bumps the minor group with 23 updates:

| Package | From | To |
| --- | --- | --- |
| [@codemirror/autocomplete](https://github.com/codemirror/autocomplete) | `6.17.0` | `6.18.6` |
| [@codemirror/language](https://github.com/codemirror/language) | `6.10.8` | `6.11.0` |
| [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome) | `6.6.0` | `6.7.2` |
| [@fortawesome/free-brands-svg-icons](https://github.com/FortAwesome/Font-Awesome) | `6.6.0` | `6.7.2` |
| [@fortawesome/free-solid-svg-icons](https://github.com/FortAwesome/Font-Awesome) | `6.6.0` | `6.7.2` |
| [bonjour-service](https://github.com/onlxltd/bonjour-service) | `1.2.1` | `1.3.0` |
| [fuse.js](https://github.com/krisk/Fuse) | `7.0.0` | `7.1.0` |
| [re-resizable](https://github.com/bokuweb/react-resizable-box) | `6.9.17` | `6.11.2` |
| [react-hot-toast](https://github.com/timolins/react-hot-toast) | `2.4.1` | `2.5.2` |
| [three](https://github.com/mrdoob/three.js) | `0.172.0` | `0.174.0` |
| [@types/three](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/three) | `0.172.0` | `0.174.0` |
| [uuid](https://github.com/uuidjs/uuid) | `11.0.2` | `11.1.0` |
| [vscode-uri](https://github.com/microsoft/vscode-uri) | `3.0.8` | `3.1.0` |
| [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) | `7.25.4` | `7.26.9` |
| [@electron-forge/cli](https://github.com/electron/forge) | `7.6.1` | `7.7.0` |
| [@electron-forge/plugin-fuses](https://github.com/electron/forge) | `7.6.1` | `7.7.0` |
| [@electron-forge/plugin-vite](https://github.com/electron/forge/tree/HEAD/packages/plugin/vite) | `7.6.1` | `7.7.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.49.1` | `1.51.0` |
| [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) | `2.30.0` | `2.31.0` |
| [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) | `28.10.0` | `28.11.0` |
| [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `5.1.0` | `5.2.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.7.3` | `5.8.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.23.0` | `8.26.1` |


Updates `@codemirror/autocomplete` from 6.17.0 to 6.18.6
- [Changelog](https://github.com/codemirror/autocomplete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/autocomplete/compare/6.17.0...6.18.6)

Updates `@codemirror/language` from 6.10.8 to 6.11.0
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.10.8...6.11.0)

Updates `@fortawesome/fontawesome-svg-core` from 6.6.0 to 6.7.2
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.6.0...6.7.2)

Updates `@fortawesome/free-brands-svg-icons` from 6.6.0 to 6.7.2
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.6.0...6.7.2)

Updates `@fortawesome/free-solid-svg-icons` from 6.6.0 to 6.7.2
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.6.0...6.7.2)

Updates `bonjour-service` from 1.2.1 to 1.3.0
- [Release notes](https://github.com/onlxltd/bonjour-service/releases)
- [Commits](https://github.com/onlxltd/bonjour-service/compare/1.2.1...1.3.0)

Updates `fuse.js` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v7.0.0...v7.1.0)

Updates `re-resizable` from 6.9.17 to 6.11.2
- [Release notes](https://github.com/bokuweb/react-resizable-box/releases)
- [Changelog](https://github.com/bokuweb/re-resizable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bokuweb/react-resizable-box/commits)

Updates `react-hot-toast` from 2.4.1 to 2.5.2
- [Release notes](https://github.com/timolins/react-hot-toast/releases)
- [Commits](https://github.com/timolins/react-hot-toast/compare/v2.4.1...v2.5.2)

Updates `three` from 0.172.0 to 0.174.0
- [Release notes](https://github.com/mrdoob/three.js/releases)
- [Commits](https://github.com/mrdoob/three.js/commits)

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

Updates `uuid` from 11.0.2 to 11.1.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.0.2...v11.1.0)

Updates `vscode-uri` from 3.0.8 to 3.1.0
- [Release notes](https://github.com/microsoft/vscode-uri/releases)
- [Commits](https://github.com/microsoft/vscode-uri/compare/v3.0.8...v3.1.0)

Updates `@babel/preset-env` from 7.25.4 to 7.26.9
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.9/packages/babel-preset-env)

Updates `@electron-forge/cli` from 7.6.1 to 7.7.0
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.6.1...v7.7.0)

Updates `@electron-forge/plugin-fuses` from 7.6.1 to 7.7.0
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.6.1...v7.7.0)

Updates `@electron-forge/plugin-vite` from 7.6.1 to 7.7.0
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/commits/v7.7.0/packages/plugin/vite)

Updates `@playwright/test` from 1.49.1 to 1.51.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.49.1...v1.51.0)

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

Updates `eslint-plugin-import` from 2.30.0 to 2.31.0
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.30.0...v2.31.0)

Updates `eslint-plugin-jest` from 28.10.0 to 28.11.0
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.10.0...v28.11.0)

Updates `eslint-plugin-react-hooks` from 5.1.0 to 5.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

Updates `typescript` from 5.7.3 to 5.8.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.2)

Updates `typescript-eslint` from 8.23.0 to 8.26.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.26.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@codemirror/autocomplete"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@codemirror/language"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@fortawesome/fontawesome-svg-core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@fortawesome/free-brands-svg-icons"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@fortawesome/free-solid-svg-icons"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: bonjour-service
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: fuse.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: re-resizable
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: react-hot-toast
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: three
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@types/three"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vscode-uri
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@babel/preset-env"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@electron-forge/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@electron-forge/plugin-fuses"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@electron-forge/plugin-vite"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@types/three"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: eslint-plugin-react-hooks
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: typescript-eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:45:35 -07:00
178d943423 Bump the minor group in /packages/codemirror-lsp-client with 4 updates (#5824)
Bumps the minor group in /packages/codemirror-lsp-client with 4 updates: [@codemirror/language](https://github.com/codemirror/language), [@codemirror/state](https://github.com/codemirror/state), [typescript](https://github.com/microsoft/TypeScript) and [vscode-uri](https://github.com/microsoft/vscode-uri).


Updates `@codemirror/language` from 6.10.2 to 6.11.0
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.10.2...6.11.0)

Updates `@codemirror/state` from 6.4.1 to 6.5.2
- [Changelog](https://github.com/codemirror/state/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/state/compare/6.4.1...6.5.2)

Updates `typescript` from 5.7.2 to 5.8.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.2...v5.8.2)

Updates `vscode-uri` from 3.0.8 to 3.1.0
- [Release notes](https://github.com/microsoft/vscode-uri/releases)
- [Commits](https://github.com/microsoft/vscode-uri/compare/v3.0.8...v3.1.0)

---
updated-dependencies:
- dependency-name: "@codemirror/language"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: "@codemirror/state"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: typescript
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vscode-uri
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 09:40:03 -07:00
e78788482e Move build:wasm commands to dedicated scripts (#5813)
* add script to build on mac mini

* Remove wasm-prep, rust flag only on wasm-pack build, add ps1 script

* Make it exectuable, tested on macOS

* Add set -e

* Stop scripts on error

---------

Co-authored-by: lf94 <ircsurfer33@gmail.com>
2025-03-17 12:24:40 -04:00
e50e9a00d4 Make scripts/get-latest-wasm-bundle.sh executable (#5791) 2025-03-17 12:23:28 -04:00
dc82b4c8ea Fix mixed units assemblies to work, regardless of project settings (#5818)
* Add mixed units test

* Add incorrect output

* Change to always set units since old units are not accurate

* Update output after fix
2025-03-17 15:26:56 +00:00
a8b0e1a771 Use arrays for multiple geometry (#5770)
* Parse [T] instead of T[] for array types

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

* homogenous arrays, type coercion, remove solid set and sketch set, etc

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-17 17:57:26 +13:00
75a975b1e1 Replace snapshot mechanism with epochs (#5764)
* Make tag identifiers monotonic

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

* Use epochs rather than snapshots in memory

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

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-17 12:28:51 +13:00
3f02bb2065 deterministic id generator per module (#5811)
* deterministic id generator per module

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

* non

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

* updates

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

* do not remake the planes if they are alreaady made;

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

* updates

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

* do not remake the planes if they are alreaady made;

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

* clippy

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-15 17:08:39 +00:00
9c986d3aa8 Fix window creation failures on Mac (most noteable in CI) (#5809)
Allow window to be larger than screen on mac as well, fixing tests
2025-03-14 21:29:00 +00:00
4741d9592b Add edit flow for Shell thickness (#5525)
* WIP: Add edit flow for Shell thickness
Would fix #5406 but not like this :sad:

* Early win with working edit on shell

* Extend to logic to walls with tags

* Remove skip

* Add wall test

* Fixing inconsistencies in code. Tests not working on win

* Refactor addShell for consitency

* Clean up

* More clean up

* Add validation on both params

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

* Passing tests

* Prettier

* Change from test.skip to comment

* Clean up for review

* Add review suggestions and disable thickness validator

* 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 uneccessary changes

* Add edit flow for Shell thickness
Fixes #5406

* Enable edit step in test that works only if the start code is properly formatted

* Clean up for review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-14 16:05:41 -04:00
e9806b83d7 Clip deltaY to +/- 100 (#5793)
* Clip deltaY to +/- 100
Should fix #5120

* Lint

* Fix the clip with @nickmccleery's suggestion
2025-03-14 13:17:10 -04:00
0229105158 fix: add better errors for missing commas in arrays and objects (#5210)
* fix: add better errors for missing commas in arrays and objects

* chore: add object prop shorthand missing comma test

* fix: wording on unexpected character in arrays and objects

* fix: don't eagerly evaluate whether there is a closing brace/bracket

* feat: exit early when detecting missing commas if encountering invalid tokens

* 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)

* fix: updated reserved word in test to replace removed one

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-14 16:52:10 +00:00
9e37e13b6b Further unfrig playwright (#5794)
* Bail on cleanup after 10s; move setup even higher

* Give macos time before starting an electron instance

* Try it again

* Again

* Remove stale TODO

* Use Kolmogorov complexity instead of text for ai assertions

* Help out prompt-to-edit test with being more explicit

* Try to give Mac more time to open a window

* Fix the other var change for prompt-to-edit test

* Forgot this

* Try some crazier shit

* 🤦 I forgot the return.

* Ok things were actually just not working well at all

* Fix export test to expect smaller size

* yarn tsc && yarn lint && yarn fmt
2025-03-14 10:19:58 +00:00
58e0c0e916 Improve KCL Samples (#5767)
* improve KCL Samples & .gitignore

* update block and car wheel assembly

* update flange and lego, delete flange xy

* artifacts

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

* updates

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

* updates

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

* scale

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

* updates

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

* docs

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 23:38:51 -07:00
dd99c27d56 bump all the rust things (#5806)
* updates

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

* getrandom

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

* updates

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

* simlimk

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

* Revert "simlimk"

This reverts commit 3f5221db7e.

* terst

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 23:38:41 -07:00
3cff26b987 make sure all enter sketch mode are with the stuff they need in the same batch order always (#5646)
* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* updates

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

* comment out

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

* updates

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

* update artifacts

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

* small

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

* updates

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

* updates

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

* last of the artifacts

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

* update playwirght

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

* updates

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

* add crazy multi-profile test

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

* updates

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

* steps

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

* fix artifact graph

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

* updates

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

* cleanup

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

* updates
;

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

* more artifact grph

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

* turn back on playwright

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

* fmt

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

* playwright fixes

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

* playwright fixes

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 21:59:39 -07:00
78ac5b0a11 make deterministic date cleaner (#5777)
* make deterministic date cleaner

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

* update all the steps

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

* more steps

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

* steps

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 18:01:47 -07:00
24d0b14668 change runner (#5800)
Revert "try codspeed runner (#5796)"

This reverts commit ec64daa01f.
2025-03-13 13:57:24 -07:00
6fb32eeff2 fix python bindings building on linux (#5795)
* updates

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

* updates;

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 12:05:45 -07:00
ec64daa01f try codspeed runner (#5796)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 12:05:15 -07:00
e8886bb358 hide program memory source ranges from the app (#5759)
* hide program memory source ranges from the app

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

fix unit tests

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

fix memory

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

remove from paths

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

updates

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

fixes

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

updates

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

updates

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

updates

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

updates

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

updates

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

updates

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

* fix program memory source ranges

* updates

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

* updates

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2025-03-13 11:13:33 -07:00
05a6313d97 try execute bench with walltime (#5772)
* try execute bench

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

* timeout longer

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

* timeout longer

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

* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 10:33:00 -07:00
80f78e1c61 Revolve/Sweep multiple sketches at once (#5779)
* revolve multiple sketches at once

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

* updates

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

* updates

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

* updates

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

* updates

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

* do the same for swweep

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

* updates

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

* do the same for swweep

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-03-13 09:38:22 -07:00
c441a3ab1c Fix playwright mental model, don't make me psycho, thanks (#5680)
* Fix flakey tests with new toolbar.exitSketch

* tsc && lint && fmt

* Disable pw electron thing again

* Unfrig Playwright-Electron a ton; fix another ton of flakes.

* More deflaky

* Fix a ton of tests and playwright related hell

* Run jess's magic incantation to build rust kcl things

* yarn tsc

* yarn lint

* yarn fmt

* Remove double logs

* Revert to old settings spreads momentarily

* Expect error *in the fixtureSetup*, does not circumvent typechecking for regular usage

* Fix unit tests
2025-03-13 10:54:00 -04:00
e894242768 Add warning when using a module with no return value (#5771)
* Add warning when using a module with no return value

* Update output files since changing source range of the pipeline argument

* Change wording of error message to not use the term unlabeled
2025-03-12 18:05:18 +00:00
d8dff03746 Add cache for wasm in build-apps (#5758)
* WIP: break out e2e snapshots

* Separate prepare-wasm job

* Force e2e to run on pierremtb/adhoc/break-out-snapshots

* Quick fix

* Quick fix2

* Quick fix3

* Quick fix4

* Quick fix5

* Remove ifs for flow tests

* Clean up for review

* Cache wasm on PR for build-apps

* Add checks on build wasm tings

* Clean up for review
2025-03-12 13:53:30 -04:00
60aee7ddba Bump the security group with 2 updates (#5773)
Bumps the security group with 2 updates: [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) and [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime).


Updates `@babel/helpers` from 7.25.0 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

Updates `@babel/runtime` from 7.25.0 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
  dependency-group: security
- dependency-name: "@babel/runtime"
  dependency-type: indirect
  dependency-group: security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 10:03:14 -07:00
781 changed files with 458837 additions and 536305 deletions

View File

@ -33,26 +33,63 @@ jobs:
- run: yarn install
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'rust/**'
- name: Download Wasm Cache
id: download-wasm
if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.rust == 'false' }}
uses: dawidd6/action-download-artifact@v7
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: rust/kcl-wasm-lib/pkg
- name: Build WASM condition
id: wasm
run: |
set -euox pipefail
# Build wasm if this is a push to main or tag, there are Rust changes, or
# downloading from the wasm cache failed.
if [[ ${{github.event_name}} == 'push' || ${{steps.filter.outputs.rust}} == 'true' || ${{steps.download-wasm.outcome}} == 'failure' ]]; then
echo "should-build-wasm=true" >> $GITHUB_OUTPUT
else
echo "should-build-wasm=false" >> $GITHUB_OUTPUT
fi
- name: Use correct Rust toolchain
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
# TODO: see if we can fetch from main instead if no diff at rust
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with:
tool: wasm-pack
- name: Rust Cache
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- name: Run build:wasm
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "yarn build:wasm"
- name: Set nightly version, product name, release notes, and icons

View File

@ -50,12 +50,13 @@ jobs:
- name: Build the benchmark target(s)
run: |
cd rust
cargo codspeed build
cargo codspeed build --measurement-mode walltime
- name: Run the benchmarks
uses: CodSpeedHQ/action@v3
with:
working-directory: rust
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
mode: walltime
env:
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}

View File

@ -75,7 +75,7 @@ jobs:
prepare-wasm:
# seperate job on Ubuntu to build or fetch the wasm blob once on the fastest runner
runs-on: namespace-profile-ubuntu-8-cores
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: conditions
steps:
- uses: actions/checkout@v4
@ -163,7 +163,7 @@ jobs:
snapshots:
name: playwright:snapshots:ubuntu
runs-on: namespace-profile-ubuntu-8-cores
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: [conditions, prepare-wasm]
steps:
- uses: actions/create-github-app-token@v1
@ -220,8 +220,12 @@ jobs:
- name: Run ubuntu/chrome snapshots
if: needs.conditions.outputs.should-run == 'true'
run: |
yarn test:snapshots
uses: nick-fields/retry@v3.0.2
with:
shell: bash
command: yarn test:snapshots
timeout_minutes: 30
max_attempts: 3
env:
CI: true
NODE_ENV: development
@ -233,19 +237,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
with:
name: playwright-report-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
overwrite: true
- name: Clean up test-results
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
continue-on-error: true
run: rm -r test-results
- name: check for changes
if: ${{ needs.conditions.outputs.should-run == 'true' && matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 && github.ref != 'refs/heads/main' }}
if: ${{ needs.conditions.outputs.should-run == 'true' && github.ref != 'refs/heads/main' }}
shell: bash
id: git-check
run: |
@ -266,31 +265,36 @@ jobs:
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -m "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git commit -m "A snapshot a day keeps the bugs away! 📷🐛" || true
git push
git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.git-check.outputs.modified == 'true' }}
with:
name: playwright-report-ubuntu-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
electron:
needs: [conditions, prepare-wasm]
timeout-minutes: 60
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
env:
OS_NAME: ${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}
name: playwright:electron:${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}:${{ matrix.shardIndex }}:${{ matrix.shardTotal }}
strategy:
fail-fast: false
matrix:
# TODO: enable self-hosted-windows-8-cores once available
os: [namespace-profile-ubuntu-8-cores, namespace-profile-macos-8-cores, windows-16-cores]
# TODO: enable namespace-profile-windows-latest once available
os:
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
- namespace-profile-macos-8-cores
- windows-latest
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
# Disable macos and windows tests on hourly e2e tests since we only care
# about server side changes.
# Technique from https://github.com/joaomcteixeira/python-project-skeleton/pull/31/files
isScheduled:
- ${{ github.event_name == 'schedule' }}
exclude:
- os: namespace-profile-macos-8-cores
isScheduled: true
- os: windows-latest
isScheduled: true
# TODO: add ref here for main and latest release tag
runs-on: ${{ matrix.os }}
steps:
@ -347,7 +351,7 @@ jobs:
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run playwright/electron flow (with retries)
@ -356,9 +360,9 @@ jobs:
uses: nick-fields/retry@v3.0.2
with:
shell: bash
command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}}
timeout_minutes: 30
max_attempts: 25
command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{ env.OS_NAME }}
timeout_minutes: 45
max_attempts: 15
env:
CI: true
FAIL_ON_CONSOLE_ERRORS: true
@ -370,7 +374,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
include-hidden-files: true
retention-days: 30
@ -379,7 +383,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30

3
.gitignore vendored
View File

@ -53,13 +53,14 @@ e2e/playwright/export-snapshots/*
/public/kcl-samples.zip
/public/kcl-samples/.github
/public/kcl-samples/screenshots/main.kcl
/public/kcl-samples/step/main.kcl
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/src/lang/std/artifactMapCache
## generated files
src/**/*.typegen.ts

File diff suppressed because one or more lines are too long

View File

@ -6,13 +6,13 @@ layout: manual
Extend a 2-dimensional sketch through a third dimension in order to create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.
You can provide more than one sketch to extrude, and they will all be extruded in the same direction.
```js
extrude(
sketchSet: SketchSet,
sketches: [Sketch],
length: number,
): SolidSet
): [Solid]
```
@ -20,12 +20,12 @@ extrude(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketches should be extruded | Yes |
| `sketches` | [`[Sketch]`](/docs/kcl/types/Sketch) | Which sketch or sketches should be extruded | Yes |
| `length` | [`number`](/docs/kcl/types/number) | How far to extrude the given sketches | Yes |
### Returns
[`SolidSet`](/docs/kcl/types/SolidSet) - A solid or a group of solids.
[`[Solid]`](/docs/kcl/types/Solid)
### Examples

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.
```js
hole(
holeSketch: SketchSet,
holeSketch: [Sketch],
sketch: Sketch,
): Sketch
```
@ -20,7 +20,7 @@ hole(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `holeSketch` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
| `holeSketch` | [`[Sketch]`](/docs/kcl/types/Sketch) | | Yes |
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | | Yes |
### Returns

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ Repeat a 2-dimensional sketch some number of times along a partial or complete c
```js
patternCircular2d(
sketchSet: SketchSet,
sketchSet: [Sketch],
instances: integer,
center: [number],
arcDegrees: number,
@ -24,7 +24,7 @@ patternCircular2d(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | Which sketch(es) to pattern | Yes |
| `sketchSet` | [`[Sketch]`](/docs/kcl/types/Sketch) | Which sketch(es) to pattern | Yes |
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `center` | [`[number]`](/docs/kcl/types/number) | The center about which to make the pattern. This is a 2D vector. | Yes |
| `arcDegrees` | [`number`](/docs/kcl/types/number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |

View File

@ -10,7 +10,7 @@ Repeat a 3-dimensional solid some number of times along a partial or complete ci
```js
patternCircular3d(
solidSet: SolidSet,
solids: [Solid],
instances: integer,
axis: [number],
center: [number],
@ -25,7 +25,7 @@ patternCircular3d(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid(s) to pattern | Yes |
| `solids` | [`[Solid]`](/docs/kcl/types/Solid) | Which solid(s) to pattern | Yes |
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `axis` | [`[number]`](/docs/kcl/types/number) | The axis around which to make the pattern. This is a 3D vector | Yes |
| `center` | [`[number]`](/docs/kcl/types/number) | The center about which to make the pattern. This is a 3D vector. | Yes |

View File

@ -10,7 +10,7 @@ Repeat a 2-dimensional sketch along some dimension, with a dynamic amount of dis
```js
patternLinear2d(
sketchSet: SketchSet,
sketches: [Sketch],
instances: integer,
distance: number,
axis: [number],
@ -23,7 +23,7 @@ patternLinear2d(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketchSet` | [`SketchSet`](/docs/kcl/types/SketchSet) | The sketch(es) to duplicate | Yes |
| `sketches` | [`[Sketch]`](/docs/kcl/types/Sketch) | The sketch(es) to duplicate | Yes |
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `distance` | [`number`](/docs/kcl/types/number) | Distance between each repetition. Also known as 'spacing'. | Yes |
| `axis` | [`[number]`](/docs/kcl/types/number) | The axis of the pattern. A 2D vector. | Yes |

View File

@ -10,7 +10,7 @@ Repeat a 3-dimensional solid along a linear path, with a dynamic amount of dista
```js
patternLinear3d(
solidSet: SolidSet,
solids: [Solid],
instances: integer,
distance: number,
axis: [number],
@ -23,7 +23,7 @@ patternLinear3d(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
| `solids` | [`[Solid]`](/docs/kcl/types/Solid) | The solid(s) to duplicate | Yes |
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `distance` | [`number`](/docs/kcl/types/number) | Distance between each repetition. Also known as 'spacing'. | Yes |
| `axis` | [`[number]`](/docs/kcl/types/number) | The axis of the pattern. A 2D vector. | Yes |

View File

@ -36,7 +36,7 @@ The transform function returns a transform object. All properties of the object
```js
patternTransform(
solidSet: SolidSet,
solids: [Solid],
instances: integer,
transform: FunctionSource,
useOriginal?: bool,
@ -48,7 +48,7 @@ patternTransform(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | The solid(s) to duplicate | Yes |
| `solids` | [`[Solid]`](/docs/kcl/types/Solid) | The solid(s) to duplicate | Yes |
| `instances` | `integer` | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
| `useOriginal` | [`bool`](/docs/kcl/types/bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,10 +10,10 @@ Remove volume from a 3-dimensional shape such that a wall of the provided thickn
```js
shell(
solidSet: SolidSet,
solids: [Solid],
thickness: number,
faces: [FaceTag],
): SolidSet
): [Solid]
```
@ -21,13 +21,13 @@ shell(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solidSet` | [`SolidSet`](/docs/kcl/types/SolidSet) | Which solid (or solids) to shell out | Yes |
| `solids` | [`[Solid]`](/docs/kcl/types/Solid) | Which solid (or solids) to shell out | Yes |
| `thickness` | [`number`](/docs/kcl/types/number) | The thickness of the shell | Yes |
| `faces` | [`[FaceTag]`](/docs/kcl/types/FaceTag) | The faces you want removed | Yes |
### Returns
[`SolidSet`](/docs/kcl/types/SolidSet) - A solid or a group of solids.
[`[Solid]`](/docs/kcl/types/Solid)
### Examples

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,5 @@ A face.
| `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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -22,6 +22,5 @@ A helix.
| `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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -22,6 +22,5 @@ A helix.
| `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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -18,6 +18,5 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -25,7 +25,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Uuid`| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -42,7 +41,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Bool`| | No |
| `value` |`boolean`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -60,7 +58,6 @@ Any KCL value.
| `type` |enum: `Number`| | No |
| `value` |[`number`](/docs/kcl/types/number)| | No |
| `ty` |[`NumericType`](/docs/kcl/types/NumericType)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -77,7 +74,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `String`| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -94,7 +90,22 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `MixedArray`| | No |
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `HomArray`| | No |
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
----
@ -111,7 +122,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: `Object`| | No |
| `value` |`object`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -128,8 +138,6 @@ Any KCL value.
|----------|------|-------------|----------|
| `type` |enum: [`TagIdentifier`](/docs/kcl/types#tag-identifier)| | No |
| `value` |[`string`](/docs/kcl/types/string)| | No |
| `info` |[`TagEngineInfo`](/docs/kcl/types/TagEngineInfo)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -207,22 +215,6 @@ Any KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Sketches`| | No |
| `value` |`[` [`Sketch`](/docs/kcl/types/Sketch) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
@ -239,22 +231,6 @@ Any KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Solids`| | No |
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
@ -279,7 +255,6 @@ Data for an imported geometry.
| `type` |enum: [`ImportedGeometry`](/docs/kcl/types/ImportedGeometry)| | No |
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -295,7 +270,6 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Function`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -312,7 +286,6 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `type` |enum: `Module`| | No |
| `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Identifier of a source file. Uses a u32 to keep the size small. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -328,7 +301,6 @@ Data for an imported geometry.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Type`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -345,24 +317,6 @@ Data for an imported geometry.
|----------|------|-------------|----------|
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Tombstone`| | No |
| `value` |`null`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -126,6 +126,30 @@ A base path.
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----
A base path.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArcThreePoint`| | No |
| `p1` |`[number, number]`| Point 1 of the arc (base on the end of previous segment) | No |
| `p2` |`[number, number]`| Point 2 of the arc (interior kwarg) | No |
| `p3` |`[number, number]`| Point 3 of the arc (end kwarg) | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----
A path that is horizontal.

View File

@ -1,57 +0,0 @@
---
title: "SketchSet"
excerpt: "A sketch or a group of sketches."
layout: manual
---
A sketch or a group of sketches.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `sketch`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |[`string`](/docs/kcl/types/string)| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
**Type:** `[object, array]`
`[` [`Sketch`](/docs/kcl/types/Sketch) `]`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `sketches`| | No |
----

View File

@ -32,7 +32,6 @@ A sketch type.
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
@ -57,7 +56,6 @@ A face.
| `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 |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -12,31 +12,6 @@ Data for a solid or an imported geometry.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `solid`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the solid. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID of the solid. Unlike `id`, this doesn't change. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |[`number`](/docs/kcl/types/number)| The height of the solid. | No |
| `startCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion start cap | No |
| `endCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
Data for an imported geometry.
**Type:** `object`
@ -52,7 +27,21 @@ Data for an imported geometry.
| `type` |enum: `importedGeometry`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `[object, array]`
`[` [`Solid`](/docs/kcl/types/Solid) `]`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `solidSet`| | No |
----

View File

@ -1,58 +0,0 @@
---
title: "SolidSet"
excerpt: "A solid or a group of solids."
layout: manual
---
A solid or a group of solids.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `solid`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the solid. | No |
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID of the solid. Unlike `id`, this doesn't change. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |[`number`](/docs/kcl/types/number)| The height of the solid. | No |
| `startCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion start cap | No |
| `endCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
**Type:** `[object, array]`
`[` [`Solid`](/docs/kcl/types/Solid) `]`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `solids`| | No |
----

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import {
getUtils,
TEST_COLORS,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'

View File

@ -10,7 +10,11 @@ import fsp from 'fs/promises'
test(
'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => {
async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -86,7 +90,7 @@ test(
await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
await test.step('Check the export size', async () => {
@ -165,7 +169,7 @@ test(
]))
const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
await test.step('Check the export size', async () => {
@ -181,7 +185,7 @@ test(
},
{ timeout: 15_000 }
)
.toBeGreaterThan(100_000)
.toBeGreaterThan(70_000)
})
})
}

View File

@ -158,11 +158,14 @@ test.describe('when using the file tree to', () => {
await createNewFile('lee')
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
).toHaveCount(5)
await expect
.poll(() =>
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
.count()
)
.toEqual(5)
})
}
)

View File

@ -27,28 +27,19 @@ type CmdBarSerialised =
export class CmdBarFixture {
public page: Page
get cmdBarOpenBtn() {
return this.page.getByTestId('command-bar-open-button')
}
get cmdBarElement() {
return this.page.getByTestId('command-bar')
}
public cmdBarOpenBtn!: Locator
public cmdBarElement!: Locator
constructor(page: Page) {
this.page = page
this.cmdBarOpenBtn = this.page.getByTestId('command-bar-open-button')
this.cmdBarElement = this.page.getByTestId('command-bar')
}
get currentArgumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
// Put all selectors here because this method is re-run on fixture creation.
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' }

View File

@ -24,11 +24,6 @@ export class EditorFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content[data-language="kcl"]')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
@ -87,6 +82,30 @@ export class EditorFixture {
toContain: this._expectEditorToContain(),
not: { toContain: this._expectEditorToContain(true) },
}
snapshot = async (options?: { timeout?: number; name?: string }) => {
const wasPaneOpen = await this.checkIfPaneIsOpen()
if (!wasPaneOpen) {
await this.openPane()
}
try {
// Use expect.poll to implement retry logic
await expect
.poll(
async () => {
const code = await this.codeContent.textContent()
return code || ''
},
{ timeout: options?.timeout || 5000 }
)
.toMatchSnapshot(options?.name || 'editor-content')
} finally {
// Reset pane state if needed
if (!wasPaneOpen) {
await this.closePane()
}
}
}
private _serialiseDiagnostics = async (): Promise<Array<string>> => {
const diagnostics = await this.diagnosticsGutterIcon.all()
const diagnosticsContent: string[] = []

View File

@ -1,13 +1,31 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type {
BrowserContext,
ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo,
Page,
} from '@playwright/test'
import { getUtils, setup, setupElectron } from '../test-utils'
import {
_electron as electron,
PlaywrightTestArgs,
PlaywrightWorkerArgs,
} from '@playwright/test'
import * as TOML from '@iarna/toml'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
TEST_SETTINGS_DEFAULT_THEME,
} from '../storageStates'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import fs from 'node:fs'
import path from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
@ -23,7 +41,7 @@ export class AuthenticatedApp {
public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 }
public electronApp: undefined | ElectronApplication
public dir: string = ''
public projectDirName: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context
@ -46,7 +64,7 @@ export class AuthenticatedApp {
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
'utf-8'
)
}
@ -59,101 +77,326 @@ export interface Fixtures {
scene: SceneFixture
homePage: HomePageFixture
}
export class AuthenticatedTronApp {
public originalPage: Page
public page: Page
public browserContext: BrowserContext
public context: BrowserContext
public readonly testInfo: TestInfo
public electronApp: ElectronApplication | undefined
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor(
browserContext: BrowserContext,
originalPage: Page,
testInfo: TestInfo
) {
this.page = originalPage
this.originalPage = originalPage
this.browserContext = browserContext
// Will be overwritten in the initializer
this.context = browserContext
this.testInfo = testInfo
}
async initialise(
arg: {
fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
} = { fixtures: {} }
) {
const { electronApp, page, context, dir } = await setupElectron({
testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings,
viewport: this.viewPortSize,
export class ElectronZoo {
public available: boolean = true
public electron!: ElectronApplication
public firstUrl = ''
public viewPortSize = { width: 1200, height: 500 }
public projectDirName = ''
public page!: Page
public context!: BrowserContext
constructor() {}
// Help remote end by signaling we're done with the connection.
// If it takes longer than 10s to stop, just resolve.
async makeAvailableAgain() {
await this.page.evaluate(async () => {
return new Promise((resolve) => {
if (!window.engineCommandManager.engineConnection?.state?.type) {
return resolve(undefined)
}
window.engineCommandManager.tearDown()
// Keep polling (per js event tick) until state is Disconnected.
const timeA = Date.now()
const checkDisconnected = () => {
// It's possible we never even created an engineConnection
// e.g. never left Projects view.
if (
window.engineCommandManager?.engineConnection?.state.type ===
'disconnected'
) {
return resolve(undefined)
}
if (Date.now() - timeA > 10000) {
return resolve(undefined)
}
setTimeout(checkDisconnected, 0)
}
checkDisconnected()
})
})
this.page = page
// These assignments "fix" some brokenness in the Playwright Workbench when
// running against electron applications.
// The timeline is still broken but failure screenshots work again.
this.context = context
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled
// Object.assign(this.browserContext, this.context)
await this.context.tracing.stopChunk({ path: 'trace.zip' })
this.electronApp = electronApp
this.dir = dir
// Only after cleanup we're ready.
this.available = true
}
// Easier to access throughout utils
this.page.dir = dir
async createInstanceIfMissing(testInfo: TestInfo) {
// Create or otherwise clear the folder.
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
// We need to expose this in order for some tests that require folder
// creation and some code below.
const that = this
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if (
!fixture ||
fixture instanceof AuthenticatedApp ||
fixture instanceof AuthenticatedTronApp
)
continue
fixture.reConstruct(page)
const options = {
timeout: 120000,
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: this.projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? {
executablePath:
process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron',
}
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: this.viewPortSize,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!this.electron) {
this.electron = await electron.launch(options)
// Mac takes quite a long time to create the first window in CI.
// Turns out we can't trust firstWindow() either. So loop.
let timeoutId: ReturnType<typeof setTimeout>
const tryToGetWindowPage = () =>
new Promise((resolve) => {
const fn = () => {
this.page = this.electron.windows()[0]
timeoutId = setTimeout(() => {
if (this.page) {
clearTimeout(timeoutId)
return resolve(undefined)
}
fn()
}, 0)
}
fn()
})
await tryToGetWindowPage()
this.context = this.electron.context()
await this.context.tracing.start({ screenshots: true, snapshots: true })
}
await this.context.tracing.startChunk()
await setup(this.context, this.page, testInfo)
await this.cleanProjectDir()
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
this.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await this.setViewportSize(dims)
await that.electron?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return this.evaluate(async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
}, dims)
}
await this.page.setBodyDimensions(this.viewPortSize)
this.context.folderSetupFn = async function (fn) {
return fn(that.projectDirName)
.then(() => that.page.reload())
.then(() => ({
dir: that.projectDirName,
}))
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
if (!this.firstUrl) {
await this.page.getByText('Your Projects').count()
this.firstUrl = this.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
// Always start at the root view
await this.page.goto(this.firstUrl)
// Force a hard reload, destroying the stream and other state
await this.page.reload()
}
close = async () => {
await this.electronApp?.close?.()
async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
try {
if (fs.existsSync(this.projectDirName)) {
await fsp.rm(this.projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
try {
await fsp.mkdir(this.projectDirName)
} catch (e) {
// Not a problem if it already exists.
}
const tempSettingsFilePath = path.join(
this.projectDirName,
SETTINGS_FILE_NAME
)
let settingsOverridesToml = ''
if (appSettings) {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
...appSettings.app,
},
},
})
} else {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
},
},
})
}
await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
}
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
}
export const fixtures = {
cmdBar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
// If yee encounter this, please try to type it.
type FnUse = any
const fixturesForElectron = {
page: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await use(tronApp.page)
},
context: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await use(tronApp.context)
},
}
const fixturesForWeb = {
page: async (
{ page, context }: { page: Page; context: BrowserContext },
use: FnUse,
testInfo: TestInfo
) => {
page.setBodyDimensions = page.setViewportSize
// We do the same thing in ElectronZoo. addInitScript simply doesn't fire
// at the correct time, so we reload the page and it fires appropriately.
const oldPageAddInitScript = page.addInitScript
page.addInitScript = async function (...args) {
// @ts-expect-error
await oldPageAddInitScript.apply(this, args)
await page.reload()
}
const oldContextAddInitScript = context.addInitScript
context.addInitScript = async function (...args) {
// @ts-expect-error
await oldContextAddInitScript.apply(this, args)
await page.reload()
}
const webApp = new AuthenticatedApp(context, page, testInfo)
await webApp.initialise()
await use(page)
},
}
const fixturesBasedOnProcessEnvPlatform = {
cmdBar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
editor: async ({ page }: { page: Page }, use: FnUse) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
toolbar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
scene: async ({ page }: { page: Page }, use: FnUse) => {
await use(new SceneFixture(page))
},
homePage: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
homePage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new HomePageFixture(page))
},
}
if (process.env.PLATFORM === 'web') {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
} else {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
}
export { fixturesBasedOnProcessEnvPlatform }

View File

@ -27,10 +27,6 @@ export class HomePageFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectSection = this.page.getByTestId('home-section')
@ -96,8 +92,12 @@ export class HomePageFixture {
}
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
projectsLoaded = async () => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded()
await this.projectButtonNew.click()
await this.projectTextName.click()
await this.projectTextName.fill(projectTitle)

View File

@ -53,7 +53,12 @@ export class SceneFixture {
constructor(page: Page) {
this.page = page
this.reConstruct(page)
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo()
@ -72,17 +77,6 @@ export class SceneFixture {
.toEqual(expected)
}
reConstruct = (page: Page) => {
this.page = page
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
makeMouseHelpers = (
x: number,
y: number,
@ -253,7 +247,7 @@ export class SceneFixture {
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await u.closeDebugPanel()
await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled()

View File

@ -8,6 +8,7 @@ import {
} from '../test-utils'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { ToolbarModeName } from 'lib/toolbar'
export class ToolbarFixture {
public page: Page
@ -37,13 +38,12 @@ export class ToolbarFixture {
featureTreeId = 'feature-tree' as const
/** The pane element for the Feature Tree */
featureTreePane!: Locator
gizmo!: Locator
gizmoDisabled!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
@ -67,6 +67,13 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane')
this.fileCreateToast = page.getByText('Successfully created')
// Note to test writers: having two locators like this is preferable to one
// which changes another el property because it means our test "signal" is
// completely decoupled from the elements themselves. It means the same
// element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
}
get editSketchBtn() {
@ -86,6 +93,18 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
waitUntilSketchingReady = async () => {
await expect(this.gizmoDisabled).toBeVisible()
}
startSketchThenCallbackThenWaitUntilReady = async (
cb: () => Promise<void>
) => {
await this.startSketchBtn.click()
await cb()
await this.waitUntilSketchingReady()
}
exitSketch = async () => {
await this.exitSketchBtn.click()
await expect(
@ -102,6 +121,15 @@ export class ToolbarFixture {
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
}
private _getMode = () =>
this.page.locator('[data-current-mode]').getAttribute('data-current-mode')
expectToolbarMode = {
toBe: (mode: ToolbarModeName) => expect.poll(this._getMode).toEqual(mode),
not: {
toBe: (mode: ToolbarModeName) =>
expect.poll(this._getMode).not.toEqual(mode),
},
}
private _serialiseFileTree = async () => {
return this.page
@ -158,6 +186,22 @@ export class ToolbarFixture {
).toBeVisible()
await this.page.getByTestId('dropdown-circle-three-points').click()
}
selectArc = async () => {
await this.page
.getByRole('button', { name: 'caret down Tangential Arc:' })
.click()
await expect(this.page.getByTestId('dropdown-arc')).toBeVisible()
await this.page.getByTestId('dropdown-arc').click()
}
selectThreePointArc = async () => {
await this.page
.getByRole('button', { name: 'caret down Tangential Arc:' })
.click()
await expect(
this.page.getByTestId('dropdown-three-point-arc')
).toBeVisible()
await this.page.getByTestId('dropdown-three-point-arc').click()
}
async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -21,58 +21,54 @@ import { expectPixelColor } from './fixtures/sceneFixture'
// we must set it to empty for the tests where we want to see the onboarding immediately.
test.describe('Onboarding tests', () => {
test(
'Onboarding code is shown in the editor',
{
appSettings: {
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(
8
)
test('Onboarding code is shown in the editor', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test(
'Desktop: fresh onboarding executes and loads',
{
tag: '@electron',
appSettings: {
},
async ({ page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page }) => {
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -107,223 +103,235 @@ test.describe('Onboarding tests', () => {
}
)
test(
'Code resets after confirmation',
{
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`
test('Code resets after confirmation', async ({
context,
page,
homePage,
tronApp,
scene,
cmdBar,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
const initialCode = `sketch001 = startSketchOn('XZ')`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
const nextButton = page.getByTestId('onboarding-next')
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next')
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
})
test('Click through each onboarding step and back', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
}
)
test(
'Click through each onboarding step and back',
{
appSettings: {
app: {
onboarding_status: '',
},
},
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
}
)
test(
'Onboarding redirects and code updating',
{
appSettings: {
app: {
onboarding_status: '/export',
},
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
})
test('Onboarding redirects and code updating', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/export',
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000'
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
const originalCode = 'sigmaAllow = 15000'
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboarding_status: '/parametric-modeling',
},
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
cleanProjectDir: true,
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/parametric-modeling',
},
})
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
})
// (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',
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -388,15 +396,16 @@ test.describe('Onboarding tests', () => {
test.fixme(
"Avatar text doesn't mention avatar when no avatar",
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -444,15 +453,17 @@ test.describe('Onboarding tests', () => {
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{
appSettings: {
async ({ context, page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: 'dismissed',
},
},
cleanProjectDir: true,
},
async ({ context, page }) => {
})
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
@ -849,159 +850,157 @@ openSketch = startSketchOn('XY')
})
})
test(`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.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 }
// 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
// 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
await scene.expectPixelColor(
backgroundColor,
secondPointLocation,
tolerance
// 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
)
})
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)
// 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()
// Wait for the scene and stream to load
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
backgroundColor,
secondPointLocation,
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('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('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,
@ -1023,7 +1022,7 @@ openSketch = startSketchOn('XY')
await page.waitForTimeout(15000)
await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
//await scene.expectPixelColor([50, 51, 96], testPoint, 15) // FIXME
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.offsetPlaneButton.click()
@ -1065,7 +1064,7 @@ openSketch = startSketchOn('XY')
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Delete')
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
//await scene.expectPixelColor([50, 51, 96], testPoint, 15) // FIXME
})
})
@ -1407,7 +1406,7 @@ sketch002 = startSketchOn('XZ')
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
// await scene.expectPixelColor([135, 64, 73], testPoint, 15) // FIXME
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
@ -2270,8 +2269,8 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle(center = [0, 0], radius = 30)
extrude001 = extrude(sketch001, length = 30)
|> circle(center = [0, 0], radius = 30)
extrude001 = extrude(sketch001, length = 30)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
@ -2285,6 +2284,8 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration =
"shell001 = shell(extrude001, faces = ['end'], thickness = 5)"
const editedShellDeclaration =
"shell001 = shell(extrude001, faces = ['end'], thickness = 2)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
@ -2351,6 +2352,45 @@ chamfer04 = chamfer(extrude001, length = 5, tags = [getOppositeEdge(seg02)])
})
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
})
await test.step('Edit shell via feature tree selection works', async () => {
await toolbar.closePane('code')
await toolbar.openPane('feature-tree')
const operationButton = await toolbar.getFeatureTreeOperation(
'Shell',
0
)
await operationButton.dblclick()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'thickness',
currentArgValue: '5',
headerArguments: {
Thickness: '5',
},
highlightedHeaderArg: 'thickness',
commandName: 'Shell',
})
await page.keyboard.insertText('2')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Thickness: '2',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([150, 150, 150], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(editedShellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [editedShellDeclaration],
highlightedCode: '',
})
})
})
})
@ -2386,6 +2426,8 @@ extrude001 = extrude(sketch001, length = 40)
const mutatedCode = 'xLine(length = -40, tag = $seg01)'
const shellDeclaration =
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 5)"
const editedShellDeclaration =
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 1)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
@ -2434,6 +2476,41 @@ extrude001 = extrude(sketch001, length = 40)
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
})
await test.step('Edit shell via feature tree selection works', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
await operationButton.dblclick({ button: 'left' })
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'thickness',
currentArgValue: '5',
headerArguments: {
Thickness: '5',
},
highlightedHeaderArg: 'thickness',
commandName: 'Shell',
})
await page.keyboard.insertText('1')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Thickness: '1',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([150, 150, 150], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(editedShellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [editedShellDeclaration],
highlightedCode: '',
})
})
await test.step('Delete shell via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
@ -2528,7 +2605,7 @@ extrude002 = extrude(sketch002, length = 50)
highlightedCode: '',
})
await toolbar.closePane('code')
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
await scene.expectPixelColor([80, 80, 80], testPoint, 15)
})
})
})

View File

@ -163,7 +163,7 @@ test(
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
.toBeLessThan(20)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -464,7 +464,11 @@ test.describe('Can export from electron app', () => {
test(
`Can export using ${method}`,
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => {
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
@ -516,6 +520,7 @@ test.describe('Can export from electron app', () => {
storage: 'embedded',
presentation: 'pretty',
},
tronApp.projectDirName,
page,
method
)
@ -523,7 +528,7 @@ test.describe('Can export from electron app', () => {
})
const filepath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
'main.gltf'
)
@ -781,6 +786,7 @@ test(
page.on('console', console.log)
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Loading your Projects...')).not.toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible()
await page.keyboard.press('Delete')
@ -858,7 +864,7 @@ test.describe(`Project management commands`, () => {
test(
`Delete from project page`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
async ({ context, page, scene, cmdBar }, testInfo) => {
const projectName = `my_project_to_delete`
await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -887,6 +893,8 @@ test.describe(`Project management commands`, () => {
await projectHomeLink.click()
await u.waitForPageLoad()
await scene.connectionEstablished()
await scene.settled(cmdBar)
})
await test.step(`Run delete command via command palette`, async () => {
@ -909,7 +917,7 @@ test.describe(`Project management commands`, () => {
test(
`Rename from home page`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
async ({ context, page, homePage }, testInfo) => {
const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -936,6 +944,7 @@ test.describe(`Project management commands`, () => {
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await homePage.projectsLoaded()
await expect(projectHomeLink).toBeVisible()
})
@ -1682,7 +1691,11 @@ test(
test(
'You can change the root projects directory and nothing is lost',
{ tag: '@electron' },
async ({ context, page, electronApp }, testInfo) => {
async ({ context, page, tronApp, homePage }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
@ -1712,6 +1725,8 @@ test(
await fsp.rm(newProjectDirName, { recursive: true })
}
await homePage.projectsLoaded()
await test.step('We can change the root project directory', async () => {
// expect to see the project directory settings link
await expect(
@ -1725,7 +1740,7 @@ test(
.locator('section#projectDirectory input')
.inputValue()
const handleFile = electronApp?.evaluate(
const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths })
@ -1741,6 +1756,8 @@ test(
await page.getByTestId('settings-close-button').click()
await homePage.projectsLoaded()
await expect(page.getByText('No Projects found')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true })
await expect(
@ -1755,7 +1772,7 @@ test(
await page.getByTestId('project-directory-settings-link').click()
const handleFile = electronApp?.evaluate(
const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths })
@ -1767,6 +1784,7 @@ test(
await page.getByTestId('project-directory-button').click()
await handleFile
await homePage.projectsLoaded()
await expect(page.locator('section#projectDirectory input')).toHaveValue(
originalProjectDirName
)
@ -2000,8 +2018,8 @@ test(
test(
'Settings persist across restarts',
{ tag: '@electron', cleanProjectDir: true },
async ({ page }, testInfo) => {
{ tag: '@electron' },
async ({ page, scene, cmdBar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2014,6 +2032,10 @@ test(
await expect(page.getByTestId('app-theme')).toHaveValue('dark')
await page.getByTestId('app-theme').selectOption('light')
await expect(page.getByTestId('app-theme')).toHaveValue('light')
// Give time to system for writing to a persistent store
await page.waitForTimeout(1000)
})
await test.step('Starting the app again and we can see the same theme', async () => {

View File

@ -53,46 +53,47 @@ sketch003 = startSketchOn('XY')
|> close()
extrude003 = extrude(sketch003, length = 20)
`
test.describe('edit with AI example snapshots', () => {
test(
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
test(
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
})
})
})
await test.step('fire off edit prompt', async () => {
await cmdBar.captureTextToCadRequestSnapshot(test.info())
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
})
}
)
await test.step('fire off edit prompt', async () => {
await cmdBar.captureTextToCadRequestSnapshot(test.info())
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
})
}
)
})

View File

@ -35,7 +35,7 @@ sketch003 = startSketchOn('XY')
extrude003 = extrude(sketch003, length = 20)
`
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.fixme('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Check the happy path, for basic changing color', () => {
const cases = [
{
@ -233,7 +233,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await cmdBar.openCmdBar('promptToEdit')
await page
.getByTestId('cmd-bar-arg-value')
.fill('Please rename to mySketch')
.fill('Please rename to mySketch001')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
@ -244,10 +244,10 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
})
await test.step('verify rename change and accept it', async () => {
await editor.expectEditor.toContain('mySketch = startSketchOn')
await editor.expectEditor.toContain('mySketch001 = startSketchOn')
await editor.expectEditor.not.toContain('sketch002 = startSketchOn')
await editor.expectEditor.toContain(
'extrude002 = extrude(mySketch, length = 50)'
'extrude002 = extrude(mySketch001, length = 50)'
)
await acceptBtn.click()

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
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'
@ -318,7 +319,6 @@ extrude001 = extrude(sketch001, length = 50)
'when engine fails export we handle the failure and alert the user',
{ tag: '@skipLocalEngine' },
async ({ scene, page, homePage, cmdBar }) => {
const u = await getUtils(page)
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
@ -491,7 +491,7 @@ extrude001 = extrude(sketch001, length = 50)
}
)
test(
test.fixme(
`Network health indicator only appears in modeling view`,
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
@ -635,11 +635,8 @@ extrude001 = extrude(sketch001, length = 50)
await homePage.goToModelingScene()
})
const toolBarMode = () =>
page.locator('[data-currentMode]').getAttribute('data-currentMode')
await test.step('Start sketch and select a plane', async () => {
await expect.poll(toolBarMode).toEqual('modeling')
await toolbar.expectToolbarMode.toBe('modeling')
// Click the start sketch button
await toolbar.startSketchPlaneSelection()
@ -648,10 +645,10 @@ extrude001 = extrude(sketch001, length = 50)
// Check that the modeling toolbar doesn't appear during the animation
// The animation typically takes around 500ms, so we'll check for a second
await expect.poll(toolBarMode, { timeout: 1000 }).not.toEqual('modeling')
await toolbar.expectToolbarMode.not.toBe('modeling')
// After animation completes, we should see the sketching toolbar
await expect.poll(toolBarMode).toEqual('sketching')
await toolbar.expectToolbarMode.toBe('sketching')
})
})

View File

@ -1,8 +1,9 @@
import { readFileSync } from 'fs'
const secrets: Record<string, string> = {}
const secretsPath = './e2e/playwright/playwright-secrets.env'
try {
const file = readFileSync('./e2e/playwright/playwright-secrets.env', 'utf8')
const file = readFileSync(secretsPath, 'utf8')
file
.split('\n')
.filter((line) => line && line.length > 1)
@ -15,9 +16,12 @@ try {
})
} catch (err) {
// probably running in CI
secrets.token = process.env.token || ''
secrets.snapshottoken = process.env.snapshottoken || ''
// add more env vars here to make them available in CI
console.warn(
`Error reading ${secretsPath}; environment variables will be used`
)
}
secrets.token = secrets.token || process.env.token || ''
secrets.snapshottoken = secrets.snapshottoken || process.env.snapshottoken || ''
// add more env vars here to make them available in CI
export { secrets }

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import fs from 'node:fs/promises'
import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
@ -11,6 +12,7 @@ import {
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
@ -190,7 +192,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
page: Page,
homePage: HomePageFixture,
openPanes: string[],
scene: SceneFixture
scene: SceneFixture,
cmdBar: CmdBarFixture
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
@ -200,13 +203,22 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %)
|> arcTo({
interior = [20.18, -1.7],
end = [11.82, -1.16]
}, %)
|> arc({
radius = 5.92,
angleStart = -89.36,
angleEnd = 135.81
}, %)
|> close()`
)
})
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -241,7 +253,17 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %)
|> close()`)
|> arcTo({
interior = [20.18, -1.7],
end = [11.82, -1.16]
}, %)
|> arc({
radius = 5.92,
angleStart = -89.36,
angleEnd = 135.81
}, %)
|> close()
`)
} else {
// Ensure we don't see the code.
await expect(u.codeLocator).not.toBeVisible()
@ -271,7 +293,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
// drag startProfileAt handle
await page.mouse.move(startPX[0], startPX[1])
@ -309,22 +331,93 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arcTo interior handle (three point arc)
const arcToHandle = await u.getBoundingBox('[data-overlay-index="2"]')
await page.mouse.move(arcToHandle.x, arcToHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToHandle.x - dragPX,
arcToHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arcTo end handle (three point arc)
const arcToEndHandle = await u.getBoundingBox('[data-overlay-index="3"]')
await page.mouse.move(arcToEndHandle.x, arcToEndHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToEndHandle.x - dragPX,
arcToEndHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arc radius handle
const arcRadiusHandle = await u.getBoundingBox('[data-overlay-index="4"]')
await page.mouse.move(arcRadiusHandle.x, arcRadiusHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcRadiusHandle.x - dragPX,
arcRadiusHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arc center handle (we'll have to hardcode the position because it doesn't have a overlay near the handle)
const arcCenterHandle = { x: 745, y: 214 }
await page.mouse.move(arcCenterHandle.x, arcCenterHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcCenterHandle.x - dragPX,
arcCenterHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// Open the code pane
await u.openKclCodePanel()
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97])
|> tangentialArcTo([24.95, -5.38], %)
|> line(end = [1.97, 2.06])
|> close()`)
|> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97])
|> tangentialArcTo([26.92, -3.32], %)
|> arcTo({
interior = [18.11, -3.73],
end = [9.77, -3.19]
}, %)
|> arc({
radius = 3.75,
angleStart = -58.29,
angleEnd = 161.17
}, %)
|> close()
`)
}
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene }) => {
async ({ page, homePage, scene, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -337,14 +430,20 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
await doEditSegmentsByDraggingHandle(
page,
homePage,
['code'],
scene,
cmdBar
)
}
)
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene }) => {
async ({ page, homePage, scene, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -352,7 +451,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
await doEditSegmentsByDraggingHandle(page, homePage, [], scene, cmdBar)
}
)
})
@ -361,6 +460,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
page,
editor,
homePage,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -372,6 +473,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -1173,7 +1276,7 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|> line(endAbsolute = [
railWideWidth / 2,
railClampable / 2 + railBaseLength
], $seg01)
], tag = $seg01)
|> line(endAbsolute = [railTop / 2, railBaseLength])
|> line(endAbsolute = [railBaseWidth / 2, railBaseLength])
|> line(endAbsolute = [railBaseWidth / 2, 0])
@ -1354,7 +1457,7 @@ test.describe('multi-profile sketching', () => {
test(
`test it removes half-finished expressions when changing tools in sketch mode`,
{ tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage }) => {
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
@ -1374,7 +1477,10 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
)
})
const [continueProfile2Clk] = scene.makeMouseHelpers(954, 282)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -1385,7 +1491,13 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
const [circlePoint1] = scene.makeMouseHelpers(700, 200)
await test.step('equip circle tool and click first point', async () => {
await toolbar.circleBtn.click()
// await page.waitForTimeout(100)
await expect
.poll(async () => {
await toolbar.circleBtn.click()
return toolbar.circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await page.waitForTimeout(100)
await circlePoint1()
await editor.expectEditor.toContain(
@ -1400,6 +1512,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
const [circle3Point1] = scene.makeMouseHelpers(650, 200)
const [circle3Point2] = scene.makeMouseHelpers(750, 200)
// const [circle3Point3] = scene.makeMouseHelpers(700, 150)
await test.step('equip three point circle tool and click first two points', async () => {
await toolbar.selectCircleThreePoint()
@ -1410,25 +1523,40 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await editor.expectEditor.toContain('profile003 = circleThreePoint(')
})
await test.step('equip line tool and verify three point circle code is removed', async () => {
await test.step('equip line tool and verify three-point circle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain(
'profile003 = circleThreePoint('
)
})
await test.step('equip three-point-arc tool and click first two points', async () => {
await page.waitForTimeout(200)
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await circle3Point1()
await page.waitForTimeout(200)
await circle3Point2()
await editor.expectEditor.toContain('arcTo({')
})
await test.step('equip line tool and verify three-point-arc code is removed after second click', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arcTo({')
})
const [cornerRectPoint1] = scene.makeMouseHelpers(600, 300)
await test.step('equip corner rectangle tool and click first point', async () => {
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await cornerRectPoint1()
await editor.expectEditor.toContain('profile003 = startProfileAt(')
await editor.expectEditor.toContain('profile004 = startProfileAt(')
})
await test.step('equip line tool and verify corner rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
})
const [centerRectPoint1] = scene.makeMouseHelpers(700, 300)
@ -1437,12 +1565,24 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await toolbar.selectCenterRectangle()
await page.waitForTimeout(100)
await centerRectPoint1()
await editor.expectEditor.toContain('profile003 = startProfileAt(')
await editor.expectEditor.toContain('profile004 = startProfileAt(')
})
await test.step('equip line tool and verify center rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
})
await test.step('continue profile002 with the three point arc tool, and then switch back to the line tool to verify it only removes the last expression in the pipe', async () => {
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await continueProfile2Clk()
await page.waitForTimeout(200)
await circle3Point1()
await editor.expectEditor.toContain('arcTo({')
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arcTo({')
await editor.expectEditor.toContain('profile002')
})
}
)
@ -1531,6 +1671,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -1594,7 +1735,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
await test.step('Create a close profile stopping mid profile to equip the tangential arc, then three-point arc, and then back to the line tool', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
@ -1612,12 +1753,45 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
await editor.expectEditor.toContain(
`|> tangentialArcTo([16.61, 4.14], %)`
)
// Add a three-point arc segment
await toolbar.selectThreePointArc()
await page.waitForTimeout(300)
// select end of profile again
await endLineStartTanArc()
await page.waitForTimeout(300)
// Define points for the three-point arc
const [threePointInterior, threePointInteriorMove] =
scene.makeMouseHelpers(600, 200)
const [threePointEnd, threePointEndMove] = scene.makeMouseHelpers(
590,
270
)
// Create the three-point arc
await page.waitForTimeout(300)
await threePointInteriorMove()
await threePointInterior()
await page.waitForTimeout(300)
await threePointEndMove()
await threePointEnd()
await page.waitForTimeout(300)
// Verify the three-point arc was created correctly
await editor.expectEditor.toContain(`|> arcTo(`)
// Switch back to line tool to continue
await toolbar.lineBtn.click()
await page.waitForTimeout(300)
await endArcStartLine()
// Continue with the original line segment
await threePointEnd()
await page.waitForTimeout(300)
await page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line(end = [-11.73, 5.35])`)
await editor.expectEditor.toContain(`|> line(end = [-1.22, 10.85])`)
await startProfile1()
await editor.expectEditor.toContain(
`|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
@ -1819,8 +1993,68 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
)
})
await test.step('double check that circle three point can be unequiped', async () => {
// this was tested implicitly for other tools, but not for circle three point since it's last
await test.step('create three-point arcs in a row without an unequip', async () => {
// Define points for the first three-point arc
const [arc1Point1, arc1Point1Move] = scene.makeMouseHelpers(700, 397)
const [arc1Point2, arc1Point2Move] = scene.makeMouseHelpers(724, 346)
const [arc1Point3, arc1Point3Move] = scene.makeMouseHelpers(785, 415)
// Define points for the second three-point arc
const [arc2Point1, arc2Point1Move] = scene.makeMouseHelpers(792, 225)
const [arc2Point2, arc2Point2Move] = scene.makeMouseHelpers(820, 207)
const [arc2Point3, arc2Point3Move] = scene.makeMouseHelpers(905, 229)
// Select the three-point arc tool
await toolbar.selectThreePointArc()
// Create the first three-point arc
await arc1Point1Move()
await arc1Point1()
await page.waitForTimeout(300)
await arc1Point2Move()
await arc1Point2()
await page.waitForTimeout(300)
await arc1Point3Move()
await arc1Point3()
await page.waitForTimeout(300)
// Verify the first three-point arc was created correctly
await editor.expectEditor.toContain(
`profile011 = startProfileAt([13.56, -9.97], sketch001)
|> arcTo({
interior = [15.19, -6.51],
end = [19.33, -11.19]
}, %)`,
{ shouldNormalise: true }
)
// Create the second three-point arc
await arc2Point1Move()
await arc2Point1()
await page.waitForTimeout(300)
await arc2Point2Move()
await arc2Point2()
await page.waitForTimeout(300)
await arc2Point3Move()
await arc2Point3()
await page.waitForTimeout(300)
// Verify the second three-point arc was created correctly
await editor.expectEditor.toContain(
` |> arcTo({
interior = [19.8, 1.7],
end = [21.7, 2.92]
}, %)
|> arcTo({
interior = [27.47, 1.42],
end = [27.57, 1.52]
}, %)`,
{ shouldNormalise: true }
)
})
await test.step('double check that three-point arc can be unequipped', async () => {
// this was tested implicitly for other tools, but not for three-point arc since it's last
await page.waitForTimeout(300)
await expect
.poll(async () => {
@ -2084,7 +2318,7 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
test(
'can enter sketch when there is an extrude',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, page }) => {
async ({ homePage, scene, toolbar, page, cmdBar }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2121,6 +2355,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2133,9 +2369,11 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600)
await test.step('check the sketch is still drawn properly', async () => {
await scene.expectPixelColor([255, 255, 255], { x: 596, y: 165 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 641, y: 220 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 763, y: 214 }, 15)
await Promise.all([
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 596, y: 165 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 641, y: 220 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 763, y: 214 }, 15),
])
})
}
)
@ -2153,6 +2391,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2165,7 +2405,7 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click()
await toolbar.exitSketch()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
@ -2181,6 +2421,8 @@ extrude001 = extrude(profile003, length = 5)
)`
)
await scene.settled(cmdBar)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
})
})
@ -2288,7 +2530,7 @@ extrude001 = extrude(thePart, length = 75)
test(
'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
async ({ homePage, scene, toolbar, editor, page, cmdBar }) => {
// TODO this test should include a test for selecting revolve walls and caps
await page.addInitScript(async () => {
@ -2373,6 +2615,8 @@ extrude003 = extrude(profile011, length = 2.5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2435,39 +2679,22 @@ extrude003 = extrude(profile011, length = 2.5)
const verifyWallProfilesAreDrawn = async () =>
test.step('verify wall profiles are drawn', async () => {
// open polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 599, y: 168 },
15
)
// closed polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 656, y: 171 },
15
)
// revolved profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 655, y: 264 },
15
)
// extruded profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 808, y: 396 },
15
)
// circle
await scene.expectPixelColor(
[
TEST_COLORS.WHITE,
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
],
{ x: 742, y: 386 },
15
)
await Promise.all([
// open polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 599, y: 168 }, 15),
// closed polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 656, y: 171 }, 15),
// revolved profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 655, y: 264 }, 15),
// extruded profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 808, y: 396 }, 15),
// circle (When entering via the circle, it's selected and therefore blue)
scene.expectPixelColor(
[TEST_COLORS.WHITE, TEST_COLORS.BLUE],
{ x: 742, y: 386 },
15
),
])
})
const verifyCapProfilesAreDrawn = async () =>

View File

@ -31,8 +31,7 @@ test.beforeEach(async ({ page, context }) => {
// Help engine-manager: tear shit down.
test.afterEach(async ({ page }) => {
await page.evaluate(() => {
// @ts-expect-error
window.tearDown()
window.engineCommandManager.tearDown()
})
})
@ -45,7 +44,11 @@ test.setTimeout(60_000)
test.skip(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context, scene, cmdBar }) => {
async ({ page, context, scene, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
}
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -134,6 +137,7 @@ part001 = startSketchOn('-XZ')
storage: 'ascii',
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -146,6 +150,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' },
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -158,6 +163,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' },
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -170,6 +176,7 @@ part001 = startSketchOn('-XZ')
units: 'in',
selection: { type: 'default_scene' },
},
tronApp.projectDirName,
page
)
)
@ -182,6 +189,7 @@ part001 = startSketchOn('-XZ')
units: 'in',
selection: { type: 'default_scene' },
},
tronApp.projectDirName,
page
)
)
@ -193,6 +201,7 @@ part001 = startSketchOn('-XZ')
coords: sysType,
units: 'in',
},
tronApp.projectDirName,
page
)
)
@ -203,6 +212,7 @@ part001 = startSketchOn('-XZ')
storage: 'embedded',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)
@ -213,6 +223,7 @@ part001 = startSketchOn('-XZ')
storage: 'binary',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)
@ -223,6 +234,7 @@ part001 = startSketchOn('-XZ')
storage: 'standard',
presentation: 'pretty',
},
tronApp.projectDirName,
page
)
)
@ -398,9 +410,9 @@ test.describe(
test(
'Draft segments should look right',
{ tag: '@snapshot' },
async ({ page, context, scene, cmdBar }) => {
async ({ page, scene, toolbar }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
// test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -409,6 +421,23 @@ test(
await scene.connectionEstablished()
const startXPx = 600
const [endOfTangentClk, endOfTangentMv] = scene.makeMouseHelpers(
startXPx + PUR * 30,
500 - PUR * 20,
{ steps: 10 }
)
const [threePointArcMidPointClk, threePointArcMidPointMv] =
scene.makeMouseHelpers(800, 250, { steps: 10 })
const [threePointArcEndPointClk, threePointArcEndPointMv] =
scene.makeMouseHelpers(750, 285, { steps: 10 })
const [arcCenterClk, arcCenterMv] = scene.makeMouseHelpers(750, 210, {
steps: 10,
})
const [arcEndClk, arcEndMv] = scene.makeMouseHelpers(750, 150, {
steps: 10,
})
// click on "Start Sketch" button
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
@ -423,7 +452,6 @@ test(
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(page.locator('.cm-content')).toHaveText(code)
@ -459,12 +487,52 @@ test(
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(500)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await endOfTangentMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await endOfTangentClk()
await toolbar.selectThreePointArc()
await page.waitForTimeout(500)
await endOfTangentClk()
await threePointArcMidPointMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await threePointArcMidPointClk()
await page.waitForTimeout(100)
await threePointArcEndPointMv()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await threePointArcEndPointClk()
await page.waitForTimeout(100)
await toolbar.selectArc()
await page.waitForTimeout(100)
// continue the profile
await threePointArcEndPointClk()
await page.waitForTimeout(100)
await arcCenterMv()
await page.waitForTimeout(500)
await arcCenterClk()
await arcEndMv()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await arcEndClk()
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -0,0 +1,33 @@
{
"original_source_code": "sketch001 = startSketchOn('XZ')\nprofile001 = startProfileAt([57.81, 250.51], sketch001)\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\nsketch002 = startSketchOn('XZ')\n |> startProfileAt([-73.64, -42.89], %)\n |> xLine(length = 173.71)\n |> line(end = [-22.12, -94.4])\n |> xLine(length = -156.98)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nsketch003 = startSketchOn('XY')\n |> startProfileAt([52.92, 157.81], %)\n |> angledLine([0, 176.4], %, $rectangleSegmentA001)\n |> angledLine([\n segAng(rectangleSegmentA001) - 90,\n 53.4\n ], %, $rectangleSegmentB001)\n |> angledLine([\n segAng(rectangleSegmentA001),\n -segLen(rectangleSegmentA001)\n ], %, $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude003 = extrude(sketch003, length = 20)\n",
"prompt": "make this neon green please, use #39FF14",
"source_ranges": [
{
"prompt": "The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range most likely refers to \"startProfileAt\" simply because this is the start of the profile that was swept.\nIf you need to operate on this cap, for example for sketching on the face, you can use the special string END i.e. `startSketchOn(someSweepVariable, END)`\nWhen they made this selection they main have intended this surface directly or meant something more general like the sweep body.\nSee later source ranges for more context.",
"range": {
"start": {
"line": 11,
"column": 5
},
"end": {
"line": 11,
"column": 40
}
}
},
{
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
"range": {
"start": {
"line": 17,
"column": 13
},
"end": {
"line": 17,
"column": 44
}
}
}
],
"kcl_version": "0.2.50"
}

View File

@ -84,7 +84,6 @@ test.describe('Test network and connection issues', () => {
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)

View File

@ -5,8 +5,9 @@ import {
_electron as electron,
ElectronApplication,
Locator,
Page,
} from '@playwright/test'
import { test, Page } from './zoo-test'
import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises'
import fsSync from 'fs'
@ -337,7 +338,7 @@ export const getMovementUtils = (opts: any) => {
async function waitForAuthAndLsp(page: Page) {
const waitForLspPromise = page.waitForEvent('console', {
predicate: async (message) => {
predicate: async (message: any) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
@ -420,7 +421,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
const overlay = page.locator(locator)
const bbox = await overlay
.boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
.then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
const angle = Number(await overlay.getAttribute('data-overlay-angle'))
const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px
const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px
@ -437,7 +438,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
page
.locator(locator)
.boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
.then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText()
@ -504,7 +505,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
) => {
if (cdpSession === null) {
// Use a fail safe if we can't simulate disconnect (on Safari)
return page.evaluate('window.tearDown()')
return page.evaluate('window.engineCommandManager.tearDown()')
}
return cdpSession?.send(
@ -631,7 +632,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => {
({ PERSIST_MODELING_CONTEXT, paneIds }: any) => {
localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds })
@ -722,14 +723,14 @@ export const makeTemplate: (
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR)
export const getPlaywrightDownloadDir = (rootDir: string) => {
return path.resolve(rootDir, PLAYWRIGHT_DOWNLOAD_DIR)
}
const moveDownloadedFileTo = async (page: Page, toLocation: string) => {
const moveDownloadedFileTo = async (rootDir: string, toLocation: string) => {
await fsp.mkdir(path.dirname(toLocation), { recursive: true })
const downloadDir = getPlaywrightDownloadDir(page)
const downloadDir = getPlaywrightDownloadDir(rootDir)
// Expect there to be at least one file
await expect
@ -756,6 +757,7 @@ export interface Paths {
export const doExport = async (
output: Models['OutputFormat_type'],
rootDir: string,
page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
@ -836,7 +838,7 @@ export const doExport = async (
// (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation)
await moveDownloadedFileTo(rootDir, downloadLocation)
}
return {
@ -859,12 +861,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
downloadThroughput: -1,
uploadThroughput: -1,
})
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page.waitForTimeout(3000)
await testInfo.tronApp?.close()
}
// settingsOverrides may need to be augmented to take more generic items,
@ -936,107 +932,11 @@ let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | undefined = undefined
let page: Page | undefined = undefined
export async function setupElectron({
testInfo,
cleanProjectDir = true,
appSettings,
viewport,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
viewport: {
width: number
height: number
}
}): Promise<{
electronApp: ElectronApplication
context: BrowserContext
page: Page
dir: string
}> {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
try {
if (fsSync.existsSync(projectDirName) && cleanProjectDir) {
await fsp.rm(projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
if (cleanProjectDir) {
await fsp.mkdir(projectDirName)
}
const options = {
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: viewport,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!electronApp) {
electronApp = await electron.launch(options)
}
if (!context || !page) {
context = electronApp.context()
page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
}
if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = settingsToToml(
appSettings
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}
return { electronApp, page, context, dir: projectDirName }
}
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => {
// No idea wtf exception is
page.on('pageerror', (exception: any) => {
if (isErrorWhitelisted(exception)) {
return
}

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { deg, getUtils, wiggleMove } from './test-utils'
import { LineInputsType } from 'lang/std/sketchcombos'
@ -158,7 +159,6 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
const unconstrainedLocator = page.locator(
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
)
await expect(unconstrainedLocator).toBeVisible()
await unconstrainedLocator.hover()
await expect(
await page.getByTestId('constraint-symbol-popover').count()
@ -273,8 +273,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
const line = await u.getBoundingBox('[data-overlay-index="0"]')
ang = await u.getAngle('[data-overlay-index="0"]')
console.log('line1', line, ang)
await clickConstrained({
hoverPos: { x: line.x, y: line.y },
@ -296,8 +296,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-index="0"]',
})
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
ang = await u.getAngle(`[data-overlay-index="1"]`)
const angledLine = await u.getBoundingBox('[data-overlay-index="1"]')
ang = await u.getAngle('[data-overlay-index="1"]')
console.log('angledLine1')
await clickConstrained({
hoverPos: { x: angledLine.x, y: angledLine.y },
@ -326,8 +326,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await page.mouse.move(700, 250)
await page.waitForTimeout(100)
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
ang = await u.getAngle(`[data-overlay-index="2"]`)
let lineTo = await u.getBoundingBox('[data-overlay-index="2"]')
ang = await u.getAngle('[data-overlay-index="2"]')
console.log('lineTo1')
await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y },
@ -352,8 +352,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="2"]',
})
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
ang = await u.getAngle(`[data-overlay-index="3"]`)
const xLineTo = await u.getBoundingBox('[data-overlay-index="3"]')
ang = await u.getAngle('[data-overlay-index="3"]')
console.log('xlineTo1')
await clickConstrained({
hoverPos: { x: xLineTo.x, y: xLineTo.y },
@ -418,8 +418,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
ang = await u.getAngle(`[data-overlay-index="4"]`)
const yLineTo = await u.getBoundingBox('[data-overlay-index="4"]')
ang = await u.getAngle('[data-overlay-index="4"]')
console.log('ylineTo1')
await clickUnconstrained({
hoverPos: { x: yLineTo.x, y: yLineTo.y - 200 },
@ -431,8 +431,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="4"]',
})
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
ang = await u.getAngle(`[data-overlay-index="5"]`)
const xLine = await u.getBoundingBox('[data-overlay-index="5"]')
ang = await u.getAngle('[data-overlay-index="5"]')
console.log('xline')
await clickUnconstrained({
hoverPos: { x: xLine.x, y: xLine.y },
@ -500,8 +500,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
ang = await u.getAngle(`[data-overlay-index="6"]`)
const yLine = await u.getBoundingBox('[data-overlay-index="6"]')
ang = await u.getAngle('[data-overlay-index="6"]')
console.log('yline1')
await clickConstrained({
hoverPos: { x: yLine.x, y: yLine.y },
@ -514,9 +514,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineOfXLength = await u.getBoundingBox(
`[data-overlay-index="7"]`
'[data-overlay-index="7"]'
)
ang = await u.getAngle(`[data-overlay-index="7"]`)
ang = await u.getAngle('[data-overlay-index="7"]')
console.log('angledLineOfXLength1')
await clickConstrained({
hoverPos: { x: angledLineOfXLength.x, y: angledLineOfXLength.y },
@ -546,9 +546,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineOfYLength = await u.getBoundingBox(
`[data-overlay-index="8"]`
'[data-overlay-index="8"]'
)
ang = await u.getAngle(`[data-overlay-index="8"]`)
ang = await u.getAngle('[data-overlay-index="8"]')
console.log('angledLineOfYLength1')
await clickUnconstrained({
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y },
@ -631,8 +631,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
ang = await u.getAngle(`[data-overlay-index="9"]`)
const angledLineToX = await u.getBoundingBox('[data-overlay-index="9"]')
ang = await u.getAngle('[data-overlay-index="9"]')
console.log('angledLineToX')
await clickConstrained({
hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
@ -658,9 +658,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineToY = await u.getBoundingBox(
`[data-overlay-index="10"]`
'[data-overlay-index="10"]'
)
ang = await u.getAngle(`[data-overlay-index="10"]`)
ang = await u.getAngle('[data-overlay-index="10"]')
console.log('angledLineToY')
await clickUnconstrained({
hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
@ -688,9 +688,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineThatIntersects = await u.getBoundingBox(
`[data-overlay-index="11"]`
'[data-overlay-index="11"]'
)
ang = await u.getAngle(`[data-overlay-index="11"]`)
ang = await u.getAngle('[data-overlay-index="11"]')
console.log('angledLineThatIntersects')
await clickUnconstrained({
hoverPos: {
@ -820,6 +820,138 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="12"]',
})
})
test('for segment [arcTo]', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([56.37, 120.33], sketch001)
|> line(end = [162.86, 106.48])
|> arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)
|> yLine(-131.54, %)
|> arc({
radius = 126.46,
angleStart = 33.53,
angleEnd = -141.07
}, %)
`
)
localStorage.setItem('disableAxis', 'true')
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
// wait for execution done
await page.getByText('line(end = [162.86, 106.48])').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
const clickUnconstrained = _clickUnconstrained(page, editor)
const clickConstrained = _clickConstrained(page, editor)
const arcTo = await u.getBoundingBox('[data-overlay-index="1"]')
let ang = await u.getAngle('[data-overlay-index="1"]')
console.log('arcTo interior x')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
ang: ang,
steps: 6,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo interior y')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, yAbs001],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
ang: ang,
steps: 10,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo end x')
await clickConstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
ang: ang + 180,
steps: 6,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo end y')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, yAbs002]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
ang: ang + 180,
steps: 10,
locator: '[data-overlay-toolbar-index="1"]',
})
})
test('for segment [circle]', async ({ page, editor, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -927,36 +1059,55 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
shouldNormalise: true,
})
await page.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`).click()
await page
.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`)
.first()
.click()
await page.getByText('Delete Segment').click()
await editor.expectEditor.not.toContain(codeToBeDeleted, {
shouldNormalise: true,
})
}
test('all segment types', async ({ page, editor, homePage }) => {
test('all segment types', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [0.5, -14 + 0])
|> angledLine({ angle = 3 + 0, length = 32 + 0 }, %)
|> line(endAbsolute = [33, 11.5 + 0])
|> xLine(endAbsolute = 9 - 5)
|> yLine(endAbsolute = -10.77, tag = $a)
|> xLine(length = 26.04)
|> yLine(length = 21.14 + 0)
|> angledLineOfXLength({ angle = 181 + 0, length = 23.14 }, %)
|> angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)
|> angledLineToX({ angle = 3 + 0, to = 26 }, %)
|> angledLineToY({ angle = 89, to = 9.14 + 0 }, %)
|> angledLineThatIntersects({
angle = 4.14,
intersectTag = a,
offset = 9
}, %)
|> tangentialArcTo([3.14 + 13, 1.14], %)
|>startProfileAt([0, 0], %)
|> line(end = [0.5, -14 + 0])
|> angledLine({ angle = 3 + 0, length = 32 + 0 }, %)
|> line(endAbsolute = [33, 11.5 + 0])
|> xLine(endAbsolute = 9 - 5)
|> yLine(endAbsolute = -10.77, tag = $a)
|> xLine(length = 26.04)
|> yLine(length = 21.14 + 0)
|> angledLineOfXLength({ angle = 181 + 0, length = 23.14 }, %)
|> angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)
|> angledLineToX({ angle = 3 + 0, to = 26 }, %)
|> angledLineToY({ angle = 89, to = 9.14 + 0 }, %)
|> angledLineThatIntersects({
angle = 4.14,
intersectTag = a,
offset = 9
}, %)
|> tangentialArcTo([3.14 + 13, 1.14], %)
|> arcTo({
interior = [16.25, 5.12],
end = [21.61, 4.15]
}, %)
|> arc({
radius = 9.03,
angleStart = 40.27,
angleEnd = -38.05
}, %)
`
)
localStorage.setItem('disableAxis', 'true')
@ -965,27 +1116,55 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await u.waitForPageLoad()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByText('xLine(endAbsolute = 9 - 5)').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
await expect(page.getByTestId('segment-overlay')).toHaveCount(16)
const deleteSegmentSequence = _deleteSegmentSequence(page, editor)
let segmentToDelete
const getOverlayByIndex = (index: number) =>
u.getBoundingBox(`[data-overlay-index="${index}"]`)
segmentToDelete = await getOverlayByIndex(14)
let ang = await u.getAngle('[data-overlay-index="14"]')
await editor.scrollToText('angleEnd')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `arc({
radius = 9.03,
angleStart = 40.27,
angleEnd = -38.05
}, %)`,
stdLibFnName: 'arc',
ang: ang + 180,
steps: 6,
locator: '[data-overlay-toolbar-index="14"]',
})
segmentToDelete = await getOverlayByIndex(13)
ang = await u.getAngle('[data-overlay-index="13"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `arcTo({
interior = [16.25, 5.12],
end = [21.61, 4.15]
}, %)`,
stdLibFnName: 'arcTo',
ang: ang,
steps: 6,
locator: '[data-overlay-toolbar-index="13"]',
})
segmentToDelete = await getOverlayByIndex(12)
let ang = await u.getAngle(`[data-overlay-index="${12}"]`)
ang = await u.getAngle('[data-overlay-index="12"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'tangentialArcTo([3.14 + 13, 1.14], %)',
@ -996,7 +1175,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(11)
ang = await u.getAngle(`[data-overlay-index="${11}"]`)
ang = await u.getAngle('[data-overlay-index="11"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `angledLineThatIntersects({
@ -1011,7 +1190,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(10)
ang = await u.getAngle(`[data-overlay-index="${10}"]`)
ang = await u.getAngle('[data-overlay-index="10"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToY({ angle = 89, to = 9.14 + 0 }, %)',
@ -1021,7 +1200,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(9)
ang = await u.getAngle(`[data-overlay-index="${9}"]`)
ang = await u.getAngle('[data-overlay-index="9"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToX({ angle = 3 + 0, to = 26 }, %)',
@ -1031,7 +1210,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(8)
ang = await u.getAngle(`[data-overlay-index="${8}"]`)
ang = await u.getAngle('[data-overlay-index="8"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted:
@ -1042,7 +1221,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(7)
ang = await u.getAngle(`[data-overlay-index="${7}"]`)
ang = await u.getAngle('[data-overlay-index="7"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted:
@ -1053,7 +1232,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(6)
ang = await u.getAngle(`[data-overlay-index="${6}"]`)
ang = await u.getAngle('[data-overlay-index="6"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'yLine(length = 21.14 + 0)',
@ -1063,7 +1242,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(5)
ang = await u.getAngle(`[data-overlay-index="${5}"]`)
ang = await u.getAngle('[data-overlay-index="5"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(length = 26.04)',
@ -1073,7 +1252,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(4)
ang = await u.getAngle(`[data-overlay-index="${4}"]`)
ang = await u.getAngle('[data-overlay-index="4"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'yLine(endAbsolute = -10.77, tag = $a)',
@ -1083,7 +1262,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(3)
ang = await u.getAngle(`[data-overlay-index="${3}"]`)
ang = await u.getAngle('[data-overlay-index="3"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(endAbsolute = 9 - 5)',
@ -1093,7 +1272,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(2)
ang = await u.getAngle(`[data-overlay-index="${2}"]`)
ang = await u.getAngle('[data-overlay-index="2"]')
await expect(page.getByText('Added variable')).not.toBeVisible()
const hoverPos = { x: segmentToDelete.x, y: segmentToDelete.y }
@ -1126,7 +1305,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(1)
ang = await u.getAngle(`[data-overlay-index="${1}"]`)
ang = await u.getAngle('[data-overlay-index="1"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLine({ angle = 3 + 0, length = 32 + 0 }, %)',
@ -1136,7 +1315,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(0)
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
ang = await u.getAngle('[data-overlay-index="0"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'line(end = [0.5, -14 + 0])',
@ -1365,7 +1544,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await expect(page.getByText('Added variable')).not.toBeVisible()
const hoverPos = await u.getBoundingBox(`[data-overlay-index="0"]`)
let ang = await u.getAngle(`[data-overlay-index="${0}"]`)
let ang = await u.getAngle('[data-overlay-index="0"]')
ang += 180
await page.mouse.move(0, 0)

View File

@ -257,6 +257,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
page,
homePage,
scene,
cmdBar,
}) => {
test.setTimeout(90_000)
const u = await getUtils(page)
@ -352,28 +353,15 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await scene.settled(cmdBar)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const camPosition1 = async () => {
await scene.moveCameraTo(
{ x: 1139.49, y: -7053, z: 8597.31 },
{ x: -2206.68, y: -1298.36, z: 60 }
)
}
await camPosition1()
const revolve = { x: 635, y: 253 }
const parentExtrude = { x: 915, y: 133 }
@ -386,7 +374,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [0, -pipeLength])'
)
await u.clearCommandLogs()
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
@ -399,11 +387,12 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
// and replace the sketch on face with a hard coded custom plane, but since there was a sketch on that plane maybe it
// should have delete the sketch? it's broken atm, but not sure if worth fixing since desired behaviour is a little
// vague
// // DELETE PARENT EXTRUDE
// DELETE PARENT EXTRUDE
// await camPosition2()
// await page.mouse.click(parentExtrude.x, parentExtrude.y)
// await page.waitForTimeout(100)
// await expect(page.locator('.cm-activeLine')).toHaveText(
// '|> line(end = [170.36, -121.61], tag = $seg01)'
// '|> line(end = [112.54, 127.64], tag = $seg02)'
// )
// await u.clearCommandLogs()
// await page.keyboard.press('Backspace')
@ -464,68 +453,71 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
})
test.fixme(
"Deleting solid that the AST mod can't handle results in a toast message",
async ({ page, homePage }) => {
'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',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
launderExtrudeThroughVar = extrude001
sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|> startProfileAt([-100.54, 16.99], %)
|> line(end = [0, 20.03])
|> line(end = [62.61, 0], tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
`part001 = startSketchOn('XY')
yo = startProfileAt([4.83, 12.56], part001)
|> line(end = [15.1, 2.48])
|> line(end = [3.15, -9.85], tag = $seg01)
|> line(end = [-15.17, -4.1])
|> angledLine([segAng(seg01), 12.35], %, $seg02)
|> line(end = [-13.02, 10.03])
|> close()
yoo = extrude(yo, length = 4)
sketch002 = startSketchOn(yoo, seg02)
sketch001 = startSketchOn(yoo, 'END')
profile002 = startProfileAt([-11.08, 2.39], sketch002)
|> line(end = [4.89, 0.9])
|> line(end = [-0.61, -2.41])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile002, length = 15)
profile001 = startProfileAt([7.49, 9.96], sketch001)
|> angledLine([0, 5.05], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.81
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
const extrudeWall = { x: 575, y: 238 }
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
// 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 = [170.36, -121.61], tag = $seg01)'
'|> line(end = [-15.17, -4.1])'
)
await u.clearCommandLogs()
await u.openAndClearDebugPanel()
await page.keyboard.press('Delete')
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(200)
await expect(page.getByText('Unable to delete selection')).toBeVisible()
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 ({

View File

@ -0,0 +1 @@
part001 = startSketchOn('XY')yo = startProfileAt([4.83, 12.56], part001) |> line(end = [15.1, 2.48]) |> line(end = [3.15, -9.85], tag = $seg01) |> line(end = [-15.17, -4.1]) |> angledLine([segAng(seg01), 12.35], %, $seg02) |> line(end = [-13.02, 10.03]) |> close()sketch002 = startSketchOn({ plane = { origin = { x = 7.49, y = 2.4, z = 0 }, xAxis = { x = -0.3, y = 0.95, z = 0 }, yAxis = { x = 0, y = 0, z = 1 }, zAxis = { x = 0.95, y = 0.3, z = 0 } }})sketch001 = startSketchOn({ plane = { origin = { x = 0, y = 0, z = 4 }, xAxis = { x = 1, y = 0, z = 0 }, yAxis = { x = 0, y = 1, z = 0 }, zAxis = { x = 0, y = 0, z = 1 } }})profile002 = startProfileAt([-11.08, 2.39], sketch002) |> line(end = [4.89, 0.9]) |> line(end = [-0.61, -2.41]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> close()extrude001 = extrude(profile002, length = 15)profile001 = startProfileAt([7.49, 9.96], sketch001) |> angledLine([0, 5.05], %, $rectangleSegmentA001) |> angledLine([ segAng(rectangleSegmentA001) - 90, 4.81 ], %) |> angledLine([ segAng(rectangleSegmentA001), -segLen(rectangleSegmentA001) ], %) |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> close()

View File

@ -20,35 +20,40 @@ import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
test.describe('Testing settings', () => {
test(
'Stored settings are validated and fall back to defaults',
test('Stored settings are validated and fall back to defaults', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// Override beforeEach test setup
// with corrupted settings
{
appSettings: TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>,
},
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await tronApp.cleanProjectDir(
TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>
)
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setBodyDimensions({ width: 1200, height: 500 })
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
)
expect(storedSettings.settings?.app?.theme).toBe('dark')
expect(storedSettings.settings?.app?.theme).toBe('dark')
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
}
)
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
})
// The behavior is actually broken. Parent always takes precedence
test.fixme(
@ -357,8 +362,6 @@ test.describe('Testing settings', () => {
`Load desktop app with no settings file`,
{
tag: '@electron',
// This is what makes no settings file get created
cleanProjectDir: false,
},
async ({ page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -379,13 +382,17 @@ test.describe('Testing settings', () => {
`Load desktop app with a settings file, but no project directory setting`,
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
theme_color: '259',
},
},
},
async ({ context, page }, testInfo) => {
})
await page.setBodyDimensions({ width: 1200, height: 500 })
// Selectors and constants
@ -405,15 +412,20 @@ test.describe('Testing settings', () => {
'user settings reload on external change, on project and modeling view',
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
// Doesn't matter what you set it to. It will
// default to 264.5
theme_color: '0',
},
},
},
async ({ context, page }, testInfo) => {
})
const { dir: projectDirName } = await context.folderSetupFn(
async () => {}
)
@ -783,128 +795,136 @@ test.describe('Testing settings', () => {
})
})
test(
`Changing system theme preferences (via media query) should update UI and stream`,
{
// Override the settings so that the theme is set to `system`
appSettings: TEST_SETTINGS_DEFAULT_THEME,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
// 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 lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
test(
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`,
{
await tronApp.cleanProjectDir({
// Override the settings so that the theme is set to `system`
...TEST_SETTINGS_DEFAULT_THEME,
})
const u = await getUtils(page)
// 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 lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
})
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override beforeEach test setup
// with debug panel open
// but "show debug panel" set to false
appSettings: {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
},
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
})
await context.addInitScript(async () => {
localStorage.setItem(
'persistModelingContext',
'{"openPanes":["debug"]}'
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
).toBeVisible()
}
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
).toBeVisible()
}
)
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
})
test(`Change inline units setting`, async ({
page,

View File

@ -1,9 +1,10 @@
import { test, expect, Page } from './zoo-test'
import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { getUtils, createProject } from './test-utils'
import { join } from 'path'
import fs from 'fs'
test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test.fixme('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page)
@ -429,7 +430,8 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
await expect(page.getByText(promptWithNewline)).toBeVisible()
})
test(
// This will be fine once greg makes prompt at top of file deterministic
test.fixme(
'can do many at once and get many prompts back, and interact with many',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
@ -490,8 +492,15 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
// Click the button.
await copyToClipboardButton.first().click()
// Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x8`)
// Do NOT do AI tests like this: "Expect the code to be pasted."
// Reason: AI tests are NONDETERMINISTIC. Thus we need to be as most
// general as we can for the assertion.
// We can use Kolmogorov complexity as a measurement of the
// "probably most minimal version of this program" to have a lower
// bound to work with. It is completely by feel because there are
// no proofs that any program is its smallest self.
const code2x8 = await page.locator('.cm-content').innerText()
await expect(code2x8.length).toBeGreaterThan(249)
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
@ -504,7 +513,8 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
await copyToClipboardButton.click()
// Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x4`)
const code2x4 = await page.locator('.cm-content').innerText()
await expect(code2x4.length).toBeGreaterThan(249)
}
)

View File

@ -35,7 +35,7 @@ test.fixme('Units menu', async ({ page, homePage }) => {
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
async ({ page, homePage, tronApp }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -92,12 +92,17 @@ part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
if (!tronApp?.projectDirName) {
fail()
}
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
tronApp?.projectDirName,
page
)
}
@ -465,7 +470,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => {
test('Sketch on face', async ({ page, homePage, scene, cmdBar, toolbar }) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -491,25 +496,22 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
await scene.connectionEstablished()
await scene.settled(cmdBar)
let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(625, 165),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
await toolbar.startSketchThenCallbackThenWaitUntilReady(async () => {
await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(625, 165),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
})
await page.waitForTimeout(300)
const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242]

View File

@ -1,21 +1,11 @@
import {
test as playwrightTestFn,
TestInfo as TestInfoPlaywright,
BrowserContext as BrowserContextPlaywright,
Page as PagePlaywright,
TestDetails as TestDetailsPlaywright,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
ElectronApplication,
} from '@playwright/test'
/* eslint-disable react-hooks/rules-of-hooks */
import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
import {
fixtures,
fixturesBasedOnProcessEnvPlatform,
Fixtures,
AuthenticatedTronApp,
AuthenticatedApp,
ElectronZoo,
} from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -23,9 +13,6 @@ import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test'
declare module '@playwright/test' {
interface TestInfo {
tronApp?: AuthenticatedTronApp
}
interface BrowserContext {
folderSetupFn: (
cb: (dir: string) => Promise<void>
@ -41,288 +28,31 @@ declare module '@playwright/test' {
}
}
export type TestInfo = TestInfoPlaywright
export type BrowserContext = BrowserContextPlaywright
export type Page = PagePlaywright
export type TestDetails = TestDetailsPlaywright & {
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
}
// Each worker spawns a new thread, which will spawn its own ElectronZoo.
// So in some sense there is an implicit pool.
// For example, the variable just beneath this text is reused many times
// *for one worker*.
const electronZooInstance = new ElectronZoo()
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed.
const pwTestFnWithFixtures = playwrightTestFn.extend<Fixtures>(fixtures)
// In JavaScript you cannot replace a function's body only (despite functions
// are themselves objects, which you'd expect a body property or something...)
// So we must redefine the function and then re-attach properties.
type PWFunction = (
args: PlaywrightTestArgs &
Fixtures &
PlaywrightWorkerArgs &
PlaywrightTestOptions &
PlaywrightWorkerOptions & {
electronApp?: ElectronApplication
},
testInfo: TestInfo
) => void | Promise<void>
let firstUrl = ''
export const test = (
desc: string,
objOrFn: PWFunction | TestDetails,
fnMaybe?: PWFunction
) => {
const hasTestConf = typeof objOrFn === 'object'
const fn = hasTestConf ? fnMaybe : objOrFn
return pwTestFnWithFixtures(
desc,
hasTestConf ? objOrFn : {},
async (
{
page,
context,
cmdBar,
editor,
toolbar,
scene,
homePage,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
) => {
// To switch to web, use PLATFORM=web environment variable.
// Only use this for debugging, since the playwright tracer is busted
// for electron.
let tronApp
if (process.env.PLATFORM === 'web') {
tronApp = new AuthenticatedApp(context, page, testInfo)
} else {
tronApp = new AuthenticatedTronApp(context, page, testInfo)
}
const fixtures: Fixtures = { cmdBar, editor, toolbar, scene, homePage }
if (tronApp instanceof AuthenticatedTronApp) {
const options = {
fixtures,
}
if (hasTestConf) {
Object.assign(options, {
appSettings: objOrFn?.appSettings,
cleanProjectDir: objOrFn?.cleanProjectDir,
})
}
await tronApp.initialise(options)
} else {
await tronApp.initialise('')
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = tronApp.context.addInitScript
tronApp.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = tronApp.page.addInitScript
tronApp.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
tronApp.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await tronApp.page.setViewportSize(dims)
if (!(tronApp instanceof AuthenticatedTronApp)) {
return
}
await tronApp.electronApp?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return tronApp.page.evaluate(
async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
},
dims
)
}
await tronApp.page.setBodyDimensions(tronApp.viewPortSize)
// We need to expose this in order for some tests that require folder
// creation. Before they used to do this by their own electronSetup({...})
// calls.
if (tronApp instanceof AuthenticatedTronApp) {
tronApp.context.folderSetupFn = async function (fn) {
return fn(tronApp.dir)
.then(() => tronApp.page.reload())
.then(() => ({
dir: tronApp.dir,
}))
}
}
if (!firstUrl) {
await tronApp.page.getByText('Your Projects').count()
firstUrl = tronApp.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await tronApp.electronApp?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, tronApp.dir)
// Always start at the root view
await tronApp.page.goto(firstUrl)
// Force a hard reload, destroying the stream and other state
await tronApp.page.reload()
// tsc aint smart enough to know this'll never be undefined
// but I dont blame it, the logic to know is complex
if (fn) {
await fn(
{
context: tronApp.context,
page: tronApp.page,
electronApp:
tronApp instanceof AuthenticatedTronApp
? tronApp.electronApp
: undefined,
...fixtures,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
)
}
testInfo.tronApp =
tronApp instanceof AuthenticatedTronApp ? tronApp : undefined
const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
}>({
tronApp: async ({}, use, testInfo) => {
if (process.env.PLATFORM === 'web') {
await use(undefined)
return
}
)
}
type ZooTest = typeof test
await electronZooInstance.createInstanceIfMissing(testInfo)
await use(electronZooInstance)
await electronZooInstance.makeAvailableAgain()
},
})
test.describe = pwTestFnWithFixtures.describe
test.beforeEach = pwTestFnWithFixtures.beforeEach
test.afterEach = pwTestFnWithFixtures.afterEach
test.step = pwTestFnWithFixtures.step
test.skip = pwTestFnWithFixtures.skip
test.setTimeout = pwTestFnWithFixtures.setTimeout
test.fixme = pwTestFnWithFixtures.fixme as unknown as ZooTest
test.only = pwTestFnWithFixtures.only
test.fail = pwTestFnWithFixtures.fail
test.slow = pwTestFnWithFixtures.slow
test.beforeAll = pwTestFnWithFixtures.beforeAll
test.afterAll = pwTestFnWithFixtures.afterAll
test.use = pwTestFnWithFixtures.use
test.expect = pwTestFnWithFixtures.expect
test.extend = pwTestFnWithFixtures.extend
test.info = pwTestFnWithFixtures.info
const test = playwrightTestFnWithFixtures_.extend<Fixtures>(
fixturesBasedOnProcessEnvPlatform
)
export { test }

View File

@ -12,17 +12,17 @@
"main": ".vite/build/main.js",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.0",
"@codemirror/language": "^6.10.8",
"@codemirror/language": "^6.11.0",
"@codemirror/lint": "^6.8.4",
"@codemirror/search": "^6.5.10",
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@csstools/postcss-oklab-function": "^4.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
@ -35,35 +35,35 @@
"@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^4.1.1",
"bonjour-service": "^1.2.1",
"bonjour-service": "^1.3.0",
"chokidar": "^4.0.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"diff": "^7.0.0",
"electron-updater": "^6.6.0",
"fuse.js": "^7.0.0",
"fuse.js": "^7.1.0",
"html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"minimist": "^1.2.8",
"openid-client": "^5.6.5",
"re-resizable": "^6.9.11",
"re-resizable": "^6.11.2",
"react": "^18.3.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hot-toast": "^2.5.2",
"react-hotkeys-hook": "^4.6.1",
"react-json-view": "^1.21.3",
"react-modal": "^3.16.3",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.28.0",
"sketch-helpers": "^0.0.4",
"three": "^0.172.0",
"three": "^0.174.0",
"ua-parser-js": "^1.0.37",
"uuid": "^11.0.2",
"uuid": "^11.1.0",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8",
"vscode-uri": "^3.1.0",
"web-vitals": "^3.5.2",
"xstate": "^5.19.2",
"yargs": "^17.7.2"
@ -93,11 +93,9 @@
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "rm -rf public/kcl-samples* && curl -L -o public/kcl-samples.zip https://github.com/KittyCAD/kcl-samples/archive/refs/heads/achalmers/kw-args-xylineto.zip && unzip -o public/kcl-samples.zip -d public && mv public/kcl-samples-* public/kcl-samples",
"build:wasm-dev": "yarn wasm-prep && (cd rust && wasm-pack build kcl-wasm-lib --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm:nocopy": "yarn wasm-prep && cd rust && wasm-pack build kcl-wasm-lib --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings",
"build:wasm": "yarn build:wasm:nocopy && cp rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm public && yarn fmt",
"build:wasm:windows": "yarn install:wasm-pack:cargo && yarn build:wasm:nocopy && ./scripts/copy-wasm.ps1 && yarn fmt",
"build:wasm": "./scripts/build-wasm.sh",
"build:wasm:windows": "./scripts/build-wasm.ps1",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf rust/kcl-wasm-lib/pkg && mkdirp rust/kcl-wasm-lib/pkg && rimraf rust/kcl-lib/bindings",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
@ -106,7 +104,7 @@
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "./node_modules/.bin/electron-rebuild",
"postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",
@ -153,16 +151,16 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.6.1",
"@electron-forge/plugin-vite": "^7.6.1",
"@babel/preset-env": "^7.26.9",
"@electron-forge/cli": "^7.7.0",
"@electron-forge/plugin-fuses": "^7.7.0",
"@electron-forge/plugin-vite": "^7.7.0",
"@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.2",
"@nabla/vite-plugin-eslint": "^2.0.5",
"@playwright/test": "^1.49.0",
"@playwright/test": "^1.51.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/diff": "^7.0.1",
@ -176,7 +174,7 @@
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.1",
"@types/react-modal": "^3.16.3",
"@types/three": "^0.172.0",
"@types/three": "^0.174.0",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5",
@ -189,11 +187,11 @@
"electron-builder": "^26.0.6",
"eslint": "^8.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-perf": "^3.3.3",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"eslint-plugin-testing-library": "^7.1.1",
@ -210,8 +208,8 @@
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.17",
"ts-node": "^10.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.23.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -19,14 +19,14 @@
"private": false,
"dependencies": {
"@codemirror/autocomplete": "6.18.6",
"@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@lezer/highlight": "^1.2.0",
"@ts-stack/markdown": "^1.5.0",
"json-rpc-2.0": "^1.7.0",
"typescript": "^5.7.2",
"typescript": "^5.8.2",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8"
"vscode-uri": "^3.1.0"
},
"devDependencies": {
"@types/node": "^22.13.9",

View File

@ -12,10 +12,10 @@
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.2":
version "6.10.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.0.tgz#5ae90972601497f4575f30811519d720bf7232c9"
integrity sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
@ -24,10 +24,12 @@
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1":
version "6.4.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0":
version "6.28.2"
@ -82,6 +84,11 @@
dependencies:
"@lezer/common" "^1.0.0"
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@ts-stack/markdown@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@ts-stack/markdown/-/markdown-1.5.0.tgz#5dc298a20dc3dc040143c5a5948201eb6bf5419d"
@ -182,10 +189,10 @@ tslib@^2.3.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
typescript@^5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
undici-types@~6.20.0:
version "6.20.0"
@ -215,10 +222,10 @@ vscode-languageserver-types@3.17.5:
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
vscode-uri@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
vscode-uri@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
w3c-keyname@^2.2.4:
version "2.2.8"

View File

@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import { platform } from 'os'
/**
* See https://playwright.dev/docs/test-configuration.
@ -13,7 +14,7 @@ export default defineConfig({
/* Do not retry */
retries: 0,
/* Different amount of parallelism on CI and local. */
workers: 8,
workers: platform() === 'win32' ? 1 : 2,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['dot'],

View File

@ -23,14 +23,14 @@ KCL samples conform to a set of style guidelines to ensure consistency and reada
When you submit a PR to add or modify KCL samples, images and STEP files will be generated and added to the repository automatically.
---
#### [3d-boaty](3d-boaty/main.kcl) ([step](step/3d-boaty.step)) ([screenshot](screenshots/3d-boaty.png))
[![3d-boaty](screenshots/3d-boaty.png)](3d-boaty/main.kcl)
#### [80-20-rail](80-20-rail/main.kcl) ([step](step/80-20-rail.step)) ([screenshot](screenshots/80-20-rail.png))
[![80-20-rail](screenshots/80-20-rail.png)](80-20-rail/main.kcl)
#### [a-parametric-bearing-pillow-block](a-parametric-bearing-pillow-block/main.kcl) ([step](step/a-parametric-bearing-pillow-block.step)) ([screenshot](screenshots/a-parametric-bearing-pillow-block.png))
[![a-parametric-bearing-pillow-block](screenshots/a-parametric-bearing-pillow-block.png)](a-parametric-bearing-pillow-block/main.kcl)
#### [ball-bearing](ball-bearing/main.kcl) ([step](step/ball-bearing.step)) ([screenshot](screenshots/ball-bearing.png))
[![ball-bearing](screenshots/ball-bearing.png)](ball-bearing/main.kcl)
#### [bench](bench/main.kcl) ([step](step/bench.step)) ([screenshot](screenshots/bench.png))
[![bench](screenshots/bench.png)](bench/main.kcl)
#### [bracket](bracket/main.kcl) ([step](step/bracket.step)) ([screenshot](screenshots/bracket.png))
[![bracket](screenshots/bracket.png)](bracket/main.kcl)
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([step](step/car-wheel-assembly.step)) ([screenshot](screenshots/car-wheel-assembly.png))
@ -45,10 +45,8 @@ When you submit a PR to add or modify KCL samples, images and STEP files will be
[![enclosure](screenshots/enclosure.png)](enclosure/main.kcl)
#### [exhaust-manifold](exhaust-manifold/main.kcl) ([step](step/exhaust-manifold.step)) ([screenshot](screenshots/exhaust-manifold.png))
[![exhaust-manifold](screenshots/exhaust-manifold.png)](exhaust-manifold/main.kcl)
#### [flange-with-patterns](flange-with-patterns/main.kcl) ([step](step/flange-with-patterns.step)) ([screenshot](screenshots/flange-with-patterns.png))
[![flange-with-patterns](screenshots/flange-with-patterns.png)](flange-with-patterns/main.kcl)
#### [flange-xy](flange-xy/main.kcl) ([step](step/flange-xy.step)) ([screenshot](screenshots/flange-xy.png))
[![flange-xy](screenshots/flange-xy.png)](flange-xy/main.kcl)
#### [flange](flange/main.kcl) ([step](step/flange.step)) ([screenshot](screenshots/flange.png))
[![flange](screenshots/flange.png)](flange/main.kcl)
#### [focusrite-scarlett-mounting-bracket](focusrite-scarlett-mounting-bracket/main.kcl) ([step](step/focusrite-scarlett-mounting-bracket.step)) ([screenshot](screenshots/focusrite-scarlett-mounting-bracket.png))
[![focusrite-scarlett-mounting-bracket](screenshots/focusrite-scarlett-mounting-bracket.png)](focusrite-scarlett-mounting-bracket/main.kcl)
#### [food-service-spatula](food-service-spatula/main.kcl) ([step](step/food-service-spatula.step)) ([screenshot](screenshots/food-service-spatula.png))

View File

@ -15,90 +15,57 @@ padding = 1.5
bearingDia = 3
// (Needs to be updated). Sketch the block and extrude up to where the counterbore diameter starts.
block = startSketchOn('XY')
extrude001 = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> line(endAbsolute = [width / 2, -length / 2])
|> line(endAbsolute = [width / 2, length / 2])
|> line(endAbsolute = [-width / 2, length / 2])
|> close()
|> hole(circle(
center = [
|> extrude(length = height)
extrude002 = startSketchOn(extrude001, 'end')
|> circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
length / 2 - (padding / 2)
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
length / 2 - (padding / 2)
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = bearingDia / 2
), %)
|> extrude(length = height - cbDepth)
],
radius = cbDia / 2,
)
|> patternLinear2d(
instances = 2,
distance = length - padding,
axis = [0, 1],
)
|> patternLinear2d(
instances = 2,
distance = width - padding,
axis = [1, 0],
)
|> extrude(%, length = -cbDepth)
// Create a second sketch that creates the counterbore diameters and extrude the rest of the way to get the total height. Note: You cannot use startSketchOn(block, 'end'). The extrude lives outside the bounds, and the engine will not execute. This is a known issue.
secondHalf = startSketchOn({
plane = {
origin = { x = 0, y = 0, z = height - cbDepth },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
})
|> startProfileAt([-width / 2, -length / 2], %)
|> line(endAbsolute = [width / 2, -length / 2])
|> line(endAbsolute = [width / 2, length / 2])
|> line(endAbsolute = [-width / 2, length / 2])
|> close()
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
-(width / 2 - (padding / 2)),
length / 2 - (padding / 2)
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
length / 2 - (padding / 2)
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [
width / 2 - (padding / 2),
-(length / 2 - (padding / 2))
],
radius = cbDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = bearingDia / 2
), %)
|> extrude(length = cbDepth)
extrude003 = startSketchOn(extrude001, 'start')
|> circle(
center = [
-(width / 2 - (padding / 2)),
-(length / 2 - (padding / 2))
],
radius = holeDia / 2,
)
|> patternLinear2d(
instances = 2,
distance = length - padding,
axis = [0, 1],
)
|> patternLinear2d(
instances = 2,
distance = width - padding,
axis = [1, 0],
)
|> extrude(length = -height + cbDepth)
extrude004 = startSketchOn(extrude001, 'end')
|> circle(
center = [0, 0],
radius = bearingDia/2,
)
|> extrude(length = -height)

View File

@ -16,21 +16,8 @@ chainWidth = sphereDia / 2
chainThickness = sphereDia / 8
linkDiameter = sphereDia / 4
customPlane = {
plane = {
origin = {
x = 0,
y = 0,
z = -overallThickness / 2
},
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Sketch the inside bearing piece
insideWallSketch = startSketchOn(customPlane)
insideWallSketch = startSketchOn(offsetPlane("XY", offset = -overallThickness / 2))
|> circle(
center = [0, 0],
radius = shaftDia / 2 + wallThickness
@ -109,7 +96,7 @@ linkRevolve = revolve({ axis = 'Y', angle = 360 / nBalls }, linkSketch)
)
// Create the sketch for the outside walls
outsideWallSketch = startSketchOn(customPlane)
outsideWallSketch = startSketchOn(offsetPlane("XY", offset = -overallThickness / 2))
|> circle(
center = [0, 0],
radius = outsideDiameter / 2

View File

@ -1,4 +1,4 @@
// 3D Boaty
// Bench
// This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench.
// Set units in millimeters (mm)
@ -8,12 +8,12 @@
benchLength = 56
// Import various constants and functions from our library
import dividerThickness from "boat-parts.kcl"
import divider from "boat-parts.kcl"
import connector from "boat-parts.kcl"
import seatSlats from "boat-parts.kcl"
import backSlats from "boat-parts.kcl"
import armRest from "boat-parts.kcl"
import dividerThickness from "bench-parts.kcl"
import divider from "bench-parts.kcl"
import connector from "bench-parts.kcl"
import seatSlats from "bench-parts.kcl"
import backSlats from "bench-parts.kcl"
import armRest from "bench-parts.kcl"
// Create the dividers, these hold the seat and back slats
divider("YZ")

View File

@ -1,113 +1,76 @@
// Shelf Bracket
// This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided.
// Set units
@settings(defaultLengthUnit = in)
// Define constants
sigmaAllow = 35000 // psi (6061-T6 aluminum)
width = 6
width = 6 // inch
p = 300 // Force on shelf - lbs
factorOfSafety = 1.2 // FOS of 1.2
shelfMountL = 5
wallMountL = 2
shelfMountL = 5 // inches
wallMountL = 2 // inches
shelfDepth = 12 // Shelf is 12 inches in depth from the wall
moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in)
filletRadius = .375
extFilletRadius = .25
mountingHoleDiameter = 0.5
// Calculate required thickness of bracket
thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches)
filletRadius = .25
extFilletRadius = filletRadius + thickness
mountingHoleDiameter = 0.5
// Sketch the bracket body and fillet the inner and outer edges of the bend
bracketLeg1Sketch = startSketchOn('XY')
sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [shelfMountL - filletRadius, 0], tag = $fillet1)
|> line(end = [0, width], tag = $fillet2)
|> line(end = [-shelfMountL + filletRadius, 0])
|> xLine(length = shelfMountL - thickness, tag = $seg01)
|> yLine(length = thickness, tag = $seg02)
|> xLine(length = -shelfMountL, tag = $seg03)
|> yLine(length = -wallMountL, tag = $seg04)
|> xLine(length = thickness, tag = $seg05)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg06)
|> close()
|> hole(circle(
center = [1, 1],
radius = mountingHoleDiameter / 2
), %)
|> hole(circle(
center = [shelfMountL - 1.5, width - 1],
radius = mountingHoleDiameter / 2
), %)
|> hole(circle(
center = [1, width - 1],
radius = mountingHoleDiameter / 2
), %)
|> hole(circle(
center = [shelfMountL - 1.5, 1],
radius = mountingHoleDiameter / 2
), %)
// Extrude the leg 2 bracket sketch
bracketLeg1Extrude = extrude(bracketLeg1Sketch, length = thickness)
|> extrude(%, length = width)
|> fillet(
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet1),
getNextAdjacentEdge(fillet2)
]
tags = [getNextAdjacentEdge(seg03)],
)
// Sketch the fillet arc
filletSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [0, thickness])
|> arc({
angleEnd = 180,
angleStart = 90,
radius = filletRadius + thickness
}, %)
|> line(end = [thickness, 0])
|> arc({
angleEnd = 90,
angleStart = 180,
radius = filletRadius
}, %)
// Sketch the bend
filletExtrude = extrude(filletSketch, length = -width)
// Create a custom plane for the leg that sits on the wall
customPlane = {
plane = {
origin = { x = -filletRadius, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
}
// Create a sketch for the second leg
bracketLeg2Sketch = startSketchOn(customPlane)
|> startProfileAt([0, -filletRadius], %)
|> line(end = [width, 0])
|> line(end = [0, -wallMountL], tag = $fillet3)
|> line(end = [-width, 0], tag = $fillet4)
|> close()
|> hole(circle(
center = [1, -1.5],
radius = mountingHoleDiameter / 2
), %)
|> hole(circle(
center = [5, -1.5],
radius = mountingHoleDiameter / 2
), %)
// Extrude the second leg
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness)
|> fillet(
radius = extFilletRadius,
tags = [
getNextAdjacentEdge(fillet3),
getNextAdjacentEdge(fillet4)
]
radius = filletRadius,
tags = [getNextAdjacentEdge(seg06)],
)
|> fillet(
radius = filletRadius,
tags = [seg02, getOppositeEdge(seg02)],
)
|> fillet(
radius = filletRadius,
tags = [seg05, getOppositeEdge(seg05)],
)
sketch002 = startSketchOn(sketch001, seg03)
|> circle(
center = [-1.25, 1],
radius = mountingHoleDiameter / 2,
)
|> patternLinear2d(
instances = 2,
distance = 2.5,
axis = [-1, 0],
)
|> patternLinear2d(
instances = 2,
distance = 4,
axis = [0, 1],
)
|> extrude(%, length = -thickness-.01)
sketch003 = startSketchOn(sketch001, seg04)
|> circle(
center = [1, -1],
radius = mountingHoleDiameter / 2,
)
|> patternLinear2d(
instances = 2,
distance = 4,
axis = [1, 0],
)
|> extrude(%, length = -thickness-0.1)

View File

@ -9,18 +9,8 @@
// Import Constants
import caliperTolerance, caliperPadLength, caliperThickness, caliperOuterEdgeRadius, caliperInnerEdgeRadius, rotorDiameter, rotorTotalThickness, yAxisOffset from "globals.kcl"
// Create the plane for the brake caliper. This is so it can match up with the rotor model.
brakeCaliperPlane = {
plane = {
origin = { x = 0, y = yAxisOffset, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Sketch the brake caliper profile
brakeCaliperSketch = startSketchOn(brakeCaliperPlane)
brakeCaliperSketch = startSketchOn('XY')
|> startProfileAt([
rotorDiameter / 2 + caliperTolerance,
0

View File

@ -9,64 +9,61 @@
// Import Constants
import rotorDiameter, rotorInnerDiameter, rotorSinglePlateThickness, rotorInnerDiameterThickness, lugHolePatternDia, lugSpacing, rotorTotalThickness, spacerPatternDiameter, spacerDiameter, spacerLength, spacerCount, wheelDiameter, lugCount, yAxisOffset, drillAndSlotCount from "globals.kcl"
rotorPlane = {
plane = {
origin = { x = 0, y = yAxisOffset, z = 0 },
xAxis = { x = -1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
fn lugPattern(plane) {
lugHolePattern = circle(
plane,
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
return lugHolePattern
}
rotorSketch = startSketchOn(rotorPlane)
rotorSketch = startSketchOn('XZ')
|> circle(
center = [0, 0],
radius = rotorDiameter / 2
)
|> hole(lugPattern(%), %)
rotor = extrude(rotorSketch, length = rotorSinglePlateThickness)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
rotorBumpSketch = startSketchOn(rotorPlane)
rotorBumpSketch = startSketchOn(rotor, 'end')
|> circle(
center = [0, 0],
radius = rotorInnerDiameter / 2
)
|> hole(lugPattern(%), %)
rotorBump = extrude(rotorBumpSketch, length = -rotorInnerDiameterThickness)
rotorBump = extrude(rotorBumpSketch, length = rotorInnerDiameterThickness)
lugHoles = startSketchOn(rotorBump, 'end')
|> circle(
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
|> extrude(%, length = -(rotorInnerDiameterThickness + rotorSinglePlateThickness))
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
rotorSecondaryPlatePlane = {
plane = {
origin = {
x = 0,
y = yAxisOffset + rotorTotalThickness * 0.75,
z = 0
},
xAxis = { x = -1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
secondaryRotorSketch = startSketchOn(rotorSecondaryPlatePlane)
// (update when boolean is available)
centerSpacer = startSketchOn(rotor, 'start')
|> circle(%, center = [0, 0], radius = .25)
|> extrude(%, length = spacerLength)
secondaryRotorSketch = startSketchOn(centerSpacer, 'end')
|> circle(
center = [0, 0],
radius = rotorDiameter / 2
)
|> hole(lugPattern(%), %)
secondRotor = extrude(secondaryRotorSketch, length = rotorSinglePlateThickness)
spacerSketch = startSketchOn(rotorSecondaryPlatePlane)
lugHoles2 = startSketchOn(secondRotor, 'end')
|> circle(
center = [-lugSpacing / 2, 0],
radius = 0.315
)
|> patternCircular2d(
arcDegrees = 360,
center = [0, 0],
instances = lugCount,
rotateDuplicates = true
)
|> extrude(length = -rotorSinglePlateThickness)
spacerSketch = startSketchOn(rotor, 'start')
|> circle(
center = [spacerPatternDiameter / 2, 0],
radius = spacerDiameter
@ -77,8 +74,8 @@ spacerSketch = startSketchOn(rotorSecondaryPlatePlane)
instances = spacerCount,
rotateDuplicates = true
)
spacers = extrude(spacerSketch, length = -spacerLength)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
spacers = extrude(spacerSketch, length = spacerLength)
rotorSlottedSketch = startSketchOn(rotor, 'START')
|> startProfileAt([2.17, 2.56], %)
|> xLine(length = 0.12)
@ -107,5 +104,6 @@ secondRotorSlottedSketch = startSketchOn(secondRotor, 'END')
arcDegrees = 360,
rotateDuplicates = true
)
secondRotorSlotted = extrude(secondRotorSlottedSketch, length = -rotorSinglePlateThickness / 2)
extrude(secondRotorSlottedSketch, length = -rotorSinglePlateThickness / 2)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90)

View File

@ -12,6 +12,7 @@ import 'car-tire.kcl' as carTire
import lugCount from 'globals.kcl'
carRotor
|> translate(translate = [0, 0.5, 0])
carWheel
lugNut
|> patternCircular3d(
@ -22,4 +23,5 @@ lugNut
rotateDuplicates = false
)
brakeCaliper
|> translate(translate = [0, 0.5, 0])
carTire

View File

@ -1,87 +0,0 @@
// Flange with XY coordinates
// A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others.
// Set units
@settings(defaultLengthUnit = in)
// Define constants
mountingHoleDia = .625
baseDia = 4.625
pipeDia = 1.25
thickness = .625
totalThickness = 0.813
topTotalDiameter = 2.313
bottomThickness = 0.06
bottomTotalDiameter = 2.5
mountingHolePlacementDiameter = 3.5
baseThickness = .625
topTotalThickness = totalThickness - (bottomThickness + baseThickness)
holeLocator = baseDia - 8
nHoles = 4
// Add assertion so nHoles are always greater than 1
assertGreaterThan(nHoles, 1, "nHoles must be greater than 1")
// Create the flange base and the six mounting holes
flangeBase = startSketchOn('XY')
|> circle(
center = [0, 0],
radius = baseDia / 2
)
|> hole(circle(
center = [mountingHolePlacementDiameter / 2, 0],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, mountingHolePlacementDiameter / 2],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [-mountingHolePlacementDiameter / 2, 0],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, -mountingHolePlacementDiameter / 2],
radius = mountingHoleDia / 2
), %)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = baseThickness)
// Plane for top face
topFacePlane = {
plane = {
origin = { x = 0, y = 0, z = baseThickness },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the extrusion on the top of the flange base
topExtrusion = startSketchOn(topFacePlane, 'end')
|> circle(
center = [0, 0],
radius = topTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = topTotalThickness)
// Create the extrusion on the bottom of the flange base
bottomExtrusion = startSketchOn("XY")
|> circle(
center = [0, 0],
radius = bottomTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = -bottomThickness)
// https://www.mcmaster.com/44685K193/

View File

@ -8,7 +8,6 @@
mountingHoleDia = .625
baseDia = 4.625
pipeDia = 1.25
thickness = .625
totalThickness = 0.813
topTotalDiameter = 2.313
bottomThickness = 0.06
@ -16,7 +15,6 @@ bottomTotalDiameter = 2.5
mountingHolePlacementDiameter = 3.5
baseThickness = .625
topTotalThickness = totalThickness - (bottomThickness + baseThickness)
holeLocator = baseDia - 8
nHoles = 4
// Add assertion so nHoles are always greater than 1
@ -42,42 +40,25 @@ flangeBase = startSketchOn('XY')
radius = baseDia / 2
)
|> hole(circles, %)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = baseThickness)
// Plane for top face
topFacePlane = {
plane = {
origin = { x = 0, y = 0, z = baseThickness },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the extrusion on the top of the flange base
topExtrusion = startSketchOn(topFacePlane)
topExtrusion = startSketchOn(flangeBase, 'end')
|> circle(
center = [0, 0],
radius = topTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = topTotalThickness)
// Create the extrusion on the bottom of the flange base
bottomExtrusion = startSketchOn("XY")
bottomExtrusion = startSketchOn(flangeBase, 'start')
|> circle(
center = [0, 0],
radius = bottomTotalDiameter / 2
)
|> hole(circle(
center = [0, 0],
radius = pipeDia / 2
), %)
|> extrude(length = -bottomThickness)
|> extrude(length = bottomThickness)
// Cut a hole through the entire body
pipeHole = startSketchOn(topExtrusion, 'end')
|> circle(center = [0, 0], radius = pipeDia/2)
|> extrude(%, length = -(topTotalThickness + baseThickness + bottomThickness))

View File

@ -5,8 +5,8 @@
@settings(defaultLengthUnit = in)
// Define constants
lbumps = 5 // number of bumps long
wbumps = 3 // number of bumps wide
lbumps = 10 // number of bumps long
wbumps = 5 // number of bumps wide
pitch = 8.0
clearance = 0.1
bumpDiam = 4.8
@ -25,28 +25,8 @@ wSegments = totalWidth / wbumps
assertGreaterThan(lbumps, 1, "lbumps must be greater than 1")
assertGreaterThan(wbumps, 1, "wbumps must be greater than 1")
// Create the plane for the pegs. This is a hack so that the pegs can be patterned along the face of the lego base.
pegFace = {
plane = {
origin = { x = 0, y = 0, z = height },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the plane for the tubes underneath the lego. This is a hack so that the tubes can be patterned underneath the lego.
tubeFace = {
plane = {
origin = { x = 0, y = 0, z = height - t },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Make the base
s = startSketchOn('XY')
base = startSketchOn('XY')
|> startProfileAt([-totalWidth / 2, -totalLength / 2], %)
|> line(end = [totalWidth, 0])
|> line(end = [0, totalLength])
@ -54,8 +34,8 @@ s = startSketchOn('XY')
|> close()
|> extrude(length = height)
// Sketch and extrude a rectangular shape to create the shell underneath the lego. This is a hack until we have a shell function.
shellExtrude = startSketchOn(s, "start")
// Sketch and extrude a rectangular shape to create the shell underneath the lego. Will replace with shell function when able to call a face created from shell.
shellExtrude = startSketchOn(base, "start")
|> startProfileAt([
-(totalWidth / 2 - t),
-(totalLength / 2 - t)
@ -67,7 +47,7 @@ shellExtrude = startSketchOn(s, "start")
|> extrude(length = -(height - t))
// Create the pegs on the top of the base
peg = startSketchOn(s, 'end')
peg = startSketchOn(base, 'end')
|> circle(
center = [
-(pitch * (wbumps - 1) / 2),
@ -88,7 +68,7 @@ peg = startSketchOn(s, 'end')
|> extrude(length = bumpHeight)
// Create the pegs on the bottom of the base
tubePattern = startSketchOn(tubeFace)
tubePattern = startSketchOn(shellExtrude, 'start')
|> circle(
center = [
-(pitch * (wbumps - 1) / 2 - (pitch / 2)),
@ -106,4 +86,4 @@ tubePattern = startSketchOn(tubeFace)
instances = lbumps - 1,
distance = pitch
)
|> extrude(length = -bumpHeight)
|> extrude(length = bumpHeight)

View File

@ -1,11 +1,4 @@
[
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "3d-boaty/main.kcl",
"multipleFiles": true,
"title": "3D Boaty",
"description": "This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
@ -27,6 +20,13 @@
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bench/main.kcl",
"multipleFiles": true,
"title": "Bench",
"description": "This is a slight remix of Depep1's original 3D Boaty (https://www.printables.com/model/1141963-3d-boaty). This is a tool used for benchmarking 3D FDM printers for bed adhesion, overhangs, bridging and top surface quality. The name of this file is a bit of misnomer, the shape of the object is a typical park bench."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
@ -78,18 +78,11 @@
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange/main.kcl",
"multipleFiles": false,
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
"multipleFiles": false,
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

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