Compare commits

...

36 Commits

Author SHA1 Message Date
4a5ef17c92 Remove extra console error that shouldn't be needed anymore 2025-04-18 17:23:54 -04:00
83220a7255 Add text to the error message when JSON parse fails 2025-04-18 17:19:50 -04:00
a0f3f0f50e Fix KCLError to have a real message (#6396)
* Fix KCLError to have a real message

* Remove unneeded error exception
2025-04-18 17:13:09 -04:00
dad0d5488f Support any installation path for rust toolchain (#6380) 2025-04-18 20:28:38 +00:00
77773fb052 ci: Add error to console exception (#6392)
* Add error to console exception

* Ignore in codespell

* Increase retry on snapshots

* Fixme's all around

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-18 15:51:13 -04:00
1240ca6f21 Revert "Update onboarding interactive numbers test to use editor fixture" (#6393)
Revert "Update onboarding interactive numbers test to use editor fixture (#6356)"

This reverts commit c77bb92da9.
2025-04-18 14:57:40 -04:00
ce3a6ad286 Revert "Remove all use of bold mono font" (#6394)
Revert "Remove all use of bold mono font (#6373)"

This reverts commit 03c4121ab7.
2025-04-18 14:57:10 -04:00
a20f420a55 Add fixmes to failing e2e tests (#6388)
* Add fixmes to bad snapshots tests

* max attempts to 5 on snapshots

* Skip revolve on windows

* Another one

* Add fixme to edit with ML snapshot test

* fmt

* Use absolute import, fix lint

* Disable another test

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-18 14:14:23 -04:00
03c4121ab7 Remove all use of bold mono font (#6373)
* Remove all use of bold mono font
A few mono font points are left, mostly for numbers which look better in
monospace.

* make React happy with `clipRule`
2025-04-18 14:11:20 -04:00
8c2c877817 Bump crossbeam-channel from 0.5.14 to 0.5.15 in /rust in the security group (#6354)
Bump crossbeam-channel in /rust in the security group

Bumps the security group in /rust with 1 update: [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam).


Updates `crossbeam-channel` from 0.5.14 to 0.5.15
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  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-04-18 14:10:54 -04:00
c77bb92da9 Update onboarding interactive numbers test to use editor fixture (#6356)
Trying to bring it down from the leaderboard of failed tests.

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-04-18 14:06:23 -04:00
bd4bad0020 allow sending async commands to engine (#6342)
* start of async

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

check at end if the async commands completed

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

run at the end of inner_run

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

set import as async

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>

add to the wasm side

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

updates

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

fmt

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

* fire

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

* flake

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

* fixup for awaiting on import

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 mock

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

* fix mock

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>

* add a test where we import then do a bunch of other stuff

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

* updates

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

* fixup to see

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

* fixups

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

* fix tests

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

* updates

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

* cross platform time

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>

* another appearance tests

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

* new docs and tests

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

* updates

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

* dont loop so tight

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-04-17 17:22:19 -07:00
0b9889e313 Double the timeout in sketch editing test (#6376) 2025-04-17 14:32:11 -04:00
f9fbaa2298 Add files via upload (#6227)
* Add files via upload

Adding parametric pc fan and bottle

* Update and rename globals.kcl to parameters.kcl

* Update fan-housing.kcl

* Update fan-housing.kcl

* Update fan.kcl

* Update motor.kcl

* Update parameters.kcl

* Update kcl-samples simulation test output

* Update main.kcl

avoiding fn imports

* Update kcl-samples simulation test output

* remove functions

* angledLine kwargs

* tangentalArc kwargs

* Update kcl-samples simulation test output

* Update housing middle

more tweaks because I just can't help myself

* Update kcl-samples simulation test output

* Update kcl-samples simulation test output

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: jgomez720 <114548659+jgomez720@users.noreply.github.com>
2025-04-17 17:46:56 +00:00
6f2d127c4f Assemblies: Set translate and rotate via point-and-click (#6167)
* WIP: Add point-and-click Import for geometry
Will eventually fix #6120
Right now the whole loop is there but the codemod doesn't work yet

* Better pathToNOde, log on non-working cm dispatch call

* Add workaround to updateModelingState not working

* Back to updateModelingState with a skip flag

* Better todo

* Change working from Import to Insert, cleanups

* Sister command in kclCommands to populate file options

* Improve path selector

* Unsure: move importAstMod to kclCommands onSubmit 😶

* Add e2e test

* Clean up for review

* Add native file menu entry and test

* No await yo lint said so

* WIP: UX improvements around foreign file imports
Fixes #6152

* WIP: Set translate and rotate via point-and-click on imports. Boilerplate code
Will eventually close #6020

* Full working loop of rotate and translate pipe mutation, including edits, only on module imports. VERY VERBOSE

* Add first e2e test for set transform. Bunch of caveats listed as TODOs

* @lrev-Dev's suggestion to remove a comment

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

* Update to scene.settled(cmdBar)

* Add partNNN default name for alias

* Lint

* Lint

* Fix unit tests

* Add sad path insert test
Thanks @Irev-Dev for the suggestion

* Add step insert test

* Lint

* Add test for second foreign import thru file tree click

* WIP: Add point-and-click Load to copy files from outside the project into the project
Towards #6210

* Move Insert button to modeling toolbar, update menus and toolbars

* Add default value for local name alias

* Aligning tests

* Fix tests

* Add padding for filenames starting with a digit

* Lint

* Lint

* Update snapshots

* Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project

* Add disabled transform subbutton

* Allow start of Transform flow from toolbar with selection

* Merge kcl-samples and local disk load into one 'Load external model' command

* Fix em tests

* Fix test

* Add test for file pick import, better input

* Fix non .kcl loading

* Lint

* Update snapshots

* Fix issue leading to test failure

* Fix clone test

* Add note

* Fix nested clone issue

* Clean up for review

* Add valueSummary for path

* Fix test after path change

* Clean up for review

* Support much wider range for transform

* Set display names

* Bug fixed itself moment...

* Add test for extrude tranform

* Oops missed a thing

* Clean up selection arg

* More tests incl for variable stuff

* Fix imports

* Add supportsTransform: true on all solids returning operations

* Fix edit flow on variables, add test

* Split transform command into translate and rotate

* Clean up and comment

* Clean up operations.ts

* Add comment

* Improve assemblies test

* Support more things

* Typo

* Fix test after unit change on import

* Last clean up for review

* Fix remaining test

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2025-04-17 15:44:31 +00:00
056a4d4a22 [FIX]: "Connecting to engine" overlay shows too often (#6350)
* Use the engineStreamState that's available right there silly

* fix lint

* Just look at playing/paused

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-04-17 12:49:31 +00:00
bab79331cb #6367 Tangent snapping improvements (#6369)
* fix snapping line being culled

* make snap line more grayed out

* make snapping tolerance smaller
2025-04-17 08:26:05 -04:00
16bbc970ae fix sketch flash part 2 (#6366) 2025-04-17 08:15:54 -04:00
0f1cff316c add toast for selections we don't recognise (#6370)
* ad toast for selections we don't recognise

* remove log
2025-04-17 21:32:13 +10:00
938a2bae13 Fix to not duplicate arg labels on constrained sketches (#6337) 2025-04-17 07:16:55 +00:00
3324835b72 ci: Fix so that just commands regenerate ast output (#6338)
* Fix so that just commands regenerate ast output

* Update just command to include manifest

* Fix overwrite-sim-test kcl_samples to generate manifest

This is the command used in CI.
2025-04-17 06:03:58 +00:00
1628ea86b2 Remove pixel color checks (#6365) 2025-04-17 14:25:45 +10:00
be119248a6 put execution benchmarks behind flag (#6364)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-16 21:23:30 -07:00
ac75181f7f fix bug and remove flash in sketch mode (#6346)
* fix bug and remove flash

* add test

* fix tests

* fix tests
2025-04-17 10:10:27 +10:00
d9fe78171f parallelized modules from kcl (#6206)
* wip

* sketch a bit more; going to pull this out of tests next

* wip

* lock start things

* this was a bad idea

* Revert "this was a bad idea"

This reverts commit a2092e7ed6.

* prepare prelude before spawning

* error

* poop

* yike

* :(

* ok

* Reapply "this was a bad idea"

This reverts commit fafdf41093.

* chip away more

* man this is bad

* fix rebase add feature flag

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

* get rid of execution kind

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

* clippy

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

* logs

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

* updates

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

* no extra executes

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

* race w batch

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

* cluppy

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

* no printlns

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

* no printlns

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

* fix source ranges

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

* batch shit

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>

* fix

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

* fix some bugs

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

* fix error

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

* cut 1

* preserve mem

* re-ad deep_clone

the helper we were calling was pushing a new call, which was hanging
out. we can skip the middleman since we already have something properly
prepared, just without a stdlib in some cases.

* skip non-kcl

* clean up source range bug

* error message changed

the uuids also changed because the error is hit before execute even
starts.

* typo

* rensnapshot a few

* order things

* MAYBE REVERT LATER:

attempt at an ordering

* snapsnap

* Revert "snapsnap"

This reverts commit 7350b32c7d.

* Revert "MAYBE REVERT LATER:"

This reverts commit ab49f3e85f.

* ugh

* poop

* poop2

* lint

* tranche 1

* more

* more snaps

* snap

* more

* update

* MAYBE REVERT THIS

* cache multi-file

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

* addd tests

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

* set to false

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

* add test outputs

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

* clippy

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

* kcl-py-bindings uses carwheel

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

* update snapshots

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: Paul R. Tagliamonte <paul@zoo.dev>
Co-authored-by: Paul Tagliamonte <paultag@gmail.com>
2025-04-16 11:52:14 -07:00
5586646a60 Fix insta version in Cargo.toml to match lock file (#6348)
Cargo decided to downgrade `windows-sys` for some reason.
2025-04-16 17:50:39 +00:00
e955e783f4 KCL: Error when user sets a keyword arg multiple times (#6339)
Previously, KCL would silently ignore any duplicated keyword args, setting the parameter to one arbitrarily-chosen argument. Now this is instead a parse error. 

I've tested that
1. The error message makes sense
2. The error is on a reasonable part of the source code
3. The error doesn't prevent other kinds of parse errors being picked up later

Thanks for noticing this one Frank!
2025-04-16 11:18:45 -05:00
9162c4d723 Split out the contributor guide (#6347) 2025-04-16 15:48:36 +00:00
f345a07bcd Update helix e2e test to account for 90deg engine change (#6341)
The engine fixed the start angle for helices, but our e2e test has a
pixel check so it broke. I made the test change the `startAngle` by
`-90` rather than try to hunt around for a new working pixel value.
2025-04-16 00:09:58 +00:00
f5e3b4bbe7 Update helix snapshots (#6340)
Engine fixed a bug where helices were starting at the wrong position. Requires updating the snapshots.
2025-04-15 17:15:37 -05:00
941911ed30 docs: Fix syntax error and deprecated string planes in types.md (#6332) 2025-04-15 17:42:24 +00:00
599bd2d958 Switch to a 6-core profile for mac (#6323)
* Switch to a 6-core profile for mac

* Bump workers percentage to optimize for CPU count (#6329)

* Bump macOS workers percentage to optimize for CPU count

* Handle floor math

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-04-15 13:07:16 -04:00
a847c7f608 Release KCL 62 (#6330) 2025-04-15 15:53:01 +00:00
d9be308c36 ~Maybe~ (Lee: ABSOLUTELY) use biome (#6327)
* maybe use biome

* import organising

* Removed unused new fmt commands; fix to not use linter

* Don't use fmt:generated - fmt is fast, but also use Makefile for this sort of thing

---------

Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
2025-04-15 10:11:25 -04:00
3cca4a30af [BUG] Grab bag of sketch mode bug fixes (#6229)
* use artifact id for sketch mode entry plane

* fix up re-eval as best as possible

* remove some async dodgyness

* fmt

* fix old sycronous re-execute shit

* add a test

* fix existing test

* add toast for error state

* spelling

* test stuff

* fmt

* fix toast

* test fix

* some other fix ups

* fix test
2025-04-15 09:27:50 -04:00
c51db5bd4a Make playwright:snapshots:ubuntu fail on mismatching image snapshots (#6322)
* pierremtb/adhoc/playwright-should-fail-on-mismatching-image-snapshots

* Add network-toggle to masks

* Add network masks

* :oops: tangentialArc snapshot update

* Update snaps

* Cleaner constants for masks

* Update snaps

* Clean up, updated README
2025-04-15 09:43:39 +00:00
397 changed files with 5712102 additions and 10175 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,./e2e/playwright/lib/console-error-whitelist.ts,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts

View File

@ -50,13 +50,12 @@ jobs:
- name: Build the benchmark target(s)
run: |
cd rust
cargo codspeed build --measurement-mode walltime
cargo codspeed build
- 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

@ -226,8 +226,8 @@ jobs:
with:
shell: bash
command: npm run test:snapshots
timeout_minutes: 30
max_attempts: 3
timeout_minutes: 5
max_attempts: 5
env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
@ -285,7 +285,7 @@ jobs:
# 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
- namespace-profile-macos-6-cores
- windows-latest-8-cores
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
@ -295,7 +295,7 @@ jobs:
isScheduled:
- ${{ github.event_name == 'schedule' }}
exclude:
- os: namespace-profile-macos-8-cores
- os: namespace-profile-macos-6-cores
isScheduled: true
- os: windows-latest-8-cores
isScheduled: true

View File

@ -25,7 +25,7 @@ jobs:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install
- run: npm run fmt-check
- run: npm run fmt:check
npm-build-wasm:
# Build the wasm blob once on the fastest runner.

1
.gitignore vendored
View File

@ -84,3 +84,4 @@ dist
venv
.vscode-test
.biome/

432
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,432 @@
# Contributor Guide
## Running a development build
Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation).
On Windows, it's also recommended to [upgrade your PowerShell version](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#winget), we're using 7.
Then in the repo run the following to install and use the node version specified in `.nvmrc`. You might need to specify your processor architecture with `--arch arm64` or `--arch x64` if it's not autodetected.
```
fnm install --corepack-enabled
fnm use
```
Install the NPM dependencies with:
```
npm install
```
This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We have package scripts to run rustup, see `package.json` for reference:
```
# macOS/Linux
npm run install:rust
npm run install:wasm-pack:sh
# Windows
npm run install:rust:windows
npm run install:wasm-pack:cargo
```
Then to build the WASM layer, run:
```
# macOS/Linux
npm run build:wasm
# Windows
npm run build:wasm:windows
```
Or if you have the `gh` cli installed and want to download the latest main wasm bundle. Note that on Windows, you need to associate .ps1 files with PowerShell, which can be done via the right click menu, selecting `C:\Program Files\PowerShell\7\pwsh.exe`, and you can install tools like `gh` via `npm run install:tools:windows`.
```
# macOS/Linux
npm run fetch:wasm
# Windows
npm run fetch:wasm:windows
```
That will build the WASM binary and put in the `public` dir (though gitignored).
Finally, to run the web app only, run:
```
npm start
```
If you're not a Zoo employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development.local` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to `localhost:3000` again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome
Chrome is in the process of rolling out a new default which
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
If you're having trouble logging into the `modeling-app`, you may need to
enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Desktop
To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then:
```
npm run tron:start
```
This will start the application and hot-reload on changes.
Devtools can be opened with the usual Command-Option-I (macOS) or Ctrl-Shift-I (Linux and Windows).
To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables).
## Checking out commits / Bisecting
Which commands from setup are one off vs. need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash
npm install
npm run build:wasm
npm start # or npm run build:local && npm run serve for slower but more production-like build
```
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `npm run fmt`
- `npm run tsc`
- `npm run test`
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
## Release a new version
#### 1. Create a 'Cut release $VERSION' issue
It will be used to document changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag
Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`.
```
VERSION=$(./scripts/semantic-release.sh)
git tag $VERSION
git push origin --tags
```
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
#### 3. Manually test artifacts
##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds
The other `build-apps` output in the release `build-apps` workflow (triggered by 2.) is `updater-test-{arch}-{platform}`. It's a semi-automated process: for macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage
```
#### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well.
Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue
If everything is well and the release is out to the public, the issue tracking the release shall be closed.
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd rust/kcl-lib
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
## Tests
### Playwright tests
You will need a `./e2e/playwright/playwright-secrets.env` file:
```bash
$ touch ./e2e/playwright/playwright-secrets.env
$ cat ./e2e/playwright/playwright-secrets.env
token=<dev.zoo.dev/account/api-tokens>
snapshottoken=<zoo.dev/account/api-tokens>
```
or use `export` to set the environment variables `token` and `snapshottoken`.
#### Snapshot tests (Google Chrome on Ubuntu only)
Only Ubunu and Google Chrome is supported for the set of tests evaluating screenshot snapshots.
If you don't run Ubuntu locally or in a VM, you may use a GitHub Codespace.
```
npm run playwright -- install chrome
npm run test:snapshots
```
You may use `-- --update-snapshots` as needed.
#### Electron flow tests (Chromium on Ubuntu, macOS, Windows)
```
npm run playwright -- install chromium
npm run test:playwright:electron:local
```
You may use `-- -g "my test"` to match specific test titles, or `-- path/to/file.spec.ts` for a test file.
#### Debugger
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
Or comment out browsers in `playwright.config.ts`.
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `npm run playwright codegen -b wk --load-storage ./store localhost:3000`
<details>
<summary>
Where `./store` should look like this
</summary>
```JSON
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{
"name": "store",
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
},
{
"name": "persistCode",
"value": ""
},
{
"name": "TOKEN_PERSIST_KEY",
"value": "your-token"
}
]
}
]
}
```
</details>
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
### Unit and component tests
If you already haven't, run the following:
```
npm
npm run build:wasm
npm start
```
and finally:
```
npm run test:unit
```
For individual testing:
```
npm run test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default.
### Rust tests
**Dependencies**
- `KITTYCAD_API_TOKEN`
- `cargo-nextest`
- `just`
#### Setting KITTYCAD_API_TOKEN
Use the production zoo.dev token, set this environment variable before running the tests
#### Installing cargonextest
```
cd rust
cargo search cargo-nextest
cargo install cargo-nextest
```
#### just
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
#### Running the tests
```bash
# With just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
# Make sure you installed just
cd rust
just test
```
```bash
# Without just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
cd rust
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
```
Where `XXX` is an API token from the production engine (NOT the dev environment).
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
```
cd rust
KITTYCAD_API_TOKEN=XXX cargo run nextest
```
### Mapping CI CD jobs to local commands
When you see the CI CD fail on jobs you may wonder three things
- Do I have a bug in my code?
- Is the test flaky?
- Is there a bug in `main`?
To answer these questions the following commands will give you confidence to locate the issue.
#### Static Analysis
Part of the CI CD pipeline performs static analysis on the code. Use the following commands to mimic the CI CD jobs.
The following set of commands should get us closer to one and done commands to instantly retest issues.
```
npm run test-setup
```
> Gotcha, are packages up to date and is the wasm built?
```
npm run tsc
npm run fmt:check
npm run lint
npm run test:unit:local
```
> Gotcha: Our unit tests have integration tests in them. You need to run a localhost server to run the unit tests.
#### E2E Tests
**Playwright Electron**
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
```
npm run test:playwright:electron:local
npm run test:playwright:electron:windows:local
npm run test:playwright:electron:macos:local
npm run test:playwright:electron:ubuntu:local
```
> Why does it say local? The CI CD commands that run in the pipeline cannot be ran locally. A single command will not properly setup the testing environment locally.
#### Some notes on CI
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
How to interpret failing playwright tests?
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
#### Getting started writing a playwright test in our app
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
<details>
<summary>
PS: for the debug panel, the following JSON is useful for snapping the camera
</summary>
```JSON
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
```
</details>
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.

View File

@ -15,8 +15,8 @@ ifdef WINDOWS
CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe
WASM_PACK ?= $(USERPROFILE)/.cargo/bin/wasm-pack.exe
else
CARGO ?= ~/.cargo/bin/cargo
WASM_PACK ?= ~/.cargo/bin/wasm-pack
CARGO ?= $(shell which cargo || echo ~/.cargo/bin/cargo)
WASM_PACK ?= $(shell which wasm-pack || echo ~/.cargo/bin/wasm-pack)
endif
.PHONY: install

473
README.md
View File

@ -1,8 +1,8 @@
![Zoo Design Studio](/public/zma-logomark-outlined.png)
## Zoo Design Studio
# Zoo Design Studio
download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
[zoo.dev/modeling-app](https://zoo.dev/modeling-app)
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
@ -23,7 +23,7 @@ Design Studio is a _hybrid_ user interface for CAD modeling. You can point-and-c
The 3D view in Design Studio is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools
## Technology
- UI
- [React](https://react.dev/)
@ -38,471 +38,16 @@ The 3D view in Design Studio is just a video stream from our hosted geometry eng
- Modeling
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
## Get Started
[Original demo slides](https://github.com/user-attachments/files/19010536/demo.pdf)
We recommend downloading the latest application binary from our [releases](https://github.com/KittyCAD/modeling-app/releases) page. If you don't see your platform or architecture supported there, please file an issue.
## Get started
If you'd like to try out upcoming changes sooner, you can also download those from our [nightly releases](https://zoo.dev/modeling-app/download/nightly) page.
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
## Developing
## Running a development build
Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation).
On Windows, it's also recommended to [upgrade your PowerShell version](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#winget), we're using 7.
Then in the repo run the following to install and use the node version specified in `.nvmrc`. You might need to specify your processor architecture with `--arch arm64` or `--arch x64` if it's not autodetected.
```
fnm install --corepack-enabled
fnm use
```
Install the NPM dependencies with:
```
npm install
```
This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We have package scripts to run rustup, see `package.json` for reference:
```
# macOS/Linux
npm run install:rust
npm run install:wasm-pack:sh
# Windows
npm run install:rust:windows
npm run install:wasm-pack:cargo
```
Then to build the WASM layer, run:
```
# macOS/Linux
npm run build:wasm
# Windows
npm run build:wasm:windows
```
Or if you have the `gh` cli installed and want to download the latest main wasm bundle. Note that on Windows, you need to associate .ps1 files with PowerShell, which can be done via the right click menu, selecting `C:\Program Files\PowerShell\7\pwsh.exe`, and you can install tools like `gh` via `npm run install:tools:windows`.
```
# macOS/Linux
npm run fetch:wasm
# Windows
npm run fetch:wasm:windows
```
That will build the WASM binary and put in the `public` dir (though gitignored).
Finally, to run the web app only, run:
```
npm start
```
If you're not a Zoo employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development.local` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to `localhost:3000` again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome
Chrome is in the process of rolling out a new default which
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
If you're having trouble logging into the `modeling-app`, you may need to
enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Desktop
To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then:
```
npm run tron:start
```
This will start the application and hot-reload on changes.
Devtools can be opened with the usual Command-Option-I (macOS) or Ctrl-Shift-I (Linux and Windows).
To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables).
## Checking out commits / Bisecting
Which commands from setup are one off vs. need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash
npm install
npm run build:wasm
npm start # or npm run build:local && npm run serve for slower but more production-like build
```
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `npm run fmt`
- `npm run tsc`
- `npm run test`
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
## Release a new version
#### 1. Create a 'Cut release $VERSION' issue
It will be used to document changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag
Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`.
```
VERSION=$(./scripts/semantic-release.sh)
git tag $VERSION
git push origin --tags
```
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
#### 3. Manually test artifacts
##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds
The other `build-apps` output in the release `build-apps` workflow (triggered by 2.) is `updater-test-{arch}-{platform}`. It's a semi-automated process: for macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
```
# Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
# macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage
```
#### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well.
Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue
If everything is well and the release is out to the public, the issue tracking the release shall be closed.
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd rust/kcl-lib
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
## Tests
### Playwright tests
You will need a `./e2e/playwright/playwright-secrets.env` file:
```bash
$ touch ./e2e/playwright/playwright-secrets.env
$ cat ./e2e/playwright/playwright-secrets.env
token=<dev.zoo.dev/account/api-tokens>
snapshottoken=<your-snapshot-token>
```
For a portable way to run Playwright you'll need Docker.
#### Generic example
After that, open a terminal and run:
```bash
docker run --network host --rm --init -it playwright/chrome:playwright-x.xx.x
```
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ npm run playwright test --project="Google Chrome" <test suite>
```
#### Specific example
open a terminal and run:
```bash
docker run --network host --rm --init -it playwright/chrome:playwright-1.46.0
```
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ npm run playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts
```
run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests)
**Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date.
run headed
```
npm run playwright test --headed
```
run with step through debugger
```
PWDEBUG=1 npm run playwright test
```
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
Or comment out browsers in `playwright.config.ts`.
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `npm run playwright codegen -b wk --load-storage ./store localhost:3000`
<details>
<summary>
Where `./store` should look like this
</summary>
```JSON
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{
"name": "store",
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
},
{
"name": "persistCode",
"value": ""
},
{
"name": "TOKEN_PERSIST_KEY",
"value": "your-token"
}
]
}
]
}
```
</details>
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
### Unit and component tests
If you already haven't, run the following:
```
npm
npm run build:wasm
npm start
```
and finally:
```
npm run test:unit
```
For individual testing:
```
npm run test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default.
### Rust tests
**Dependencies**
- `KITTYCAD_API_TOKEN`
- `cargo-nextest`
- `just`
#### Setting KITTYCAD_API_TOKEN
Use the production zoo.dev token, set this environment variable before running the tests
#### Installing cargonextest
```
cd rust
cargo search cargo-nextest
cargo install cargo-nextest
```
#### just
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
#### Running the tests
```bash
# With just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
# Make sure you installed just
cd rust
just test
```
```bash
# Without just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
cd rust
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
```
Where `XXX` is an API token from the production engine (NOT the dev environment).
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
```
cd rust
KITTYCAD_API_TOKEN=XXX cargo run nextest
```
### Mapping CI CD jobs to local commands
When you see the CI CD fail on jobs you may wonder three things
- Do I have a bug in my code?
- Is the test flaky?
- Is there a bug in `main`?
To answer these questions the following commands will give you confidence to locate the issue.
#### Static Analysis
Part of the CI CD pipeline performs static analysis on the code. Use the following commands to mimic the CI CD jobs.
The following set of commands should get us closer to one and done commands to instantly retest issues.
```
npm run test-setup
```
> Gotcha, are packages up to date and is the wasm built?
```
npm run tsc
npm run fmt-check
npm run lint
npm run test:unit:local
```
> Gotcha: Our unit tests have integration tests in them. You need to run a localhost server to run the unit tests.
#### E2E Tests
**Playwright Electron**
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
```
npm run test:playwright:electron:local
npm run test:playwright:electron:windows:local
npm run test:playwright:electron:macos:local
npm run test:playwright:electron:ubuntu:local
```
> Why does it say local? The CI CD commands that run in the pipeline cannot be ran locally. A single command will not properly setup the testing environment locally.
#### Some notes on CI
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
How to interpret failing playwright tests?
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
#### Getting started writing a playwright test in our app
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
<details>
<summary>
PS: for the debug panel, the following JSON is useful for snapping the camera
</summary>
```JSON
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
```
</details>
Finally, if you'd like to run a development build or contribute to the project, please visit our [contributor guide](CONTRIBUTING.md) to get started.
## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/rust/kcl-lib).
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.
To contribute to the KittyCAD Language, see the [README](https://github.com/KittyCAD/modeling-app/tree/main/rust/kcl-lib) for KCL.

48
biome.json Normal file
View File

@ -0,0 +1,48 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error"
},
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"useBlockStatements": "error",
"useShorthandArrayType": "error"
}
}
},
"formatter": {
"enabled": true,
"indentWidth": 2,
"indentStyle": "space",
"lineWidth": 80,
"formatWithErrors": true
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"trailingCommas": "es5",
"semicolons": "asNeeded"
},
"parser": {
"unsafeParameterDecoratorsEnabled": true
}
},
"css": {
"parser": {
"cssModules": true
}
},
"files": {
"ignore": ["**/*.json"]
}
}

File diff suppressed because one or more lines are too long

View File

@ -109,3 +109,98 @@ Coordinate systems:
- `zoo` (the default), forward: -Y, up: +Z, handedness: right
- `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left
### Performance
Parallelized foreign-file imports now let you overlap file reads, initialization,
and rendering. To maximize throughput, you need to understand the three distinct
stages—reading, initializing (background render start), and invocation (blocking)
—and structure your code to defer blocking operations until the end.
#### Foreign Import Execution Stages
1. **Import (Read) Stage**
```norun
import "tests/inputs/cube.step" as cube
```
- Reads the file from disk and makes its API available.
- **Does _not_** start Engine rendering or block your script.
2. **Initialization (Background Render) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube // <- This line starts background rendering
```
- Invoking the imported symbol (assignment or plain call) triggers Engine rendering _in the background_.
- This kickstarts the render pipeline but doesnt block—you can continue other work while the Engine processes the model.
3. **Invocation (Blocking) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube
myCube
|> translate(z=10) // <- This line blocks
```
- Any method call (e.g., `translate`, `scale`, `rotate`) waits for the background render to finish before applying transformations.
- This is the only point where your script will block.
> **Nuance:** Foreign imports differ from pure KCL modules—calling the same import symbol multiple times (e.g., `screw` twice) starts background rendering twice.
#### Best Practices
##### 1. Defer Blocking Calls
Initialize early but delay all transformations until after your heavy computation:
```norun
import "tests/inputs/cube.step" as cube // 1) Read
myCube = cube // 2) Background render starts
// --- perform other operations and calculations or setup here ---
myCube
|> translate(z=10) // 3) Blocks only here
```
##### 2. Encapsulate Imports in Modules
Keep `main.kcl` free of reads and initialization; wrap them:
```norun
// imports.kcl
import "tests/inputs/cube.step" as cube // Read only
export myCube = cube // Kick off rendering
```
```norun
// main.kcl
import myCube from "imports.kcl" // Import the initialized object
// ... computations ...
myCube
|> translate(z=10) // Blocking call at the end
```
##### 3. Avoid Immediate Method Calls
```norun
import "tests/inputs/cube.step" as cube
cube
|> translate(z=10) // Blocks immediately, negating parallelism
```
Both calling methods right on `cube` immediately or leaving an implicit import without assignment introduce blocking.
#### Future Improvements
Upcoming releases will autoanalyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.

View File

@ -53,7 +53,7 @@ rotate(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -37,7 +37,7 @@ scale(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

File diff suppressed because one or more lines are too long

View File

@ -28101,14 +28101,62 @@
"args": [
{
"name": "solids",
"type": "[Solid]",
"type": "SolidOrImportedGeometry",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid",
"type": "array",
"items": {
"$ref": "#/components/schemas/Solid"
},
"title": "SolidOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
"type": "object",
"required": [
"id",
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"importedGeometry"
]
},
"id": {
"description": "The ID of the imported geometry.",
"type": "string",
"format": "uuid"
},
"value": {
"description": "The original file paths.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
{
"type": [
"object",
"array"
],
"items": {
"$ref": "#/components/schemas/Solid"
},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"solidSet"
]
}
}
}
],
"definitions": {
"Solid": {
"type": "object",
@ -34571,14 +34619,62 @@
],
"returnValue": {
"name": "",
"type": "[Solid]",
"type": "SolidOrImportedGeometry",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Array_of_Solid",
"type": "array",
"items": {
"$ref": "#/components/schemas/Solid"
},
"title": "SolidOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
"type": "object",
"required": [
"id",
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"enum": [
"importedGeometry"
]
},
"id": {
"description": "The ID of the imported geometry.",
"type": "string",
"format": "uuid"
},
"value": {
"description": "The original file paths.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
{
"type": [
"object",
"array"
],
"items": {
"$ref": "#/components/schemas/Solid"
},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"solidSet"
]
}
}
}
],
"definitions": {
"Solid": {
"type": "object",
@ -36198,7 +36294,8 @@
"// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.\n// This example shows _before_ the pattern.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([0, 0], %)\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)",
"// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.\n// This example shows _after_ the pattern.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([0, 0], %)\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)",
"// Color the result of a 2D pattern that was extruded.\nexampleSketch = startSketchOn(XZ)\n |> startProfileAt([.5, 25], %)\n |> line(end = [0, 5])\n |> line(end = [-1, 0])\n |> line(end = [0, -5])\n |> close()\n |> patternCircular2d(\n center = [0, 0],\n instances = 13,\n arcDegrees = 360,\n rotateDuplicates = true,\n )\n\nexample = extrude(exampleSketch, length = 1)\n |> appearance(color = '#ff0000', metalness = 90, roughness = 90)",
"// Color the result of a sweep.\n\n// Create a path for the sweep.\nsweepPath = startSketchOn(XZ)\n |> startProfileAt([0.05, 0.05], %)\n |> line(end = [0, 7])\n |> tangentialArc(angle = 90, radius = 5)\n |> line(end = [-3, 0])\n |> tangentialArc(angle = -90, radius = 5)\n |> line(end = [0, 7])\n\npipeHole = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 1.5)\n\nsweepSketch = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 2)\n |> hole(pipeHole, %)\n |> sweep(path = sweepPath)\n |> appearance(color = \"#ff0000\", metalness = 50, roughness = 50)"
"// Color the result of a sweep.\n\n// Create a path for the sweep.\nsweepPath = startSketchOn(XZ)\n |> startProfileAt([0.05, 0.05], %)\n |> line(end = [0, 7])\n |> tangentialArc(angle = 90, radius = 5)\n |> line(end = [-3, 0])\n |> tangentialArc(angle = -90, radius = 5)\n |> line(end = [0, 7])\n\npipeHole = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 1.5)\n\nsweepSketch = startSketchOn(XY)\n |> circle(center = [0, 0], radius = 2)\n |> hole(pipeHole, %)\n |> sweep(path = sweepPath)\n |> appearance(color = \"#ff0000\", metalness = 50, roughness = 50)",
"// Change the appearance of an imported model.\n\n\nimport \"tests/inputs/cube.sldprt\" as cube\n\ncube\n// |> appearance(\n// color = \"#ff0000\",\n// metalness = 50,\n// roughness = 50\n// )"
]
},
{
@ -249578,7 +249675,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -260975,7 +261072,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -262721,7 +262818,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -270879,7 +270976,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -319774,7 +319871,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",
@ -327932,7 +328029,7 @@
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "SolidOrSketchOrImportedGeometry",
"description": "Data for a solid or an imported geometry.",
"description": "Data for a solid, sketch, or an imported geometry.",
"oneOf": [
{
"description": "Data for an imported geometry.",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -33,7 +33,7 @@ translate(
### Returns
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -145,7 +145,7 @@ This helps keep your code neat and avoid unnecessary declarations.
Say you have a long pipeline of sketch functions, like this:
```norun
startSketchOn('XZ')
startSketchOn(XZ)
|> line(%, end = [3, 4])
|> line(%, end = [10, 10])
|> line(%, end = [-13, -14])
@ -160,7 +160,7 @@ means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent
could be rewritten as
```norun
startSketchOn('XZ')
startSketchOn(XZ)
|> line(end = [3, 4])
|> line(end = [10, 10])
|> line(end = [-13, -14])
@ -182,7 +182,7 @@ The syntax for declaring a tag is `$myTag` you would use it in the following
way:
```norun
startSketchOn('XZ')
startSketchOn(XZ)
|> startProfileAt(origin, %)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine(
@ -217,7 +217,7 @@ However if the code was written like this:
```norun
fn rect(origin) {
return startSketchOn('XZ')
return startSketchOn(XZ)
|> startProfileAt(origin, %)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine(
@ -227,7 +227,7 @@ fn rect(origin) {
)
|> angledLine(
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001)
length = -segLen(rectangleSegmentA001),
tag = $rectangleSegmentC001,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
@ -247,7 +247,7 @@ For example the following code works.
```norun
fn rect(origin) {
return startSketchOn('XZ')
return startSketchOn(XZ)
|> startProfileAt(origin, %)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine(
@ -257,7 +257,7 @@ fn rect(origin) {
)
|> angledLine(
angle = segAng(rectangleSegmentA001),
length = -segLen(rectangleSegmentA001)
length = -segLen(rectangleSegmentA001),
tag = $rectangleSegmentC001,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
@ -268,11 +268,8 @@ rect([0, 0])
myRect = rect([20, 0])
myRect
|> extrude(10, %)
|> fillet(
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
)
|> extrude(length = 10)
|> fillet(radius = 0.5, tags = [myRect.tags.rectangleSegmentA001])
```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -1,10 +1,10 @@
---
title: "SolidOrSketchOrImportedGeometry"
excerpt: "Data for a solid or an imported geometry."
excerpt: "Data for a solid, sketch, or an imported geometry."
layout: manual
---
Data for a solid or an imported geometry.
Data for a solid, sketch, or an imported geometry.

View File

@ -94,11 +94,8 @@ rect([0, 0])
myRect = rect([20, 0])
myRect
|> extrude(10, %)
|> fillet(
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
)
|> extrude(length = 10)
|> fillet(radius = 0.5, tags = [myRect.tags.rectangleSegmentA001])
```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -66,15 +66,17 @@ async function doBasicSketch(
await page.waitForTimeout(500)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
await expect(
u.codeLocator
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(length = ${commonPoints.num1})`)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
await expect(
u.codeLocator
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(length = ${commonPoints.num1})
@ -85,8 +87,9 @@ async function doBasicSketch(
await page.waitForTimeout(200)
await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
await expect(
u.codeLocator
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(length = ${commonPoints.num1})
@ -142,8 +145,9 @@ async function doBasicSketch(
// Open the code pane.
await u.openKclCodePanel()
await expect(u.codeLocator)
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
await expect(
u.codeLocator
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(length = ${commonPoints.num1}, tag = $seg01)

View File

@ -1,6 +1,6 @@
import { join } from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import fsp from 'fs/promises'
import { join } from 'path'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from '@e2e/playwright/storageStates'
import {

View File

@ -1,6 +1,6 @@
import path, { join } from 'path'
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
import * as fsp from 'fs/promises'
import path, { join } from 'path'
import {
executorInputPath,

View File

@ -1,5 +1,5 @@
import fsp from 'fs/promises'
import path from 'path'
import fsp from 'fs/promises'
import {
executorInputPath,

View File

@ -1,6 +1,6 @@
import { join } from 'path'
import { uuidv4 } from '@src/lib/utils'
import fsp from 'fs/promises'
import { join } from 'path'
import {
TEST_COLORS,
@ -409,8 +409,9 @@ sketch_001 = startSketchOn(XY)
// Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn(XY)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line(end = [20, 0])
|> line(end = [0, 20])
@ -462,8 +463,9 @@ sketch_001 = startSketchOn(XY)
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(page.locator('.cm-content'))
.toHaveText(`sketch_001 = startSketchOn(XY)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch_001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line(end = [20, 0])
|> line(end = [0, 20])
@ -978,8 +980,9 @@ sketch001 = startSketchOn(XZ)
await page.keyboard.up('Control')
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn(XZ)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -5.38])

View File

@ -1,5 +1,5 @@
import * as fsp from 'fs/promises'
import { join } from 'path'
import * as fsp from 'fs/promises'
import { expect, test } from '@e2e/playwright/zoo-test'

View File

@ -1,7 +1,7 @@
import { FILE_EXT } from '@src/lib/constants'
import * as fs from 'fs'
import * as fsp from 'fs/promises'
import { join } from 'path'
import { FILE_EXT } from '@src/lib/constants'
import * as fsp from 'fs/promises'
import {
createProject,

View File

@ -1,7 +1,7 @@
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test'
import { expect } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test'
import { expect } from '@playwright/test'
export type CmdBarSerialised =
| {
@ -178,6 +178,13 @@ export class CmdBarFixture {
return this.page.getByRole('option', options)
}
/**
* Clicks the Create new variable button for kcl input
*/
createNewVariable = async () => {
await this.page.getByRole('button', { name: 'Create new variable' }).click()
}
/**
* Captures a snapshot of the request sent to the text-to-cad API endpoint
* and saves it to a file named after the current test.

View File

@ -7,11 +7,11 @@ import type {
} from '@playwright/test'
import { _electron as electron } from '@playwright/test'
import fs from 'node:fs'
import path from 'path'
import { SETTINGS_FILE_NAME } from '@src/lib/constants'
import type { DeepPartial } from '@src/lib/types'
import fsp from 'fs/promises'
import fs from 'node:fs'
import path from 'path'
import type { Settings } from '@rust/kcl-lib/bindings/Settings'

View File

@ -257,6 +257,14 @@ export class SceneFixture {
await expectPixelColor(this.page, colour, coords, diff)
}
expectPixelColorNotToBe = async (
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) => {
await expectPixelColorNotToBe(this.page, colour, coords, diff)
}
get gizmo() {
return this.page.locator('[aria-label*=gizmo]')
}
@ -278,37 +286,69 @@ function isColourArray(
return isArray(colour[0])
}
export async function expectPixelColor(
type PixelColorMatchMode = 'matches' | 'differs'
export async function checkPixelColor(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
diff: number,
mode: PixelColorMatchMode
) {
let finalValue = colour
const isMatchMode = mode === 'matches'
const actionText = isMatchMode ? 'expecting' : 'not expecting'
const functionName = isMatchMode
? 'ExpectPixelColor'
: 'ExpectPixelColourNotToBe'
await expect
.poll(
async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
let matches
if (!isColourArray(colour)) {
return pixel.every(
matches = pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
} else {
matches = colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
return isMatchMode ? matches : !matches
},
{ timeout: 10_000 }
)
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: point ${JSON.stringify(
`${functionName}: point ${JSON.stringify(
coords
)} was expecting ${colour} but got ${finalValue}`,
)} was ${actionText} ${colour} but got ${finalValue}`,
{ cause }
)
})
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
await checkPixelColor(page, colour, coords, diff, 'matches')
}
export async function expectPixelColorNotToBe(
page: Page,
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
await checkPixelColor(page, colour, coords, diff, 'differs')
}

View File

@ -1,9 +1,9 @@
import path from 'path'
import { expect, test } from '@e2e/playwright/zoo-test'
import * as fsp from 'fs/promises'
import path from 'path'
test.describe('Import UI tests', () => {
test('shows toast when trying to sketch on imported face', async ({
test('shows toast when trying to sketch on imported face, and hovering over imported geometry should NOT highlight any code', async ({
context,
page,
homePage,
@ -93,7 +93,6 @@ sketch002 = startSketchOn(extrude001, face = seg01)`
await toolbar.startSketchPlaneSelection()
// Click on a face from the imported model
// await new Promise(() => {})
await importedFaceClick()
// Verify toast appears with correct content

View File

@ -1,5 +1,5 @@
import fsp from 'fs/promises'
import { join } from 'path'
import fsp from 'fs/promises'
import { executorInputPath } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'

View File

@ -1,6 +1,6 @@
import { join } from 'path'
import { PROJECT_SETTINGS_FILE_NAME } from '@src/lib/constants'
import * as fsp from 'fs/promises'
import { join } from 'path'
import type { NamedView } from '@rust/kcl-lib/bindings/NamedView'

View File

@ -1,7 +1,7 @@
import { join } from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import { onboardingPaths } from '@src/routes/Onboarding/paths'
import fsp from 'fs/promises'
import { join } from 'path'
import { expectPixelColor } from '@e2e/playwright/fixtures/sceneFixture'
import {

View File

@ -1,5 +1,5 @@
import * as fsp from 'fs/promises'
import path from 'path'
import * as fsp from 'fs/promises'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
@ -169,6 +169,180 @@ test.describe('Point-and-click assemblies tests', () => {
}
)
test(
`Insert the bracket part into an assembly and transform it`,
{ tag: ['@electron'] },
async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
tronApp,
}) => {
if (!tronApp) {
fail()
}
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 900, y: 250 }
const bgColor: [number, number, number] = [30, 30, 30]
const partColor: [number, number, number] = [100, 100, 100]
const tolerance = 30
const u = await getUtils(page)
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
await test.step('Setup parts and expect empty assembly scene', async () => {
const projectName = 'assembly'
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, projectName)
await fsp.mkdir(bracketDir, { recursive: true })
await Promise.all([
fsp.copyFile(
path.join('public', 'kcl-samples', 'bracket', 'main.kcl'),
path.join(bracketDir, 'bracket.kcl')
),
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
])
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.openProject(projectName)
await scene.settled(cmdBar)
await toolbar.closePane('code')
})
await test.step('Insert kcl as module', async () => {
await insertPartIntoAssembly(
'bracket.kcl',
'bracket',
toolbar,
cmdBar,
page
)
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
import "bracket.kcl" as bracket
bracket
`,
{ shouldNormalise: true }
)
await scene.settled(cmdBar)
// Check scene for changes
await toolbar.closePane('code')
await u.doAndWaitForCmd(async () => {
await gizmo.click({ button: 'right' })
await resetCameraButton.click()
}, 'zoom_to_fit')
await toolbar.closePane('debug')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate on module', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('5')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '5',
Y: '0.1',
Z: '0.2',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
bracket
|> translate(x = 5, y = 0.1, z = 0.2)
`,
{ shouldNormalise: true }
)
// Expect translated part in the scene
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Set rotate on module', async () => {
await toolbar.closePane('code')
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '0.1',
Pitch: '0.2',
Yaw: '0.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
bracket
|> translate(x = 5, y = 0.1, z = 0.2)
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
`,
{ shouldNormalise: true }
)
// Expect no change in the scene as the rotations are tiny
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
}
)
test(
`Insert foreign parts into assembly as whole module import`,
{ tag: ['@electron'] },
@ -231,10 +405,6 @@ test.describe('Point-and-click assemblies tests', () => {
)
await scene.settled(cmdBar)
// TODO: remove this once #5780 is fixed
await page.reload()
await scene.settled(cmdBar)
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance)
@ -279,10 +449,6 @@ test.describe('Point-and-click assemblies tests', () => {
)
await scene.settled(cmdBar)
// TODO: remove this once #5780 is fixed
await page.reload()
await scene.settled(cmdBar)
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, partPoint, tolerance)

View File

@ -1,11 +1,14 @@
import type { Locator, Page } from '@playwright/test'
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Locator, Page } from '@playwright/test'
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
import {
orRunWhenFullSuiteEnabled,
runningOnWindows,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing point an click code gen functionality that's not sketch mode related
@ -1046,7 +1049,7 @@ openSketch = startSketchOn(XY)
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix( axis = X, radius = 5, length = 5, revolutions = 1, angleStart = 360, ccw = false,)`
const expectedOutput = `helix001 = helix( axis = X, radius = 5, length = 5, revolutions = 1, angleStart = 270, ccw = false,)`
const expectedLine = `axis=X,`
await homePage.goToModelingScene()
@ -1072,6 +1075,23 @@ openSketch = startSketchOn(XY)
await expect.poll(() => page.getByText('Axis').count()).toBe(6)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'angleStart',
highlightedHeaderArg: 'angleStart',
currentArgValue: '360',
headerArguments: {
Mode: 'Axis',
Axis: 'X',
Revolutions: '1',
AngleStart: '',
Length: '',
Radius: '',
CounterClockWise: '',
},
commandName: 'Helix',
})
await cmdBar.currentArgumentInput.locator('.cm-content').fill('270')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
@ -1080,7 +1100,7 @@ openSketch = startSketchOn(XY)
headerArguments: {
Mode: 'Axis',
Axis: 'X',
AngleStart: '360',
AngleStart: '270',
Revolutions: '1',
Length: '5',
Radius: '5',
@ -1115,7 +1135,7 @@ openSketch = startSketchOn(XY)
currentArgValue: '',
headerArguments: {
Axis: 'X',
AngleStart: '360',
AngleStart: '270',
Revolutions: '1',
Radius: '5',
Length: initialInput,
@ -1131,7 +1151,7 @@ openSketch = startSketchOn(XY)
stage: 'review',
headerArguments: {
Axis: 'X',
AngleStart: '360',
AngleStart: '270',
Revolutions: '1',
Radius: '5',
Length: newInput,
@ -3527,6 +3547,9 @@ tag=$rectangleSegmentC002,
toolbar,
cmdBar,
}) => {
if (runningOnWindows()) {
test.fixme(orRunWhenFullSuiteEnabled())
}
const initialCode = `sketch001 = startSketchOn(XZ)
|> startProfileAt([-102.57, 101.72], %)
|> angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)
@ -3818,4 +3841,469 @@ extrude001 = extrude(profile001, length = 100)
)
})
})
const translateExtrudeCases: { variables: boolean }[] = [
{
variables: false,
},
{
variables: true,
},
]
translateExtrudeCases.map(({ variables }) => {
test(`Set translate on extrude through right-click menu (variables: ${variables})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 800, y: 250 }
const bgColor: [number, number, number] = [50, 50, 50]
const partColor: [number, number, number] = [150, 150, 150]
const tolerance = 50
await test.step('Confirm extrude exists with default appearance', async () => {
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate through command bar flow', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('3')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.1')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.2',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
if (variables) {
await editor.expectEditor.toContain(
`
z001 = 0.2
y001 = 0.1
x001 = 3
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = x001, y = y001, z = z001)
`,
{ shouldNormalise: true }
)
} else {
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 3, y = 0.1, z = 0.2)
`,
{ shouldNormalise: true }
)
}
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Edit translate', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-translate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'z',
currentArgValue: variables ? 'z001' : '0.2',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.2',
},
highlightedHeaderArg: 'z',
commandName: 'Translate',
})
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
X: '3',
Y: '0.1',
Z: '0.3',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(`z = 0.3`)
// Expect almost no change in scene
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
})
})
const rotateExtrudeCases: { variables: boolean }[] = [
{
variables: false,
},
{
variables: true,
},
]
rotateExtrudeCases.map(({ variables }) => {
test(`Set rotate on extrude through right-click menu (variables: ${variables})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Set rotate through command bar flow', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('1.1')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('1.2')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await page.keyboard.insertText('1.3')
if (variables) {
await cmdBar.createNewVariable()
}
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '1.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
if (variables) {
await editor.expectEditor.toContain(
`
yaw001 = 1.3
pitch001 = 1.2
roll001 = 1.1
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> rotate(roll = roll001, pitch = pitch001, yaw = yaw001)
`,
{ shouldNormalise: true }
)
} else {
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> rotate(roll = 1.1, pitch = 1.2, yaw = 1.3)
`,
{ shouldNormalise: true }
)
}
})
await test.step('Edit rotate', async () => {
await toolbar.openPane('feature-tree')
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-set-rotate').click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'yaw',
currentArgValue: variables ? 'yaw001' : '1.3',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '1.3',
},
highlightedHeaderArg: 'yaw',
commandName: 'Rotate',
})
await page.keyboard.insertText('13')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Roll: '1.1',
Pitch: '1.2',
Yaw: '13',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.toContain(`yaw = 13`)
})
})
})
test(`Set translate and rotate on extrude through selection`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// One dumb hardcoded screen pixel value
const midPoint = { x: 500, y: 250 }
const moreToTheRightPoint = { x: 800, y: 250 }
const bgColor: [number, number, number] = [50, 50, 50]
const partColor: [number, number, number] = [150, 150, 150]
const tolerance = 50
const [clickMidPoint] = scene.makeMouseHelpers(midPoint.x, midPoint.y)
const [clickMoreToTheRightPoint] = scene.makeMouseHelpers(
moreToTheRightPoint.x,
moreToTheRightPoint.y
)
await test.step('Confirm extrude exists with default appearance', async () => {
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
})
await test.step('Set translate through command bar flow', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Translate')
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Translate',
})
await clickMidPoint()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'x',
currentArgValue: '0',
headerArguments: {
Selection: '1 path',
X: '',
Y: '',
Z: '',
},
highlightedHeaderArg: 'x',
commandName: 'Translate',
})
await page.keyboard.insertText('2')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 path',
X: '2',
Y: '0',
Z: '0',
},
commandName: 'Translate',
})
await cmdBar.progressCmdBar()
})
await test.step('Confirm code and scene have changed', async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 2, y = 0, z = 0)
`,
{ shouldNormalise: true }
)
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
await test.step('Set rotate through command bar flow', async () => {
// clear selection
await clickMidPoint()
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Rotate')
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Rotate',
})
await clickMoreToTheRightPoint()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'roll',
currentArgValue: '0',
headerArguments: {
Selection: '1 path',
Roll: '',
Pitch: '',
Yaw: '',
},
highlightedHeaderArg: 'roll',
commandName: 'Rotate',
})
await page.keyboard.insertText('0.1')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.2')
await cmdBar.progressCmdBar()
await page.keyboard.insertText('0.3')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 path',
Roll: '0.1',
Pitch: '0.2',
Yaw: '0.3',
},
commandName: 'Rotate',
})
await cmdBar.progressCmdBar()
})
await test.step('Confirm code has changed', async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [0, 0], radius = 1)
extrude001 = extrude(profile001, length = 1)
|> translate(x = 2, y = 0, z = 0)
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
`,
{ shouldNormalise: true }
)
// No change here since the angles are super small
await scene.expectPixelColor(bgColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
})
})
})

View File

@ -1,7 +1,7 @@
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants'
import fs from 'fs'
import fsp from 'fs/promises'
import path from 'path'
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants'
import fsp from 'fs/promises'
import type { Paths } from '@e2e/playwright/test-utils'
import {

View File

@ -1,4 +1,5 @@
import { expect, test } from '@e2e/playwright/zoo-test'
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
/* eslint-disable jest/no-conditional-expect */
@ -57,6 +58,7 @@ test.describe('edit with AI example snapshots', () => {
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)

View File

@ -1,8 +1,8 @@
import path from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import type { Page } from '@playwright/test'
import { reportRejection } from '@src/lib/trap'
import * as fsp from 'fs/promises'
import path from 'path'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from '@e2e/playwright/storageStates'
import type { TestColor } from '@e2e/playwright/test-utils'
@ -304,8 +304,9 @@ extrude001 = extrude(sketch001, length = 50)
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-content'))
.toContainText(`exampleSketch = startSketchOn("XZ")
await expect(
page.locator('.cm-content')
).toContainText(`exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine(angle = 50, length = 45 )
|> yLine(endAbsolute = 0)

View File

@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { roundOff, uuidv4 } from '@src/lib/utils'
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Page } from '@playwright/test'
import { roundOff, uuidv4 } from '@src/lib/utils'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
@ -161,7 +161,8 @@ sketch001 = startSketchOn(XZ)
// click to add segment
await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
await expect
.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`@settings(defaultLengthUnit = in)
@ -403,8 +404,9 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await u.openKclCodePanel()
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn(XZ)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97])
|> tangentialArc(endAbsolute = [26.92, -3.32])
@ -1363,73 +1365,6 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
})
})
test.describe('Sketch mode should be toleratant to syntax errors', () => {
test(
'adding a syntax error, recovers after fixing',
{ tag: ['@skipWin'] },
async ({ page, homePage, context, scene, editor, toolbar }) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 706, y: 129 } as const
const arrowHeadWhite = TEST_COLORS.WHITE
const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15)
await test.step('check chamfer selection changes cursor positon', async () => {
await expect(async () => {
// sometimes initial click doesn't register
await objClick()
await editor.expectActiveLinesToBe([
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
])
}).toPass({ timeout: 15_000, intervals: [500] })
})
await test.step('enter sketch and sanity check segments have been drawn', async () => {
await toolbar.editSketch()
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
await editor.replaceCode('line(endAbsolute = [pro', 'badBadBadFn([pro')
await editor.expectState({
activeLines: [],
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
highlightedCode: '',
})
// this checks sketch segments have failed to be drawn
await verifyArrowHeadColor(backgroundGray)
})
await test.step('', async () => {
await editor.replaceCode('badBadBadFn([pro', 'line(endAbsolute = [pro')
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: '',
})
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await page.waitForTimeout(100)
}
)
})
test.describe(`Sketching with offset planes`, () => {
test(`Can select an offset plane to sketch on`, async ({
context,
@ -1623,6 +1558,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile002 = startProfileAt([40.68, 87.67], sketch001)
|> xLine(length = 239.17)
@ -3109,3 +3045,337 @@ profile001 = startProfileAt([0, 0], sketch001)
)
})
})
test.describe('manual edits during sketch mode', () => {
test('Can edit sketch through feature tree with variable modifications', async ({
page,
context,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
})
await test.step('Add new variable and wait for re-execution', async () => {
await page.waitForTimeout(500) // wait for deferred execution
await editor.replaceCode('myVar2 = 6', 'myVar2 = 6\nmyVar3 = 7')
await page.waitForTimeout(2000) // wait for deferred execution
})
const handle1Location = { x: 843, y: 235 }
await test.step('Edit sketch by dragging handle', async () => {
await page.waitForTimeout(500)
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 156.54, angle = -28')
await page.mouse.move(handle1Location.x, handle1Location.y)
await page.mouse.down()
await page.mouse.move(
handle1Location.x + 50,
handle1Location.y + 50,
{
steps: 5,
}
)
await page.mouse.up()
await editor.expectEditor.toContain('length = 231.59, angle = -34')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('Delete variables and wait for re-execution', async () => {
await page.waitForTimeout(500)
await editor.replaceCode('myVar3 = 7', '')
await page.waitForTimeout(50)
await editor.replaceCode('myVar2 = 6', '')
await page.waitForTimeout(2000) // Wait for deferred execution
})
const handle2Location = { x: 872, y: 273 }
await test.step('Edit sketch again', async () => {
await editor.expectEditor.toContain('length = 231.59, angle = -34')
await page.waitForTimeout(500)
await expect
.poll(
async () => {
await page.mouse.move(handle2Location.x, handle2Location.y)
await page.mouse.down()
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 167.36, angle = -14')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('add whole other sketch before current sketch', async () => {
await page.waitForTimeout(500)
await editor.replaceCode(
`myVar1 = 5`,
`myVar1 = 5
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)`
)
await page.waitForTimeout(2000) // Wait for deferred execution
})
const handle3Location = { x: 844, y: 212 }
await test.step('edit sketch again', async () => {
await page.waitForTimeout(500) // Wait for deferred execution
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 167.36, angle = -14')
await page.mouse.move(handle3Location.x, handle3Location.y)
await page.mouse.down()
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 219.2, angle = -56')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
// exit sketch and assert whole code
await test.step('Exit sketch and assert code', async () => {
await toolbar.exitSketch()
await editor.expectEditor.toContain(
`myVar1 = 5
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 219.2, angle = -56)
|> angledLine(angle = -151, length = 116.27)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])
`,
{ shouldNormalise: true }
)
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: '',
})
})
})
test('Will exit out of sketch mode for some incompatible edits', async ({
page,
context,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([106.68, 89.77], sketch001)
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfileAt([83.39, 329.15], sketch002)
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
const expectSketchOriginToBeDrawn = async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
}
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
await expectSketchOriginToBeDrawn()
})
await test.step('rename variable of current sketch, sketch002 to changeSketchNamePartWayThrough', async () => {
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await page.waitForTimeout(100)
// three times to rename the declaration and it's use
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await page.waitForTimeout(100)
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await expect(
page.getByText('Unable to maintain sketch mode')
).toBeVisible()
})
})
test(
'adding a syntax error, recovers after fixing',
{ tag: ['@skipWin'] },
async ({ page, homePage, context, scene, editor, toolbar, cmdBar }) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 706, y: 129 } as const
const arrowHeadWhite = TEST_COLORS.WHITE
const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15)
// wait for scene to load
await scene.settled(cmdBar)
await test.step('check chamfer selection changes cursor positon', async () => {
await expect(async () => {
// sometimes initial click doesn't register
await objClick()
await editor.expectActiveLinesToBe([
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
])
}).toPass({ timeout: 15_000, intervals: [500] })
})
await test.step('enter sketch and sanity check segments have been drawn', async () => {
await toolbar.editSketch()
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
await editor.replaceCode(
'line(endAbsolute = [pro',
'badBadBadFn(endAbsolute = [pro'
)
await editor.expectState({
activeLines: [],
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
highlightedCode: '',
})
await expect(
page.getByText(
"Error in kcl script, sketch cannot be drawn until it's fixed"
)
).toBeVisible()
// this checks sketch segments have failed to be drawn
await verifyArrowHeadColor(backgroundGray)
})
await test.step('', async () => {
await editor.replaceCode(
'badBadBadFn(endAbsolute = [pro',
'line(endAbsolute = [pro'
)
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: '',
})
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await test.step('make a change to the code and expect pixel color to change', async () => {
// defends against a regression where sketch would duplicate in the scene
// https://github.com/KittyCAD/modeling-app/issues/6345
await editor.replaceCode(
'startProfileAt([75.8, 317.2',
'startProfileAt([75.8, 217.2'
)
// expect not white anymore
await scene.expectPixelColorNotToBe(
TEST_COLORS.WHITE,
arrowHeadLocation,
15
)
})
}
)
})

View File

@ -1,9 +1,9 @@
import { spawn } from 'child_process'
import path from 'path'
import type { Models } from '@kittycad/lib'
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
import { spawn } from 'child_process'
import fsp from 'fs/promises'
import JSZip from 'jszip'
import path from 'path'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
@ -13,6 +13,8 @@ import type { Paths } from '@e2e/playwright/test-utils'
import {
doExport,
getUtils,
headerMasks,
networkingMasks,
orRunWhenFullSuiteEnabled,
settingsToToml,
} from '@e2e/playwright/test-utils'
@ -366,7 +368,7 @@ const extrudeDefaultPlane = async (
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await u.openKclCodePanel()
}
@ -375,8 +377,7 @@ test.describe(
'extrude on default planes should be stable',
{ tag: '@snapshot' },
() => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
test('XY', async ({ page, context, cmdBar, scene }) => {
await extrudeDefaultPlane(context, page, cmdBar, scene, 'XY')
@ -408,6 +409,7 @@ test(
'Draft segments should look right',
{ tag: '@snapshot' },
async ({ page, scene, toolbar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -454,7 +456,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
const lineEndClick = () =>
@ -483,7 +485,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await endOfTangentClk()
@ -493,7 +495,7 @@ test(
await threePointArcMidPointMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await threePointArcMidPointClk()
await page.waitForTimeout(100)
@ -502,7 +504,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await threePointArcEndPointClk()
@ -522,7 +524,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await arcEndClk()
}
@ -532,8 +534,7 @@ test(
'Draft rectangles should look right',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -572,7 +573,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
}
)
@ -615,7 +616,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [366.89, -62.01], radius = 1)`
@ -627,8 +628,7 @@ test.describe(
'Client side scene scale should match engine scale',
{ tag: '@snapshot' },
() => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
test('Inch scale', async ({ page, cmdBar, scene }) => {
const u = await getUtils(page)
@ -688,7 +688,7 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
await u.doAndWaitForImageDiff(
@ -701,7 +701,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
})
@ -781,6 +781,7 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
})
// exit sketch
@ -794,6 +795,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
})
})
}
@ -803,8 +805,7 @@ test(
'Sketch on face with none z-up',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
@ -855,7 +856,7 @@ part002 = startSketchOn(part001, face = seg01)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
}
)
@ -864,8 +865,7 @@ test(
'Zoom to fit on load - solid 2d',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
@ -894,7 +894,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
}
)
@ -903,8 +903,7 @@ test(
'Zoom to fit on load - solid 3d',
{ tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
@ -934,7 +933,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
}
)
@ -945,13 +944,9 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
cmdBar,
scene,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
@ -1004,18 +999,14 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
mask: [...headerMasks(page), ...networkingMasks(page)],
})
})
test('Grid turned off', async ({ page, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
@ -1030,11 +1021,12 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
mask: [...headerMasks(page), ...networkingMasks(page)],
})
})
test('Grid turned on', async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
@ -1055,11 +1047,6 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
@ -1074,7 +1061,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
mask: [...headerMasks(page), ...networkingMasks(page)],
})
})
})
@ -1143,11 +1130,13 @@ test('theme persists', async ({ page, context }) => {
await expect(page, 'expect screenshot to have light theme').toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
})
})
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context, scene, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
@ -1158,9 +1147,9 @@ test.describe('code color goober', { tag: '@snapshot' }, () => {
sweepPath = startSketchOn(XZ)
|> startProfileAt([0.05, 0.05], %)
|> line(end = [0, 7])
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7])
sweepSketch = startSketchOn(XY)
@ -1187,7 +1176,7 @@ sweepSketch = startSketchOn(XY)
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
})
@ -1207,9 +1196,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ)
|> startProfileAt([0.05, 0.05], %)
|> line(end = [0, 7])
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0])
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7])
sweepSketch = startSketchOn(XY)
@ -1244,7 +1233,7 @@ sweepSketch = startSketchOn(XY)
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -29,6 +29,5 @@
}
}
],
"project_name": "testDefault",
"kcl_version": "0.2.61"
}

View File

@ -126,8 +126,9 @@ test.describe('Test network and connection issues', () => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(length = ${commonPoints.num1})`)
// Expect the network to be up
@ -215,7 +216,8 @@ test.describe('Test network and connection issues', () => {
await page.waitForTimeout(100)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(length = 12.34)
@ -225,7 +227,8 @@ profile001 = startProfileAt([12.34, -12.34], sketch001)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(length = 12.34)

View File

@ -1,3 +1,4 @@
import path from 'path'
import * as TOML from '@iarna/toml'
import type { Models } from '@kittycad/lib'
import type { BrowserContext, Locator, Page, TestInfo } from '@playwright/test'
@ -9,7 +10,6 @@ import { reportRejection } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import { isArray } from '@src/lib/utils'
import fsp from 'fs/promises'
import path from 'path'
import pixelMatch from 'pixelmatch'
import type { Protocol } from 'playwright-core/types/protocol'
import { PNG } from 'pngjs'
@ -29,6 +29,17 @@ const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '')
}
export const headerMasks = (page: Page) => [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
export const networkingMasks = (page: Page) => [
page.getByTestId('model-state-indicator'),
page.getByTestId('network-toggle'),
]
export type TestColor = [number, number, number]
export const TEST_COLORS: { [key: string]: TestColor } = {
WHITE: [249, 249, 249],

View File

@ -1,6 +1,6 @@
import path from 'node:path'
import { XOR } from '@src/lib/utils'
import * as fsp from 'fs/promises'
import path from 'node:path'
import {
TEST_COLORS,

View File

@ -1,7 +1,7 @@
import { join } from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import { FILE_EXT } from '@src/lib/constants'
import * as fsp from 'fs/promises'
import { join } from 'path'
import type { CmdBarSerialised } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'

View File

@ -78,22 +78,25 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
await expect(
page.locator('.cm-content')
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(length = ${commonPoints.num1})`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
await expect(
page.locator('.cm-content')
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(length = ${commonPoints.num1})
|> yLine(length = ${commonPoints.num1 + 0.01})`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
await expect(
page.locator('.cm-content')
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt
}, sketch001)
|> xLine(length = ${commonPoints.num1})

View File

@ -1,3 +1,4 @@
import { join } from 'path'
import {
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
@ -5,7 +6,6 @@ import {
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
import type { DeepPartial } from '@src/lib/types'
import * as fsp from 'fs/promises'
import { join } from 'path'
import type { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -20,6 +20,7 @@ import {
createProject,
executorInputPath,
getUtils,
networkingMasks,
orRunWhenFullSuiteEnabled,
tomlToSettings,
} from '@e2e/playwright/test-utils'
@ -1036,7 +1037,7 @@ fn cube`
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
}
)
@ -1053,7 +1054,7 @@ fn cube`
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: [page.getByTestId('model-state-indicator')],
mask: networkingMasks(page),
}
)
})

View File

@ -1,6 +1,6 @@
import type { Page } from '@playwright/test'
import fs from 'fs'
import { join } from 'path'
import type { Page } from '@playwright/test'
import {
createProject,

View File

@ -44,7 +44,9 @@
packages =
(with pkgs; [
rustToolchain
cargo-criterion
cargo-nextest
cargo-sort
just
postgresql.lib
openssl
@ -67,6 +69,7 @@
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = "${pkgs.playwright-driver.browsers}/chromium-1091/chrome-linux/chrome";
PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}";
NODE_ENV = "development";
RUSTFMT = "${pkgs.rust-bin.stable.latest.rustfmt}/bin/rustfmt";
};
});

View File

@ -1,8 +1,8 @@
import path from 'path'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { VitePlugin } from '@electron-forge/plugin-vite'
import type { ForgeConfig } from '@electron-forge/shared-types'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
import path from 'path'
const rootDir = process.cwd()

4
interface.d.ts vendored
View File

@ -1,10 +1,10 @@
import { MachinesListing } from 'components/MachineManagerProvider'
import 'electron'
import fs from 'node:fs/promises'
import path from 'path'
import { dialog, shell } from 'electron'
import type { WebContentSendPayload } from 'menu/channels'
import { ZooLabel } from 'menu/roles'
import fs from 'node:fs/promises'
import path from 'path'
// Extend the interface with additional custom properties
declare module 'electron' {

243
package-lock.json generated
View File

@ -76,6 +76,7 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.26.9",
"@biomejs/biome": "^1.9.4",
"@electron-forge/cli": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-vite": "^7.8.0",
@ -87,7 +88,6 @@
"@playwright/test": "^1.51.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/diff": "^7.0.2",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
@ -131,8 +131,6 @@
"pngjs": "^7.0.0",
"postcss": "^8.4.43",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.17",
"ts-node": "^10.0.0",
@ -1926,6 +1924,170 @@
"node": ">=6.9.0"
}
},
"node_modules/@biomejs/biome": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4",
"@biomejs/cli-darwin-x64": "1.9.4",
"@biomejs/cli-linux-arm64": "1.9.4",
"@biomejs/cli-linux-arm64-musl": "1.9.4",
"@biomejs/cli-linux-x64": "1.9.4",
"@biomejs/cli-linux-x64-musl": "1.9.4",
"@biomejs/cli-win32-arm64": "1.9.4",
"@biomejs/cli-win32-x64": "1.9.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
@ -5038,41 +5200,6 @@
"node": ">= 10"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
"integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"javascript-natural-sort": "^0.7.1",
"lodash": "^4.17.21"
},
"engines": {
"node": ">18.12"
},
"peerDependencies": {
"@vue/compiler-sfc": "3.x",
"prettier": "2.x - 3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "4.x || 5.x"
},
"peerDependenciesMeta": {
"@vue/compiler-sfc": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
},
"svelte": {
"optional": true
}
}
},
"node_modules/@ts-stack/markdown": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@ts-stack/markdown/-/markdown-1.5.0.tgz",
@ -13349,13 +13476,6 @@
"node": ">=10"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-diff": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
@ -16552,39 +16672,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"vue-tsc": {
"optional": true
}
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",

View File

@ -96,9 +96,10 @@
"simpleserver:stop": "kill-port 3000",
"tsc": "tsc",
"playwright": "playwright",
"fmt": "prettier --write .eslintrc.json ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg",
"fmt:generated": "prettier --write .eslintrc.json *.ts *.json *.js ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg",
"fmt-check": "prettier --check .eslintrc.json ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server",
"fmt": "biome format --write .eslintrc.json ./src *.ts *.js ./e2e ./packages ./rust/kcl-language-server ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg",
"fmt:check": "biome check --write --organize-imports-enabled=true --formatter-enabled=false --linter-enabled=false --files-ignore-unknown=true ./src *.ts *.js ./e2e ./packages ./rust/kcl-language-server ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg",
"organize-imports": "biome check --write --organize-imports-enabled=true --formatter-enabled=false --linter-enabled=false ./src ./e2e ./packages --files-ignore-unknown=true",
"check": "biome check ./src ./e2e ./packages/codemirror-lsp-client/src ./rust/kcl-language-server/client/src",
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
"fetch:wasm:windows": "powershell -ExecutionPolicy Bypass -File ./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",
@ -131,7 +132,7 @@
"tronb:package:prod": "npm run tronb:vite:prod && electron-builder --config electron-builder.yml --publish always",
"test-setup": "npm install && npm run build:wasm",
"test": "vitest --mode development",
"test:snapshots": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --update-snapshots --grep=@snapshot --trace=on --shard=1/1",
"test:snapshots": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --grep=@snapshot --trace=on --shard=1/1",
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
@ -146,32 +147,8 @@
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "npm run simpleserver:bg && npm run test:unit:kcl-samples; kill-port 3000"
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^@rust/(.*)$",
"^@public/(.*)$",
"^@e2e/(.*)$",
"^@src/(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-organize-imports"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
@ -181,6 +158,7 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.26.9",
"@biomejs/biome": "^1.9.4",
"@electron-forge/cli": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-vite": "^7.8.0",
@ -192,7 +170,6 @@
"@playwright/test": "^1.51.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/diff": "^7.0.2",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
@ -236,8 +213,6 @@
"pngjs": "^7.0.0",
"postcss": "^8.4.43",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.17",
"ts-node": "^10.0.0",

View File

@ -32,7 +32,5 @@
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.1"
},
"files": [
"dist/"
]
"files": ["dist/"]
}

View File

@ -1,6 +1,6 @@
import { fileTests } from '@lezer/generator/dist/test'
import * as fs from 'fs'
import * as path from 'path'
import { fileTests } from '@lezer/generator/dist/test'
import { KclLanguage } from '../src/index'

View File

@ -1,5 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import os from 'os'
import { defineConfig, devices } from '@playwright/test'
const platform = os.platform() // 'linux' (Ubuntu), 'darwin' (macOS), 'win32' (Windows)

View File

@ -1,5 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import os from 'os'
import { defineConfig, devices } from '@playwright/test'
const platform = os.platform() // 'linux' (Ubuntu), 'darwin' (macOS), 'win32' (Windows)
@ -20,7 +20,7 @@ if (process.env.E2E_WORKERS) {
case 'darwin':
case 'win32':
default:
workers = '25%' // Lower concurrency for heavier Electron processes
workers = '40%' // Lower concurrency for heavier Electron processes
break
}
}

View File

@ -25,10 +25,14 @@ When you submit a PR to add or modify KCL samples, images and STEP files will be
---
#### [80-20-rail](80-20-rail/main.kcl) ([screenshot](screenshots/80-20-rail.png))
[![80-20-rail](screenshots/80-20-rail.png)](80-20-rail/main.kcl)
#### [axial-fan](axial-fan/main.kcl) ([screenshot](screenshots/axial-fan.png))
[![axial-fan](screenshots/axial-fan.png)](axial-fan/main.kcl)
#### [ball-bearing](ball-bearing/main.kcl) ([screenshot](screenshots/ball-bearing.png))
[![ball-bearing](screenshots/ball-bearing.png)](ball-bearing/main.kcl)
#### [bench](bench/main.kcl) ([screenshot](screenshots/bench.png))
[![bench](screenshots/bench.png)](bench/main.kcl)
#### [bottle](bottle/main.kcl) ([screenshot](screenshots/bottle.png))
[![bottle](screenshots/bottle.png)](bottle/main.kcl)
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
[![bracket](screenshots/bracket.png)](bracket/main.kcl)
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))

View File

@ -0,0 +1,153 @@
// Fan Housing
// The plastic housing that contains the fan and the motor
// Set units
@settings(defaultLengthUnit = mm)
// Import parameters
import * from "parameters.kcl"
// Model the housing which holds the motor, the fan, and the mounting provisions
// Bottom mounting face
bottomFaceSketch = startSketchOn(XY)
|> startProfileAt([-fanSize / 2, -fanSize / 2], %)
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = fanSize, tag = $rectangleSegmentB001)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)
|> close()
|> hole(circle(center = [0, 0], radius = 4), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> extrude(length = 4)
// Add large openings to the bottom face to allow airflow through the fan
airflowPattern = startSketchOn(bottomFaceSketch, face = END)
|> startProfileAt([fanSize * 7 / 25, -fanSize * 9 / 25], %)
|> angledLine(angle = 140, length = fanSize * 12 / 25, tag = $seg01)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = -130, length = fanSize * 8 / 25)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = segAng(seg01) + 180, length = fanSize * 2 / 25)
|> tangentialArc(radius = fanSize * 8 / 25, angle = 40)
|> xLine(length = fanSize * 3 / 25)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternCircular2d(
instances = 4,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
|> extrude(length = -4)
// Create the middle segment of the fan housing body
housingMiddleLength = fanSize / 3
housingMiddleRadius = fanSize / 3 - 1
bodyMiddle = startSketchOn(bottomFaceSketch, face = END)
|> startProfileAt([
housingMiddleLength / 2,
-housingMiddleLength / 2 - housingMiddleRadius
], %)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> xLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> extrude(length = fanHeight - 4 - 4)
// Cut a hole in the body to accommodate the fan
bodyFanHole = startSketchOn(bodyMiddle, face = END)
|> circle(center = [0, 0], radius = fanSize * 23 / 50)
|> extrude(length = -(fanHeight - 4 - 4))
// Top mounting face. Cut a hole in the face to accommodate the fan
topFaceSketch = startSketchOn(bodyMiddle, face = END)
topHoles = startProfileAt([-fanSize / 2, -fanSize / 2], topFaceSketch)
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) + 90, length = fanSize, tag = $rectangleSegmentB002)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002), tag = $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD002)
|> close()
|> hole(circle(center = [0, 0], radius = fanSize * 23 / 50), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> hole(circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
), %)
|> extrude(length = 4)
// Create a housing for the electric motor to sit
motorHousing = startSketchOn(bottomFaceSketch, face = END)
|> circle(center = [0, 0], radius = 11.2)
|> extrude(length = 16)
startSketchOn(motorHousing, face = END)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = -16)
|> appearance(color = "#a55e2c")
|> fillet(
radius = abs(fanSize - mountingHoleSpacing) / 2,
tags = [
getNextAdjacentEdge(rectangleSegmentA001),
getNextAdjacentEdge(rectangleSegmentB001),
getNextAdjacentEdge(rectangleSegmentC001),
getNextAdjacentEdge(rectangleSegmentD001),
getNextAdjacentEdge(rectangleSegmentA002),
getNextAdjacentEdge(rectangleSegmentB002),
getNextAdjacentEdge(rectangleSegmentC002),
getNextAdjacentEdge(rectangleSegmentD002)
],
)

View File

@ -0,0 +1,92 @@
// Fan
// Spinning axial fan that moves airflow
// Set units
@settings(defaultLengthUnit = mm)
// Import parameters
import * from "parameters.kcl"
// Model the center of the fan
fanCenter = startSketchOn(XZ)
|> startProfileAt([-0.0001, fanHeight], %)
|> xLine(endAbsolute = -15 + 1.5)
|> tangentialArc(radius = 1.5, angle = 90)
|> yLine(endAbsolute = 4.5)
|> xLine(endAbsolute = -13)
|> yLine(endAbsolute = profileStartY(%) - 5)
|> tangentialArc(radius = 1, angle = -90)
|> xLine(endAbsolute = -1)
|> yLine(length = 2)
|> xLine(length = -0.15)
|> line(endAbsolute = [
profileStartX(%) - 1,
profileStartY(%) - 1.4
])
|> xLine(endAbsolute = profileStartX(%))
|> yLine(endAbsolute = profileStartY(%))
|> close()
|> revolve(axis = {
direction = [0.0, 1.0],
origin = [0.0, 0.0]
})
|> appearance(color = "#f3e2d8")
// Create a function for a lofted fan blade cross section that rotates about the center hub of the fan
fn fanBlade(offsetHeight, startAngle) {
fanBlade = startSketchOn(offsetPlane(XY, offset = offsetHeight))
|> startProfileAt([
15 * cos(toRadians(startAngle)),
15 * sin(toRadians(startAngle))
], %)
|> arc({
angleStart = startAngle,
angleEnd = startAngle + 14,
radius = 15
}, %)
|> arcTo({
end = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 20)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 20))
],
interior = [
fanSize * 11 / 50 * cos(toRadians(startAngle + 3)),
fanSize * 11 / 50 * sin(toRadians(startAngle + 3))
]
}, %)
|> arcTo({
end = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 24)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 24))
],
interior = [
fanSize * 22 / 50 * cos(toRadians(startAngle - 22)),
fanSize * 22 / 50 * sin(toRadians(startAngle - 22))
]
}, %)
|> arcTo({
end = [profileStartX(%), profileStartY(%)],
interior = [
fanSize * 11 / 50 * cos(toRadians(startAngle - 5)),
fanSize * 11 / 50 * sin(toRadians(startAngle - 5))
]
}, %)
|> close()
return fanBlade
}
// Loft the fan blade cross sections into a single blade, then pattern them about the fan center
loft([
fanBlade(4.5, 50),
fanBlade((fanHeight - 2 - 4) / 2, 30),
fanBlade(fanHeight - 2, 0)
])
|> appearance(color = "#f3e2d8")
|> patternCircular3d(
%,
instances = 9,
axis = [0, 0, 1],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)

View File

@ -0,0 +1,15 @@
// PC Fan
// A small axial fan, used to push or draw airflow over components to remove excess heat
// Set units
@settings(defaultLengthUnit = mm)
// Import all parts into assembly file
import "fan-housing.kcl" as fanHousing
import "motor.kcl" as motor
import "fan.kcl" as fan
// Produce the model for each imported part
fanHousing
motor
fan

View File

@ -0,0 +1,22 @@
// Motor
// A small electric motor to power the fan
// Set Units
@settings(defaultLengthUnit = mm)
// Import Parameters
import * from "parameters.kcl"
// Model the motor body and stem
topFacePlane = offsetPlane(XY, offset = 4)
motorBody = startSketchOn(topFacePlane)
|> circle(center = [0, 0], radius = 10, tag = $seg04)
|> extrude(length = 17)
|> appearance(color = "#021b55")
|> fillet(radius = 2, tags = [getOppositeEdge(seg04), seg04])
startSketchOn(offsetPlane(XY, offset = 21))
|> circle(center = [0, 0], radius = 1)
|> extrude(length = 3.8)
|> appearance(color = "#dbc89e")

View File

@ -0,0 +1,10 @@
// Global parameters for the axial fan
// Set units
@settings(defaultLengthUnit = mm)
// Define Parameters
export fanSize = 120
export fanHeight = 25
export mountingHoleSpacing = 105
export mountingHoleSize = 4.5

View File

@ -0,0 +1,35 @@
// Bottle
// A simple bottle with a hollow, watertight interior
// Set Units
@settings(defaultLengthUnit = mm)
// Input dimensions to define the bottle
bottleWidth = 80
bottleLength = 125
bottleHeight = 220
neckDepth = 18
neckDiameter = 45
wallThickness = 4
// Create a rounded body for the bottle
bottleBody = startSketchOn(XY)
|> startProfileAt([-bottleLength / 2, 0], %)
|> yLine(length = bottleWidth / 3)
|> arcTo({
end = [bottleLength / 2, bottleWidth / 3],
interior = [0, bottleWidth / 2]
}, %)
|> yLine(endAbsolute = 0)
|> mirror2d(axis = X)
|> close()
|> extrude(length = bottleHeight - neckDepth)
// Create a neck centered at the top of the bottle
bottleNeck = startSketchOn(bottleBody, face = END)
|> circle(center = [0, 0], radius = neckDiameter / 2)
|> extrude(length = neckDepth)
// Define a shell operation so that the entire body and neck are hollow, with only the top face opened
bottleShell = shell(bottleNeck, faces = [END], thickness = wallThickness)
|> appearance(%, color = "#0078c2")

View File

@ -6,6 +6,13 @@
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "axial-fan/main.kcl",
"multipleFiles": true,
"title": "PC Fan",
"description": "A small axial fan, used to push or draw airflow over components to remove excess heat"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
@ -20,6 +27,13 @@
"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": "bottle/main.kcl",
"multipleFiles": false,
"title": "Bottle",
"description": "A simple bottle with a hollow, watertight interior"
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

119
rust/Cargo.lock generated
View File

@ -665,9 +665,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@ -758,7 +758,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
@ -772,7 +772,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
@ -847,7 +847,7 @@ dependencies = [
"backtrace",
"lazy_static",
"mintex",
"parking_lot",
"parking_lot 0.12.3",
"rustc-hash 1.1.0",
"serde",
"serde_json",
@ -1697,6 +1697,18 @@ dependencies = [
"similar",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@ -1780,7 +1792,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"anyhow",
"clap",
@ -1791,7 +1803,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"Inflector",
"anyhow",
@ -1810,7 +1822,7 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"proc-macro2",
"quote",
@ -1819,7 +1831,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.61"
version = "0.2.62"
dependencies = [
"anyhow",
"clap",
@ -1840,7 +1852,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"anyhow",
"clap",
@ -1860,7 +1872,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.61"
version = "0.2.62"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1885,6 +1897,7 @@ dependencies = [
"image",
"indexmap 2.8.0",
"insta",
"instant",
"itertools 0.13.0",
"js-sys",
"kcl-derive-docs",
@ -1907,6 +1920,7 @@ dependencies = [
"serde_json",
"sha2",
"tabled",
"tempdir",
"thiserror 2.0.12",
"tokio",
"tokio-tungstenite",
@ -1920,6 +1934,7 @@ dependencies = [
"validator",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-timer",
"web-sys",
"web-time",
"winnow 0.6.24",
@ -1928,7 +1943,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.61"
version = "0.3.62"
dependencies = [
"anyhow",
"kcl-lib",
@ -1943,7 +1958,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -1956,7 +1971,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"anyhow",
"async-trait",
@ -1970,7 +1985,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.61"
version = "0.1.62"
dependencies = [
"bson",
"console_error_panic_hook",
@ -2459,6 +2474,17 @@ dependencies = [
"unicode-width 0.2.0",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -2466,7 +2492,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
@ -2477,7 +2517,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.10",
"smallvec",
"windows-targets 0.52.6",
]
@ -3017,6 +3057,15 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
@ -3084,6 +3133,15 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.12.15"
@ -3779,6 +3837,16 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand 0.4.6",
"remove_dir_all",
]
[[package]]
name = "tempfile"
version = "3.19.0"
@ -3964,7 +4032,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"parking_lot 0.12.3",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -4561,6 +4629,21 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasm-timer"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
dependencies = [
"futures",
"js-sys",
"parking_lot 0.11.2",
"pin-utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.77"

View File

@ -22,7 +22,6 @@ debug = 0
[profile.dev.package]
insta = { opt-level = 3 }
similar = { opt-level = 3 }
[profile.test]
debug = "line-tables-only"

View File

@ -33,10 +33,16 @@ new-sim-test test_name render_to_png="true":
# Run a KCL deterministic simulation test case and accept output.
overwrite-sim-test-sample test_name:
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::parse_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::unparse_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::kcl_test_execute_{{test_name}}
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::kcl_samples::test_after_engine_generate_manifest
overwrite-sim-test test_name:
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::parse
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::unparse
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::kcl_test_execute
[ {{test_name}} != "kcl_samples" ] || EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib --no-quiet -- simulation_tests::{{test_name}}::test_after_engine_generate_manifest
# Regenerate all the simulation test output.
redo-sim-tests:

View File

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

View File

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

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